Merge "Add support for playing CEA-608/708 CC tracks" into androidx-master-dev
diff --git a/.gitignore b/.gitignore
index 07ddfed..a19466e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,6 +22,7 @@
 buildSrc/build
 lifecycle/common/build
 jacoco.exec
+studio/
 
 # When Gradle configuration fails with the --profile option enabled it creates this folder.
 /reports
diff --git a/activity/api/1.0.0-alpha01.txt b/activity/api/1.0.0-alpha01.txt
index f5bed80..4f1f238 100644
--- a/activity/api/1.0.0-alpha01.txt
+++ b/activity/api/1.0.0-alpha01.txt
@@ -6,7 +6,6 @@
     method public void addOnBackPressedCallback(androidx.activity.OnBackPressedCallback);
     method public void addOnBackPressedCallback(androidx.lifecycle.LifecycleOwner, androidx.activity.OnBackPressedCallback);
     method @Deprecated public Object? getLastCustomNonConfigurationInstance();
-    method public androidx.lifecycle.Lifecycle getLifecycle();
     method public androidx.lifecycle.ViewModelStore getViewModelStore();
     method @Deprecated public Object? onRetainCustomNonConfigurationInstance();
     method public final Object? onRetainNonConfigurationInstance();
diff --git a/activity/api/1.0.0-alpha02.txt b/activity/api/1.0.0-alpha02.txt
new file mode 100644
index 0000000..4f1f238
--- /dev/null
+++ b/activity/api/1.0.0-alpha02.txt
@@ -0,0 +1,20 @@
+// Signature format: 2.0
+package androidx.activity {
+
+  public class ComponentActivity extends androidx.core.app.ComponentActivity implements androidx.lifecycle.LifecycleOwner androidx.lifecycle.ViewModelStoreOwner {
+    ctor public ComponentActivity();
+    method public void addOnBackPressedCallback(androidx.activity.OnBackPressedCallback);
+    method public void addOnBackPressedCallback(androidx.lifecycle.LifecycleOwner, androidx.activity.OnBackPressedCallback);
+    method @Deprecated public Object? getLastCustomNonConfigurationInstance();
+    method public androidx.lifecycle.ViewModelStore getViewModelStore();
+    method @Deprecated public Object? onRetainCustomNonConfigurationInstance();
+    method public final Object? onRetainNonConfigurationInstance();
+    method public void removeOnBackPressedCallback(androidx.activity.OnBackPressedCallback);
+  }
+
+  public interface OnBackPressedCallback {
+    method public boolean handleOnBackPressed();
+  }
+
+}
+
diff --git a/activity/api/current.txt b/activity/api/current.txt
index f5bed80..4f1f238 100644
--- a/activity/api/current.txt
+++ b/activity/api/current.txt
@@ -6,7 +6,6 @@
     method public void addOnBackPressedCallback(androidx.activity.OnBackPressedCallback);
     method public void addOnBackPressedCallback(androidx.lifecycle.LifecycleOwner, androidx.activity.OnBackPressedCallback);
     method @Deprecated public Object? getLastCustomNonConfigurationInstance();
-    method public androidx.lifecycle.Lifecycle getLifecycle();
     method public androidx.lifecycle.ViewModelStore getViewModelStore();
     method @Deprecated public Object? onRetainCustomNonConfigurationInstance();
     method public final Object? onRetainNonConfigurationInstance();
diff --git a/activity/ktx/api/1.0.0-alpha02.txt b/activity/ktx/api/1.0.0-alpha02.txt
new file mode 100644
index 0000000..09242be
--- /dev/null
+++ b/activity/ktx/api/1.0.0-alpha02.txt
@@ -0,0 +1,16 @@
+// Signature format: 2.0
+package androidx.activity {
+
+  public final class ActivityViewModelLazy<VM extends androidx.lifecycle.ViewModel> implements kotlin.Lazy<VM> {
+    ctor public ActivityViewModelLazy(androidx.activity.ComponentActivity activity, kotlin.reflect.KClass<VM> viewModelClass, androidx.lifecycle.ViewModelProvider.Factory? factory);
+    method public VM getValue();
+    method public boolean isInitialized();
+    property public VM value;
+  }
+
+  public final class ActivityViewModelLazyKt {
+    ctor public ActivityViewModelLazyKt();
+  }
+
+}
+
diff --git a/activity/src/androidTest/AndroidManifest.xml b/activity/src/androidTest/AndroidManifest.xml
index 40bd4dc..e833bec 100644
--- a/activity/src/androidTest/AndroidManifest.xml
+++ b/activity/src/androidTest/AndroidManifest.xml
@@ -20,6 +20,8 @@
 
     <application>
         <activity android:name="androidx.activity.LifecycleComponentActivity"/>
+        <activity android:name="androidx.activity.EagerOverrideLifecycleComponentActivity"/>
+        <activity android:name="androidx.activity.LazyOverrideLifecycleComponentActivity"/>
         <activity android:name="androidx.activity.ViewModelActivity"/>
         <activity android:name="androidx.activity.OnBackPressedComponentActivity"/>
     </application>
diff --git a/activity/src/androidTest/java/androidx/activity/ComponentActivityOverrideLifecycleTest.kt b/activity/src/androidTest/java/androidx/activity/ComponentActivityOverrideLifecycleTest.kt
new file mode 100644
index 0000000..3c75fc7
--- /dev/null
+++ b/activity/src/androidTest/java/androidx/activity/ComponentActivityOverrideLifecycleTest.kt
@@ -0,0 +1,76 @@
+/*
+ * 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.activity
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleRegistry
+import androidx.test.annotation.UiThreadTest
+import androidx.test.filters.MediumTest
+import androidx.test.rule.ActivityTestRule
+import androidx.test.runner.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.fail
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class ComponentActivityOverrideLifecycleTest {
+
+    @get:Rule
+    val activityRule = ActivityTestRule(LazyOverrideLifecycleComponentActivity::class.java)
+
+    @UiThreadTest
+    @Test
+    fun testEagerOverride() {
+        try {
+            EagerOverrideLifecycleComponentActivity()
+            fail("Constructor for ComponentActivity using a field initializer should throw")
+        } catch (e: IllegalStateException) {
+            // expected
+        }
+    }
+
+    @UiThreadTest
+    @Test
+    fun testOverrideLifecycle() {
+        val activity = activityRule.activity
+
+        assertThat(activity.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+    }
+}
+
+class EagerOverrideLifecycleComponentActivity : ComponentActivity() {
+
+    val overrideLifecycle = LifecycleRegistry(this)
+
+    override fun getLifecycle(): Lifecycle {
+        return overrideLifecycle
+    }
+}
+
+class LazyOverrideLifecycleComponentActivity : ComponentActivity() {
+    private var overrideLifecycle: LifecycleRegistry? = null
+
+    override fun getLifecycle(): Lifecycle {
+        return overrideLifecycle ?: LifecycleRegistry(this).also {
+            overrideLifecycle = it
+        }
+    }
+}
diff --git a/activity/src/main/java/androidx/activity/ComponentActivity.java b/activity/src/main/java/androidx/activity/ComponentActivity.java
index 6821514..dbea8cf 100644
--- a/activity/src/main/java/androidx/activity/ComponentActivity.java
+++ b/activity/src/main/java/androidx/activity/ComponentActivity.java
@@ -52,7 +52,7 @@
         ViewModelStore viewModelStore;
     }
 
-    private final LifecycleRegistry mLifecycleRegistry;
+    private final LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this);
 
     // Lazily recreated from NonConfigurationInstances by getViewModelStore()
     private ViewModelStore mViewModelStore;
@@ -62,9 +62,16 @@
             new CopyOnWriteArrayList<>();
 
     public ComponentActivity() {
-        mLifecycleRegistry = new LifecycleRegistry(this);
+        Lifecycle lifecycle = getLifecycle();
+        //noinspection ConstantConditions
+        if (lifecycle == null) {
+            throw new IllegalStateException("getLifecycle() returned null in ComponentActivity's "
+                    + "constructor. Please make sure you are lazily constructing your Lifecycle "
+                    + "in the first call to getLifecycle() rather than relying on field "
+                    + "initialization.");
+        }
         if (Build.VERSION.SDK_INT >= 19) {
-            mLifecycleRegistry.addObserver(new GenericLifecycleObserver() {
+            getLifecycle().addObserver(new GenericLifecycleObserver() {
                 @Override
                 public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
                     if (event == Lifecycle.Event.ON_STOP) {
@@ -77,7 +84,7 @@
                 }
             });
         }
-        mLifecycleRegistry.addObserver(new GenericLifecycleObserver() {
+        getLifecycle().addObserver(new GenericLifecycleObserver() {
             @Override
             public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
                 if (event == Lifecycle.Event.ON_DESTROY) {
@@ -100,7 +107,10 @@
     @CallSuper
     @Override
     protected void onSaveInstanceState(@NonNull Bundle outState) {
-        mLifecycleRegistry.markState(Lifecycle.State.CREATED);
+        Lifecycle lifecycle = getLifecycle();
+        if (lifecycle instanceof LifecycleRegistry) {
+            ((LifecycleRegistry) lifecycle).markState(Lifecycle.State.CREATED);
+        }
         super.onSaveInstanceState(outState);
     }
 
@@ -161,6 +171,19 @@
         return nc != null ? nc.custom : null;
     }
 
+    /**
+     * {@inheritDoc}
+     * <p>
+     * Overriding this method is no longer supported and this method will be made
+     * <code>final</code> in a future version of ComponentActivity. If you do override
+     * this method, you <code>must</code>:
+     * <ol>
+     *     <li>Return an instance of {@link LifecycleRegistry}</li>
+     *     <li>Lazily initialize your LifecycleRegistry object when this is first called.
+     *     Note that this method will be called in the super classes' constructor, before any
+     *     field initialization or object state creation is complete.</li>
+     * </ol>
+     */
     @NonNull
     @Override
     public Lifecycle getLifecycle() {
@@ -261,14 +284,15 @@
      */
     public void addOnBackPressedCallback(@NonNull LifecycleOwner owner,
             @NonNull OnBackPressedCallback onBackPressedCallback) {
-        if (owner.getLifecycle().getCurrentState() == Lifecycle.State.DESTROYED) {
+        Lifecycle lifecycle = owner.getLifecycle();
+        if (lifecycle.getCurrentState() == Lifecycle.State.DESTROYED) {
             // Already destroyed, nothing to do
             return;
         }
         // Add new callbacks to the front of the list so that
         // the most recently added callbacks get priority
         mOnBackPressedCallbacks.add(0, new LifecycleAwareOnBackPressedCallback(
-                owner.getLifecycle(), onBackPressedCallback));
+                lifecycle, onBackPressedCallback));
     }
 
     /**
diff --git a/animation/testing/build.gradle b/animation/testing/build.gradle
index ae7e3ac9..c251db0 100644
--- a/animation/testing/build.gradle
+++ b/animation/testing/build.gradle
@@ -32,7 +32,7 @@
 supportLibrary {
     name = "Android Support Animation Testing"
     publish = false
-    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenVersion = LibraryVersions.ANIMATION_TESTING
     mavenGroup = LibraryGroups.ANIMATION
     inceptionYear = "2018"
     description = "This library provides functionalities for testing animations for API 14 and above."
diff --git a/annotations/build.gradle b/annotations/build.gradle
index d55583e..7e20eac 100644
--- a/annotations/build.gradle
+++ b/annotations/build.gradle
@@ -53,7 +53,7 @@
 supportLibrary {
     name = "Android Support Library Annotations"
     publish = true
-    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenVersion = LibraryVersions.ANNOTATION
     mavenGroup = LibraryGroups.ANNOTATION
     inceptionYear = "2013"
     description = "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren't a part of the framework APIs."
diff --git a/appcompat/build.gradle b/appcompat/build.gradle
index 1c7a7c2..ff3719c 100644
--- a/appcompat/build.gradle
+++ b/appcompat/build.gradle
@@ -26,6 +26,7 @@
     androidTestImplementation project(':internal-testutils'), {
         exclude group: 'androidx.appcompat', module: 'appcompat'
     }
+    androidTestImplementation(GUAVA_LISTENABLE_FUTURE_AVOID_CONFLICT)
 }
 
 android {
diff --git a/appcompat/res/values-v21/styles_base.xml b/appcompat/res/values-v21/styles_base.xml
index 895c850..ccf7acc 100644
--- a/appcompat/res/values-v21/styles_base.xml
+++ b/appcompat/res/values-v21/styles_base.xml
@@ -153,7 +153,7 @@
     </style>
 
     <style name="Base.TextAppearance.AppCompat.Widget.PopupMenu.Header" parent="TextAppearance.AppCompat">
-        <item name="android:fontFamily">@string/abc_font_family_title_material</item>
+        <item name="android:fontFamily">sans-serif-medium</item>
         <item name="android:textSize">@dimen/abc_text_size_menu_header_material</item>
         <item name="android:textColor">?android:attr/textColorSecondary</item>
     </style>
diff --git a/appcompat/res/values/donottranslate_material.xml b/appcompat/res/values/donottranslate_material.xml
deleted file mode 100644
index 3f01f7a..0000000
--- a/appcompat/res/values/donottranslate_material.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-
-<resources>
-
-    <string name="abc_font_family_display_4_material">sans-serif-light</string>
-    <string name="abc_font_family_display_3_material">sans-serif</string>
-    <string name="abc_font_family_display_2_material">sans-serif</string>
-    <string name="abc_font_family_display_1_material">sans-serif</string>
-    <string name="abc_font_family_headline_material">sans-serif</string>
-    <string name="abc_font_family_title_material">sans-serif-medium</string>
-    <string name="abc_font_family_subhead_material">sans-serif</string>
-    <string name="abc_font_family_menu_material">sans-serif</string>
-    <string name="abc_font_family_body_2_material">sans-serif-medium</string>
-    <string name="abc_font_family_body_1_material">sans-serif</string>
-    <string name="abc_font_family_caption_material">sans-serif</string>
-    <string name="abc_font_family_button_material">sans-serif-medium</string>
-
-</resources>
\ No newline at end of file
diff --git a/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatTextViewTest.java b/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatTextViewTest.java
index eb02157..c95966f8 100644
--- a/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatTextViewTest.java
+++ b/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatTextViewTest.java
@@ -40,6 +40,7 @@
 import android.text.Layout;
 import android.text.PrecomputedText;
 import android.view.View;
+import android.view.ViewGroup;
 import android.view.textclassifier.TextClassificationManager;
 import android.view.textclassifier.TextClassifier;
 import android.widget.TextView;
@@ -49,6 +50,7 @@
 import androidx.appcompat.test.R;
 import androidx.core.content.ContextCompat;
 import androidx.core.content.res.ResourcesCompat;
+import androidx.core.os.BuildCompat;
 import androidx.core.text.PrecomputedTextCompat;
 import androidx.core.view.ViewCompat;
 import androidx.core.widget.TextViewCompat;
@@ -570,7 +572,7 @@
                 // setText may wrap the given text with SpannedString. Check the contents by casting
                 // to String.
                 assertEquals(SAMPLE_TEXT_1, tv.getText().toString());
-                if (Build.VERSION.SDK_INT >= 28) {
+                if (BuildCompat.isAtLeastQ()) {
                     assertTrue(tv.getText() instanceof PrecomputedText);
                 }
             }
@@ -590,7 +592,7 @@
                 tv.measure(UNLIMITED_MEASURE_SPEC, UNLIMITED_MEASURE_SPEC);
                 assertNotEquals(0.0f, tv.getMeasuredWidth());
                 assertEquals(SAMPLE_TEXT_1, tv.getText().toString());
-                if (Build.VERSION.SDK_INT >= 28) {
+                if (BuildCompat.isAtLeastQ()) {
                     assertTrue(tv.getText() instanceof PrecomputedText);
                 }
             }
@@ -622,7 +624,7 @@
                 // setText may wrap the given text with SpannedString. Check the contents by casting
                 // to String.
                 assertEquals(SAMPLE_TEXT_2, tv.getText().toString());
-                if (Build.VERSION.SDK_INT >= 28) {
+                if (BuildCompat.isAtLeastQ()) {
                     assertTrue(tv.getText() instanceof PrecomputedText);
                 }
             }
@@ -637,7 +639,7 @@
                 // setText may wrap the given text with SpannedString. Check the contents by casting
                 // to String.
                 assertEquals(SAMPLE_TEXT_2, tv.getText().toString());
-                if (Build.VERSION.SDK_INT >= 28) {
+                if (BuildCompat.isAtLeastQ()) {
                     assertTrue(tv.getText() instanceof PrecomputedText);
                 }
             }
@@ -645,6 +647,38 @@
     }
 
     @Test
+    public void testSetTextAsync_directionDifference() throws Throwable {
+        mActivity.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mActivity.setContentView(R.layout.appcompat_textview_rtl);
+                final ViewGroup container = mActivity.findViewById(R.id.container);
+                final AppCompatTextView tv = mActivity.findViewById(R.id.text_view_rtl);
+                tv.setTextFuture(PrecomputedTextCompat.getTextFuture(
+                        SAMPLE_TEXT_1, tv.getTextMetricsParamsCompat(), null));
+                container.measure(UNLIMITED_MEASURE_SPEC, UNLIMITED_MEASURE_SPEC);
+            }
+        });
+    }
+
+    @Test
+    public void testSetTextAsync_createAndAttach() throws Throwable {
+        mActivity.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mActivity.setContentView(R.layout.appcompat_textview_rtl);
+                final ViewGroup container = mActivity.findViewById(R.id.container);
+
+                final AppCompatTextView tv = new AppCompatTextView(mActivity);
+                tv.setTextFuture(PrecomputedTextCompat.getTextFuture(
+                        SAMPLE_TEXT_1, tv.getTextMetricsParamsCompat(), null));
+                container.addView(tv);
+                container.measure(UNLIMITED_MEASURE_SPEC, UNLIMITED_MEASURE_SPEC);
+            }
+        });
+    }
+
+    @Test
     public void testSetTextAsync_executionOrder_withNull() throws Throwable {
         final ManualExecutor executor = new ManualExecutor();
         mActivity.runOnUiThread(new Runnable() {
diff --git a/appcompat/src/androidTest/res/layout/appcompat_textview_rtl.xml b/appcompat/src/androidTest/res/layout/appcompat_textview_rtl.xml
new file mode 100644
index 0000000..5b84a24
--- /dev/null
+++ b/appcompat/src/androidTest/res/layout/appcompat_textview_rtl.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 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.
+-->
+
+<LinearLayout
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        android:id="@+id/container"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical"
+        android:layoutDirection="rtl">
+
+        <androidx.appcompat.widget.AppCompatTextView
+                android:id="@+id/text_view_rtl"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:text="Hello, World"/>
+</LinearLayout>
diff --git a/asynclayoutinflater/build.gradle b/asynclayoutinflater/build.gradle
index e69a048..1671c98 100644
--- a/asynclayoutinflater/build.gradle
+++ b/asynclayoutinflater/build.gradle
@@ -13,7 +13,7 @@
 supportLibrary {
     name = "Android Support Library Async Layout Inflater"
     publish = true
-    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenVersion = LibraryVersions.ASYNCLAYOUTINFLATER
     mavenGroup = LibraryGroups.ASYNCLAYOUTINFLATER
     inceptionYear = "2018"
     description = "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren't a part of the framework APIs. Compatible on devices running API 14 or later."
diff --git a/benchmark/src/main/java/androidx/benchmark/BenchmarkRule.java b/benchmark/src/main/java/androidx/benchmark/BenchmarkRule.java
index 5124566..e6d097a 100644
--- a/benchmark/src/main/java/androidx/benchmark/BenchmarkRule.java
+++ b/benchmark/src/main/java/androidx/benchmark/BenchmarkRule.java
@@ -22,7 +22,7 @@
 import android.util.Log;
 
 import androidx.annotation.NonNull;
-import androidx.test.InstrumentationRegistry;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import org.junit.rules.TestRule;
 import org.junit.runner.Description;
diff --git a/benchmark/src/main/java/androidx/benchmark/BenchmarkState.java b/benchmark/src/main/java/androidx/benchmark/BenchmarkState.java
index f14482f..ac85f70 100644
--- a/benchmark/src/main/java/androidx/benchmark/BenchmarkState.java
+++ b/benchmark/src/main/java/androidx/benchmark/BenchmarkState.java
@@ -25,7 +25,7 @@
 import android.util.Log;
 
 import androidx.annotation.NonNull;
-import androidx.test.InstrumentationRegistry;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import java.io.File;
 import java.text.NumberFormat;
@@ -69,7 +69,8 @@
     private static final int REPEAT_COUNT = 5;
 
     static {
-        ApplicationInfo appInfo = InstrumentationRegistry.getTargetContext().getApplicationInfo();
+        ApplicationInfo appInfo = InstrumentationRegistry.getInstrumentation().getTargetContext()
+                .getApplicationInfo();
         IS_DEBUGGABLE = (appInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
 
         StringBuilder sb = new StringBuilder();
@@ -141,7 +142,9 @@
     private void beginBenchmark() {
         if (ENABLE_PROFILING && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
             // TODO: support data dir for old platforms
-            File f = new File(InstrumentationRegistry.getContext().getDataDir(), "benchprof");
+            File f = new File(
+                    InstrumentationRegistry.getInstrumentation().getContext().getDataDir(),
+                    "benchprof");
             Log.d(TAG, "Tracing to: " + f.getAbsolutePath());
             Debug.startMethodTracingSampling(f.getAbsolutePath(), 16 * 1024 * 1024, 100);
         }
diff --git a/browser/build.gradle b/browser/build.gradle
index 1336b63..0e00196 100644
--- a/browser/build.gradle
+++ b/browser/build.gradle
@@ -28,7 +28,7 @@
 supportLibrary {
     name = "Android Support Custom Tabs"
     publish = true
-    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenVersion = LibraryVersions.BROWSER
     mavenGroup = LibraryGroups.BROWSER
     inceptionYear = "2015"
     description = "Android Support Custom Tabs"
diff --git a/buildSrc/init.gradle b/buildSrc/init.gradle
index 7674773..562bd1ec 100644
--- a/buildSrc/init.gradle
+++ b/buildSrc/init.gradle
@@ -29,7 +29,6 @@
 def init = new Properties()
 ext.init = init
 rootProject.ext.versionChecker = new GMavenVersionChecker(rootProject.logger)
-
 apply from: "${supportRoot}/buildSrc/dependencies.gradle"
 
 def setupRepoOutAndBuildNumber() {
@@ -56,7 +55,15 @@
 def configureSubProjects() {
     subprojects {
         repos.addMavenRepositories(repositories)
-
+    }
+    if (!rootProject.hasProperty("android.injected.invoked.from.ide")) {
+        // configure build server tasks only if we are not running in the IDE to speed up
+        // configuration times
+        setupSubprojectsForBuildServer()
+    }
+}
+def setupSubprojectsForBuildServer() {
+    subprojects {
         project.plugins.whenPluginAdded { plugin ->
             def isAndroidLibrary = "com.android.build.gradle.LibraryPlugin"
                     .equals(plugin.class.name)
diff --git a/buildSrc/src/main/kotlin/androidx/build/AndroidXPlugin.kt b/buildSrc/src/main/kotlin/androidx/build/AndroidXPlugin.kt
index b1b6f61..186aa5f 100644
--- a/buildSrc/src/main/kotlin/androidx/build/AndroidXPlugin.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/AndroidXPlugin.kt
@@ -16,6 +16,7 @@
 
 package androidx.build
 
+import androidx.build.dokka.Dokka
 import androidx.build.SupportConfig.BUILD_TOOLS_VERSION
 import androidx.build.SupportConfig.CURRENT_SDK_VERSION
 import androidx.build.SupportConfig.DEFAULT_MIN_SDK_VERSION
@@ -112,6 +113,7 @@
         tasks.all { task ->
             if (task.name.startsWith(Release.DIFF_TASK_PREFIX) ||
                     "distDocs" == task.name ||
+                    Dokka.ARCHIVE_TASK_NAME == task.name ||
                     "dejetifyArchive" == task.name ||
                     CheckExternalDependencyLicensesTask.TASK_NAME == task.name) {
                 buildOnServerTask.dependsOn(task)
@@ -154,6 +156,7 @@
         project.createClockLockTasks()
 
         AffectedModuleDetector.configure(gradle, this)
+
     }
 
     private fun Project.configureAndroidCommonOptions(extension: BaseExtension) {
diff --git a/buildSrc/src/main/kotlin/androidx/build/DiffAndDocs.kt b/buildSrc/src/main/kotlin/androidx/build/DiffAndDocs.kt
index 560c87c..266eed7 100644
--- a/buildSrc/src/main/kotlin/androidx/build/DiffAndDocs.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/DiffAndDocs.kt
@@ -226,7 +226,7 @@
                 ?.forEach { docsProject?.evaluationDependsOn(it.path) }
     }
 
-    private fun registerPrebuilts(extension: SupportLibraryExtension) =
+    fun registerPrebuilts(extension: SupportLibraryExtension) =
             docsProject?.afterEvaluate { docs ->
         val depHandler = docs.dependencies
         val root = docs.rootProject
diff --git a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
index 31e8087..6ed2006 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
@@ -20,29 +20,48 @@
  * The list of versions codes of all the libraries in this project.
  */
 object LibraryVersions {
-    val ACTIVITY = Version("1.0.0-alpha01")
+    val ACTIVITY = Version("1.0.0-alpha02")
     val ANIMATION = Version("1.0.0-alpha01")
+    val ANIMATION_TESTING = Version("1.0.0")
+    val ANNOTATION = Version("1.0.0")
     val APPCOMPAT = Version("1.1.0-alpha01")
     val ARCH_CORE = Version("2.0.0")
     val ARCH_CORE_TESTING = ARCH_CORE
     val ARCH_RUNTIME = Version("2.0.1-alpha01")
+    val ASYNCLAYOUTINFLATER = Version("1.0.0")
     val BENCHMARK = Version("1.0.0-alpha01")
     val BIOMETRIC = Version("1.0.0-alpha03")
+    val BROWSER = Version("1.0.0")
     val CAR = Version("1.0.0-alpha6")
     val CAR_CLUSTER = Version("1.0.0-alpha5")
     val CAR_MODERATOR = Version("1.0.0-alpha1")
+    val CARDVIEW = Version("1.0.0")
     val COLLECTION = Version("1.1.0-alpha01")
+    val CONTENTPAGER = Version("1.0.0")
     val COORDINATORLAYOUT = Version("1.1.0-alpha01")
     val CORE = Version("1.1.0-alpha01")
+    val CORE_KTX = Version("1.0.1")
+    val CURSORADAPTER = Version("1.0.0")
+    val CUSTOMVIEW = Version("1.0.0")
+    val DOCUMENTFILE = Version("1.0.0")
+    val DRAWERLAYOUT = Version("1.0.0")
+    val DYNAMICANIMATION = Version("1.0.0")
     val DYNAMICANIMATION_KTX = Version("1.0.0-alpha01")
+    val EMOJI = Version("1.0.0")
+    val EXIFINTERFACE = Version("1.0.0")
     val FRAGMENT = Version("1.1.0-alpha01")
     val FUTURES = Version("1.0.0-alpha02")
     val GRIDLAYOUT = Version("1.1.0-alpha01")
+    val HEIFWRITER = Version("1.0.0")
+    val INTERPOLATOR = Version("1.0.0")
     val JETIFIER = Version("1.0.0-beta02")
     val LEANBACK = Version("1.1.0-alpha01")
     val LEANBACK_PREFERENCE = Version("1.1.0-alpha01")
+    val LEGACY = Version("1.0.0")
+    val LOCALBROADCASTMANAGER = Version("1.0.0")
     val LIFECYCLE = Version("2.1.0-alpha01")
     val LIFECYCLES_SAVEDSTATE = Version("1.0.0-alpha01")
+    val LOADER = Version("1.1.0-alpha01")
     val MEDIA = Version("1.1.0-alpha01")
     val MEDIA2 = Version("1.0.0-alpha03")
     val MEDIA2_EXOPLAYER = Version("1.0.0-alpha01")
@@ -51,19 +70,30 @@
     val NAVIGATION = Version("1.0.0-alpha07")
     val PAGING = Version("2.2.0-alpha01")
     val PALETTE = Version("1.1.0-alpha01")
+    val PALETTE_KTX = Version("1.0.0")
+    val PRINT = Version("1.0.0")
+    val PERCENTLAYOUT = Version("1.0.0")
     val PERSISTENCE = Version("2.0.0")
     val PREFERENCE = Version("1.1.0-alpha01")
+    val PREFERENCE_KTX = Version("1.0.0")
+    val RECOMMENDATION = Version("1.0.0")
     val RECYCLERVIEW = Version("1.1.0-alpha01")
     val REMOTECALLBACK = Version("1.0.0-alpha01")
     val ROOM = Version("2.1.0-alpha02")
     val SLICE = Version("1.1.0-alpha01")
+    val SLICE_BENCHMARK = Version("1.0.0")
     val SLICE_BUILDERS_KTX = Version("1.0.0-alpha6")
-    val SUPPORT_LIBRARY = Version("1.0.0")
+    val SLIDINGPANELAYOUT = Version("1.0.0")
     val SWIPE_REFRESH_LAYOUT = Version("1.1.0-alpha01")
     val TEXTCLASSIFIER = Version("1.0.0-alpha01")
     val TRANSITION = Version("1.1.0-alpha01")
+    val TVPROVIDER = Version("1.0.0")
+    val VECTORDRAWABLE = Version("1.0.1")
     val VECTORDRAWABLE_ANIMATED = Version("1.1.0-alpha01")
     val VERSIONED_PARCELABLE = Version("1.1.0-alpha01")
+    val VIEWPAGER = Version("1.1.0-alpha01")
+    val VIEWPAGER2 = Version("1.0.0")
+    val WEAR = Version("1.0.0")
     val WEBKIT = Version("1.1.0-alpha01")
     val WORKMANAGER = Version("1.0.0-alpha11")
 }
diff --git a/buildSrc/src/main/kotlin/androidx/build/PublishDocsRules.kt b/buildSrc/src/main/kotlin/androidx/build/PublishDocsRules.kt
index 5e69e3a..82755d3 100644
--- a/buildSrc/src/main/kotlin/androidx/build/PublishDocsRules.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/PublishDocsRules.kt
@@ -25,8 +25,9 @@
 import androidx.build.Strategy.TipOfTree
 
 val RELEASE_RULE = docsRules("public", false) {
+    prebuilts(LibraryGroups.ACTIVITY, "1.0.0-alpha01")
     prebuilts(LibraryGroups.ANNOTATION, "1.0.0")
-    prebuilts(LibraryGroups.APPCOMPAT, "1.0.0")
+    prebuilts(LibraryGroups.APPCOMPAT, "1.0.2")
     prebuilts(LibraryGroups.ASYNCLAYOUTINFLATER, "1.0.0")
     prebuilts(LibraryGroups.BIOMETRIC, "biometric", "1.0.0-alpha01")
     prebuilts(LibraryGroups.BROWSER, "1.0.0")
@@ -35,19 +36,22 @@
     prebuilts(LibraryGroups.CAR, "car", "1.0.0-alpha5")
             .addStubs("car/stubs/android.car.jar")
     prebuilts(LibraryGroups.CARDVIEW, "1.0.0")
+    ignore(LibraryGroups.COLLECTION, "collection-ktx")
     prebuilts(LibraryGroups.COLLECTION, "1.0.0")
     prebuilts(LibraryGroups.CONCURRENT, "1.0.0-alpha02")
     prebuilts(LibraryGroups.CONTENTPAGER, "1.0.0")
     prebuilts(LibraryGroups.COORDINATORLAYOUT, "1.0.0")
-    prebuilts(LibraryGroups.CORE, "1.0.0")
+    prebuilts(LibraryGroups.CORE, "1.0.1")
+    prebuilts(LibraryGroups.CORE, "core-ktx", "1.0.1")
     prebuilts(LibraryGroups.CURSORADAPTER, "1.0.0")
     prebuilts(LibraryGroups.CUSTOMVIEW, "1.0.0")
     prebuilts(LibraryGroups.DOCUMENTFILE, "1.0.0")
     prebuilts(LibraryGroups.DRAWERLAYOUT, "1.0.0")
+    ignore(LibraryGroups.DYNAMICANIMATION, "dynamicanimation-ktx")
     prebuilts(LibraryGroups.DYNAMICANIMATION, "1.0.0")
     prebuilts(LibraryGroups.EMOJI, "1.0.0")
     prebuilts(LibraryGroups.EXIFINTERFACE, "1.0.0")
-    prebuilts(LibraryGroups.FRAGMENT, "1.0.0")
+    prebuilts(LibraryGroups.FRAGMENT, "1.1.0-alpha01")
     prebuilts(LibraryGroups.GRIDLAYOUT, "1.0.0")
     prebuilts(LibraryGroups.HEIFWRITER, "1.0.0")
     prebuilts(LibraryGroups.INTERPOLATOR, "1.0.0")
@@ -62,7 +66,8 @@
     prebuilts(LibraryGroups.MEDIAROUTER, "1.0.0")
     prebuilts(LibraryGroups.PALETTE, "1.0.0")
     prebuilts(LibraryGroups.PERCENTLAYOUT, "1.0.0")
-    prebuilts(LibraryGroups.PREFERENCE, "1.0.0")
+    prebuilts(LibraryGroups.PREFERENCE, "preference-ktx", "1.0.0")
+    prebuilts(LibraryGroups.PREFERENCE, "1.1.0-alpha01")
     prebuilts(LibraryGroups.PRINT, "1.0.0")
     prebuilts(LibraryGroups.RECOMMENDATION, "1.0.0")
     prebuilts(LibraryGroups.RECYCLERVIEW, "1.0.0")
@@ -75,9 +80,10 @@
     prebuilts(LibraryGroups.SLICE, "slice-view", "1.0.0")
     prebuilts(LibraryGroups.SLIDINGPANELAYOUT, "1.0.0")
     prebuilts(LibraryGroups.SWIPEREFRESHLAYOUT, "1.0.0")
-    prebuilts(LibraryGroups.TRANSITION, "1.0.0")
+    prebuilts(LibraryGroups.TRANSITION, "1.0.1")
     prebuilts(LibraryGroups.TVPROVIDER, "1.0.0")
     prebuilts(LibraryGroups.VECTORDRAWABLE, "1.0.0")
+    prebuilts(LibraryGroups.VECTORDRAWABLE, "vectordrawable-animated", "1.0.1")
     prebuilts(LibraryGroups.VIEWPAGER, "1.0.0")
     prebuilts(LibraryGroups.WEAR, "1.0.0")
             .addStubs("wear/wear_stubs/com.google.android.wearable-stubs.jar")
@@ -88,12 +94,13 @@
     ignore(LibraryGroups.LIFECYCLE, "lifecycle-savedstate-fragment")
     ignore(LibraryGroups.LIFECYCLE, "lifecycle-viewmodel-savedstate")
     ignore(LibraryGroups.LIFECYCLE, "lifecycle-viewmodel-fragment")
+    ignore(LibraryGroups.LIFECYCLE, "lifecycle-livedata-ktx")
+    ignore(LibraryGroups.LIFECYCLE, "lifecycle-livedata-core-ktx")
     prebuilts(LibraryGroups.LIFECYCLE, "2.0.0")
     prebuilts(LibraryGroups.ARCH_CORE, "2.0.0")
-    prebuilts(LibraryGroups.PAGING, "2.1.0-alpha01")
+    prebuilts(LibraryGroups.PAGING, "2.1.0-beta01")
     prebuilts(LibraryGroups.NAVIGATION, "1.0.0-alpha07")
-    ignore(LibraryGroups.WORKMANAGER, "work-runtime-sync")
-    prebuilts(LibraryGroups.WORKMANAGER, "1.0.0-alpha10")
+    prebuilts(LibraryGroups.WORKMANAGER, "1.0.0-alpha11")
     default(Ignore)
 }
 
diff --git a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
index 802bc60..539c0f9 100644
--- a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
@@ -34,6 +34,8 @@
 const val GUAVA = "com.google.guava:guava:23.5-jre"
 const val GUAVA_ANDROID = "com.google.guava:guava:23.6-android"
 const val GUAVA_LISTENABLE_FUTURE = "com.google.guava:listenablefuture:1.0"
+const val GUAVA_LISTENABLE_FUTURE_AVOID_CONFLICT =
+        "com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava"
 const val INTELLIJ_ANNOTATIONS = "com.intellij:annotations:12.0"
 const val JAVAPOET = "com.squareup:javapoet:1.8.0"
 const val JSR250 = "javax.annotation:javax.annotation-api:1.2"
@@ -47,9 +49,9 @@
 const val PLAY_SERVICES = "com.google.android.gms:play-services-base:11.6.0@aar"
 const val REACTIVE_STREAMS = "org.reactivestreams:reactive-streams:1.0.0"
 const val RX_JAVA = "io.reactivex.rxjava2:rxjava:2.0.6"
-const val TEST_CORE = "androidx.test:core:1.0.0-beta01"
-const val TEST_RUNNER = "androidx.test:runner:1.1.0-alpha3"
-const val TEST_RULES = "androidx.test:rules:1.1.0-alpha3"
+const val TEST_CORE = "androidx.test:core:1.0.0"
+const val TEST_RUNNER = "androidx.test:runner:1.1.0"
+const val TEST_RULES = "androidx.test:rules:1.1.0"
 const val TRUTH = "com.google.truth:truth:0.34"
 /**
  * this Xerial version is newer than we want but we need it to fix
diff --git a/buildSrc/src/main/kotlin/androidx/build/dokka/Dokka.kt b/buildSrc/src/main/kotlin/androidx/build/dokka/Dokka.kt
index 065b862..f011b2a 100644
--- a/buildSrc/src/main/kotlin/androidx/build/dokka/Dokka.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/dokka/Dokka.kt
@@ -18,19 +18,25 @@
 // TODO: after DiffAndDocs and Doclava are fully obsoleted and removed, rename this from Dokka to just Docs
 package androidx.build.dokka
 
+import androidx.build.getBuildId
+import androidx.build.getDistributionDirectory
 import androidx.build.java.JavaCompileInputs
 import androidx.build.SupportLibraryExtension
+import androidx.build.Release
 import com.android.build.gradle.LibraryExtension
 import org.gradle.api.Project
 import org.gradle.api.plugins.JavaPluginConvention
+import org.gradle.api.tasks.bundling.Zip
 import org.gradle.kotlin.dsl.apply
 import org.gradle.kotlin.dsl.getPlugin
 import org.jetbrains.dokka.gradle.DokkaPlugin
 import org.jetbrains.dokka.gradle.DokkaTask
 import org.jetbrains.dokka.gradle.PackageOptions
+import androidx.build.DiffAndDocs
 
 object Dokka {
     private val RUNNER_TASK_NAME = "dokka"
+    public val ARCHIVE_TASK_NAME: String = "distDokkaDocs"
 
     private val hiddenPackages = listOf(
         "androidx.core.internal",
@@ -65,6 +71,14 @@
                 opts.suppress = true
                 docsTask.perPackageOptions.add(opts)
             }
+            project.tasks.create(ARCHIVE_TASK_NAME, Zip::class.java) { task ->
+                task.dependsOn(docsTask)
+                task.description = "Generates documentation artifact for pushing to developer.android.com"
+                task.from(docsTask.outputDirectory)
+                task.baseName = "android-support-dokka-docs"
+                task.version = getBuildId()
+                task.destinationDir = project.getDistributionDirectory()
+            }
         }
         return runnerProject.tasks.getByName(Dokka.RUNNER_TASK_NAME) as DokkaTask
     }
@@ -79,13 +93,14 @@
             return
         }
         library.libraryVariants.all { variant ->
-            if (variant.name == "release") {
+            if (variant.name == Release.DEFAULT_PUBLISH_CONFIG) {
                 project.afterEvaluate({
                     val inputs = JavaCompileInputs.fromLibraryVariant(library, variant)
                     registerInputs(inputs, project)
                 })
             }
         }
+        DiffAndDocs.registerPrebuilts(extension)
     }
 
     fun registerJavaProject(
@@ -102,6 +117,7 @@
             val inputs = JavaCompileInputs.fromSourceSet(mainSourceSet, project)
             registerInputs(inputs, project)
         })
+        DiffAndDocs.registerPrebuilts(extension)
     }
 
     fun registerInputs(inputs: JavaCompileInputs, project: Project) {
diff --git a/buildSrc/src/main/kotlin/androidx/build/metalava/Metalava.kt b/buildSrc/src/main/kotlin/androidx/build/metalava/Metalava.kt
index 369558f..8618477 100644
--- a/buildSrc/src/main/kotlin/androidx/build/metalava/Metalava.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/metalava/Metalava.kt
@@ -24,6 +24,7 @@
 import androidx.build.checkapi.hasApiFolder
 import androidx.build.checkapi.hasApiTasks
 import androidx.build.java.JavaCompileInputs
+import androidx.build.Release
 import com.android.build.gradle.LibraryExtension
 import org.gradle.api.Project
 import org.gradle.api.artifacts.Configuration
@@ -50,7 +51,7 @@
         val metalavaConfiguration = project.createMetalavaConfiguration()
 
         library.libraryVariants.all { variant ->
-            if (variant.name == "minDepVersionsRelease") {
+            if (variant.name == Release.DEFAULT_PUBLISH_CONFIG) {
                 if (!project.hasApiFolder()) {
                     project.logger.info(
                         "Project ${project.name} doesn't have an api folder, ignoring API tasks.")
diff --git a/car/cluster/api/1.0.0-alpha5.txt b/car/cluster/api/1.0.0-alpha5.txt
index beb43e4..ed909d0 100644
--- a/car/cluster/api/1.0.0-alpha5.txt
+++ b/car/cluster/api/1.0.0-alpha5.txt
@@ -35,6 +35,21 @@
     enum_constant public static final androidx.car.cluster.navigation.Distance.Unit YARDS;
   }
 
+  public class ImageReference implements androidx.versionedparcelable.VersionedParcelable {
+    method public android.net.Uri? getContentUri(@IntRange(from=1, to=java.lang.Integer.MAX_VALUE) int, @IntRange(from=1, to=java.lang.Integer.MAX_VALUE) int);
+    method public int getOriginalHeight();
+    method public int getOriginalWidth();
+    method public boolean isTintable();
+  }
+
+  public static final class ImageReference.Builder {
+    ctor public ImageReference.Builder();
+    method public androidx.car.cluster.navigation.ImageReference build();
+    method public androidx.car.cluster.navigation.ImageReference.Builder setContentUri(String);
+    method public androidx.car.cluster.navigation.ImageReference.Builder setIsTintable(boolean);
+    method public androidx.car.cluster.navigation.ImageReference.Builder setOriginalSize(@IntRange(from=1, to=java.lang.Integer.MAX_VALUE) int, @IntRange(from=1, to=java.lang.Integer.MAX_VALUE) int);
+  }
+
   public final class Lane implements androidx.versionedparcelable.VersionedParcelable {
     method public java.util.List<androidx.car.cluster.navigation.LaneDirection> getDirections();
   }
@@ -77,6 +92,7 @@
   }
 
   public final class Maneuver implements androidx.versionedparcelable.VersionedParcelable {
+    method public androidx.car.cluster.navigation.ImageReference? getIcon();
     method public int getRoundaboutExitNumber();
     method public androidx.car.cluster.navigation.Maneuver.Type getType();
   }
@@ -84,6 +100,7 @@
   public static final class Maneuver.Builder {
     ctor public Maneuver.Builder();
     method public androidx.car.cluster.navigation.Maneuver build();
+    method public androidx.car.cluster.navigation.Maneuver.Builder setIcon(androidx.car.cluster.navigation.ImageReference?);
     method public androidx.car.cluster.navigation.Maneuver.Builder setRoundaboutExitNumber(int);
     method public androidx.car.cluster.navigation.Maneuver.Builder setType(androidx.car.cluster.navigation.Maneuver.Type, androidx.car.cluster.navigation.Maneuver.Type...);
   }
@@ -168,14 +185,34 @@
     enum_constant public static final androidx.car.cluster.navigation.NavigationState.ServiceStatus REROUTING;
   }
 
+  public class RichText implements androidx.versionedparcelable.VersionedParcelable {
+  }
+
+  public static final class RichText.Builder {
+    ctor public RichText.Builder();
+    method public androidx.car.cluster.navigation.RichText.Builder addElement(androidx.car.cluster.navigation.RichTextElement);
+    method public androidx.car.cluster.navigation.RichText build();
+  }
+
+  public class RichTextElement implements androidx.versionedparcelable.VersionedParcelable {
+  }
+
+  public static class RichTextElement.Builder {
+    ctor public RichTextElement.Builder();
+    method public androidx.car.cluster.navigation.RichTextElement! build(String);
+    method public androidx.car.cluster.navigation.RichTextElement.Builder! setImage(androidx.car.cluster.navigation.ImageReference?);
+  }
+
   public class Segment implements androidx.versionedparcelable.VersionedParcelable {
     ctor public Segment(String);
     method public String getName();
   }
 
   public final class Step implements androidx.versionedparcelable.VersionedParcelable {
+    method public androidx.car.cluster.navigation.RichText? getCue();
     method public androidx.car.cluster.navigation.Distance? getDistance();
     method public java.util.List<androidx.car.cluster.navigation.Lane> getLanes();
+    method public androidx.car.cluster.navigation.ImageReference? getLanesImage();
     method public androidx.car.cluster.navigation.Maneuver? getManeuver();
   }
 
@@ -183,7 +220,9 @@
     ctor public Step.Builder();
     method public androidx.car.cluster.navigation.Step.Builder addLane(androidx.car.cluster.navigation.Lane);
     method public androidx.car.cluster.navigation.Step build();
+    method public androidx.car.cluster.navigation.Step.Builder setCue(androidx.car.cluster.navigation.RichText?);
     method public androidx.car.cluster.navigation.Step.Builder setDistance(androidx.car.cluster.navigation.Distance?);
+    method public androidx.car.cluster.navigation.Step.Builder setLanesImage(androidx.car.cluster.navigation.ImageReference?);
     method public androidx.car.cluster.navigation.Step.Builder setManeuver(androidx.car.cluster.navigation.Maneuver?);
   }
 
diff --git a/car/cluster/api/current.txt b/car/cluster/api/current.txt
index beb43e4..ed909d0 100644
--- a/car/cluster/api/current.txt
+++ b/car/cluster/api/current.txt
@@ -35,6 +35,21 @@
     enum_constant public static final androidx.car.cluster.navigation.Distance.Unit YARDS;
   }
 
+  public class ImageReference implements androidx.versionedparcelable.VersionedParcelable {
+    method public android.net.Uri? getContentUri(@IntRange(from=1, to=java.lang.Integer.MAX_VALUE) int, @IntRange(from=1, to=java.lang.Integer.MAX_VALUE) int);
+    method public int getOriginalHeight();
+    method public int getOriginalWidth();
+    method public boolean isTintable();
+  }
+
+  public static final class ImageReference.Builder {
+    ctor public ImageReference.Builder();
+    method public androidx.car.cluster.navigation.ImageReference build();
+    method public androidx.car.cluster.navigation.ImageReference.Builder setContentUri(String);
+    method public androidx.car.cluster.navigation.ImageReference.Builder setIsTintable(boolean);
+    method public androidx.car.cluster.navigation.ImageReference.Builder setOriginalSize(@IntRange(from=1, to=java.lang.Integer.MAX_VALUE) int, @IntRange(from=1, to=java.lang.Integer.MAX_VALUE) int);
+  }
+
   public final class Lane implements androidx.versionedparcelable.VersionedParcelable {
     method public java.util.List<androidx.car.cluster.navigation.LaneDirection> getDirections();
   }
@@ -77,6 +92,7 @@
   }
 
   public final class Maneuver implements androidx.versionedparcelable.VersionedParcelable {
+    method public androidx.car.cluster.navigation.ImageReference? getIcon();
     method public int getRoundaboutExitNumber();
     method public androidx.car.cluster.navigation.Maneuver.Type getType();
   }
@@ -84,6 +100,7 @@
   public static final class Maneuver.Builder {
     ctor public Maneuver.Builder();
     method public androidx.car.cluster.navigation.Maneuver build();
+    method public androidx.car.cluster.navigation.Maneuver.Builder setIcon(androidx.car.cluster.navigation.ImageReference?);
     method public androidx.car.cluster.navigation.Maneuver.Builder setRoundaboutExitNumber(int);
     method public androidx.car.cluster.navigation.Maneuver.Builder setType(androidx.car.cluster.navigation.Maneuver.Type, androidx.car.cluster.navigation.Maneuver.Type...);
   }
@@ -168,14 +185,34 @@
     enum_constant public static final androidx.car.cluster.navigation.NavigationState.ServiceStatus REROUTING;
   }
 
+  public class RichText implements androidx.versionedparcelable.VersionedParcelable {
+  }
+
+  public static final class RichText.Builder {
+    ctor public RichText.Builder();
+    method public androidx.car.cluster.navigation.RichText.Builder addElement(androidx.car.cluster.navigation.RichTextElement);
+    method public androidx.car.cluster.navigation.RichText build();
+  }
+
+  public class RichTextElement implements androidx.versionedparcelable.VersionedParcelable {
+  }
+
+  public static class RichTextElement.Builder {
+    ctor public RichTextElement.Builder();
+    method public androidx.car.cluster.navigation.RichTextElement! build(String);
+    method public androidx.car.cluster.navigation.RichTextElement.Builder! setImage(androidx.car.cluster.navigation.ImageReference?);
+  }
+
   public class Segment implements androidx.versionedparcelable.VersionedParcelable {
     ctor public Segment(String);
     method public String getName();
   }
 
   public final class Step implements androidx.versionedparcelable.VersionedParcelable {
+    method public androidx.car.cluster.navigation.RichText? getCue();
     method public androidx.car.cluster.navigation.Distance? getDistance();
     method public java.util.List<androidx.car.cluster.navigation.Lane> getLanes();
+    method public androidx.car.cluster.navigation.ImageReference? getLanesImage();
     method public androidx.car.cluster.navigation.Maneuver? getManeuver();
   }
 
@@ -183,7 +220,9 @@
     ctor public Step.Builder();
     method public androidx.car.cluster.navigation.Step.Builder addLane(androidx.car.cluster.navigation.Lane);
     method public androidx.car.cluster.navigation.Step build();
+    method public androidx.car.cluster.navigation.Step.Builder setCue(androidx.car.cluster.navigation.RichText?);
     method public androidx.car.cluster.navigation.Step.Builder setDistance(androidx.car.cluster.navigation.Distance?);
+    method public androidx.car.cluster.navigation.Step.Builder setLanesImage(androidx.car.cluster.navigation.ImageReference?);
     method public androidx.car.cluster.navigation.Step.Builder setManeuver(androidx.car.cluster.navigation.Maneuver?);
   }
 
diff --git a/car/cluster/src/androidTest/java/androidx/car/cluster/navigation/ImageReferenceTest.java b/car/cluster/src/androidTest/java/androidx/car/cluster/navigation/ImageReferenceTest.java
new file mode 100644
index 0000000..b00029a
--- /dev/null
+++ b/car/cluster/src/androidTest/java/androidx/car/cluster/navigation/ImageReferenceTest.java
@@ -0,0 +1,195 @@
+/*
+ * 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.car.cluster.navigation;
+
+import static androidx.car.cluster.navigation.utils.Assertions.assertThrows;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import android.net.Uri;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for {@link ImageReference} serialization
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ImageReferenceTest {
+    private static final String TEST_CONTENT_URI = "content://foo";
+    private static final int TEST_WIDTH = 123;
+    private static final int TEST_HEIGHT = 234;
+
+    /**
+     * Test a few equality conditions
+     */
+    @Test
+    public void equality() {
+        ImageReference expected = createSampleImage();
+
+        assertEquals(expected, createSampleImage());
+        assertNotEquals(expected, new ImageReference.Builder()
+                .setContentUri("content://bar")
+                .setIsTintable(true)
+                .setOriginalSize(TEST_WIDTH, TEST_HEIGHT)
+                .build());
+        assertNotEquals(expected, new ImageReference.Builder()
+                .setContentUri(TEST_CONTENT_URI)
+                .setIsTintable(false)
+                .setOriginalSize(TEST_WIDTH, TEST_HEIGHT)
+                .build());
+        assertNotEquals(expected, new ImageReference.Builder()
+                .setContentUri(TEST_CONTENT_URI)
+                .setIsTintable(false)
+                .setOriginalSize(1, TEST_HEIGHT)
+                .build());
+        assertNotEquals(expected, new ImageReference.Builder()
+                .setContentUri(TEST_CONTENT_URI)
+                .setIsTintable(false)
+                .setOriginalSize(TEST_WIDTH, 1)
+                .build());
+
+        assertEquals(expected.hashCode(), new ImageReference.Builder()
+                .setContentUri(TEST_CONTENT_URI)
+                .setIsTintable(true)
+                .setOriginalSize(TEST_WIDTH, TEST_HEIGHT)
+                .build()
+                .hashCode());
+    }
+
+    /**
+     * Tests the output of the {@link ImageReference.Builder}
+     */
+    @Test
+    public void builder_outputShouldMatchExpected() {
+        ImageReference image = createSampleImage();
+
+        assertEquals(TEST_CONTENT_URI, image.getRawContentUri());
+        assertEquals(true, image.isTintable());
+        assertEquals(TEST_WIDTH, image.getOriginalWidth());
+        assertEquals(TEST_HEIGHT, image.getOriginalHeight());
+    }
+
+    /**
+     * Returns a sample {@link ImageReference} instance for testing.
+     */
+    public static ImageReference createSampleImage() {
+        return new ImageReference.Builder()
+                    .setContentUri(TEST_CONTENT_URI)
+                    .setIsTintable(true)
+                    .setOriginalSize(TEST_WIDTH, TEST_HEIGHT)
+                    .build();
+    }
+
+    /**
+     * Tests {@link ImageReference.Builder} can be used to produce more than one instance.
+     */
+    @Test
+    public void builder_shouldBeReusable() {
+        final String alternativeContentUri = "content://bar";
+
+        ImageReference.Builder builder = new ImageReference.Builder()
+                .setContentUri(TEST_CONTENT_URI)
+                .setIsTintable(true)
+                .setOriginalSize(TEST_WIDTH, TEST_HEIGHT);
+        ImageReference image1 = builder.build();
+        ImageReference image2 = builder.build();
+        assertEquals(image1, image2);
+
+        builder.setContentUri(alternativeContentUri);
+        ImageReference image3 = builder.build();
+        assertEquals(image3, new ImageReference.Builder()
+                .setContentUri(alternativeContentUri)
+                .setIsTintable(true)
+                .setOriginalSize(TEST_WIDTH, TEST_HEIGHT)
+                .build());
+    }
+
+    /**
+     * {@link ImageReference.Builder#setContentUri(String)} must be called.
+     */
+    @Test(expected = NullPointerException.class)
+    public void builder_contentUriIsMandatory() {
+        new ImageReference.Builder().setOriginalSize(TEST_WIDTH, TEST_HEIGHT).build();
+    }
+
+    /**
+     * Tests that passing strings not starting with 'content://' to
+     * {@link ImageReference.Builder#setContentUri(String)} throws an exception.
+     */
+    @Test
+    public void builder_contentUriOnlyAcceptsContentSchema() {
+        ImageReference.Builder builder = new ImageReference.Builder();
+        assertThrows(NullPointerException.class, () -> builder.setContentUri(null));
+        assertThrows(IllegalArgumentException.class, () -> builder.setContentUri(""));
+        assertThrows(IllegalArgumentException.class, () -> builder.setContentUri("http://foo"));
+        assertThrows(IllegalArgumentException.class, () -> builder.setContentUri("content:/foo"));
+        assertThrows(IllegalArgumentException.class, () -> builder.setContentUri("content//foo"));
+    }
+
+    /**
+     * Even if a content URI was not received, {@link ImageReference#getRawContentUri()} should
+     * return an empty string.
+     */
+    @Test
+    public void contentUri_unsetContentUriReturnsEmptyRawUri() {
+        assertEquals("", new ImageReference().getRawContentUri());
+    }
+
+    /**
+     * If {@link ImageReference.Builder#setContentUri(String)} is not used, then
+     * {@link ImageReference#getContentUri(int, int)} should return null.
+     */
+    @Test
+    public void contentUri_unsetContentUriRetursNullUri() {
+        assertEquals(null, new ImageReference().getContentUri(TEST_WIDTH, TEST_HEIGHT));
+    }
+
+    /**
+     * {@link ImageReference#getContentUri(int, int)} should provide a fully formed URI containing
+     * with and height parameters.
+     */
+    @Test
+    public void contentUri_requestMustContainWidthAndHeightParameters() {
+        ImageReference image = new ImageReference.Builder()
+                .setContentUri(TEST_CONTENT_URI)
+                .setOriginalSize(1, 1)
+                .build();
+
+        assertEquals(Uri.parse("content://foo?w=123&h=234"),
+                image.getContentUri(TEST_WIDTH, TEST_HEIGHT));
+    }
+
+    /**
+     * {@link ImageReference#getContentUri(int, int)} throws an exception if size is not provided.
+     */
+    @Test
+    public void contentUri_widthAndHeightParametersMustBePositive() {
+        ImageReference image = new ImageReference.Builder()
+                .setContentUri(TEST_CONTENT_URI)
+                .setOriginalSize(1, 1)
+                .build();
+
+        assertThrows(IllegalArgumentException.class, () -> image.getContentUri(0, TEST_HEIGHT));
+        assertThrows(IllegalArgumentException.class, () -> image.getContentUri(TEST_WIDTH, 0));
+    }
+}
diff --git a/car/cluster/src/androidTest/java/androidx/car/cluster/navigation/LaneTest.java b/car/cluster/src/androidTest/java/androidx/car/cluster/navigation/LaneTest.java
index cd9c8e2..865d972 100644
--- a/car/cluster/src/androidTest/java/androidx/car/cluster/navigation/LaneTest.java
+++ b/car/cluster/src/androidTest/java/androidx/car/cluster/navigation/LaneTest.java
@@ -16,12 +16,18 @@
 
 package androidx.car.cluster.navigation;
 
+import static androidx.car.cluster.navigation.utils.Assertions.assertImmutable;
+
+import static org.junit.Assert.assertEquals;
+
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.ArrayList;
+
 /**
  * Tests for {@link Lane} serialization
  */
@@ -31,9 +37,34 @@
     /**
      * Tests that lists returned by {@link Lane} are immutable.
      */
-    @Test(expected = UnsupportedOperationException.class)
-    public void immutableLists() {
-        Lane lane = new Lane.Builder().build();
-        lane.getDirections().add(new LaneDirection.Builder().build());
+    @Test
+    public void immutability() {
+        assertImmutable(new Lane.Builder().build().getDirections());
+        assertImmutable(new Lane().getDirections());
+    }
+
+    /**
+     * Tests that even if we receive a null list of {@link LaneDirection}s, we return an empty list
+     * to the consumers.
+     */
+    @Test
+    public void nullability_directionsListIsNeverNull() {
+        assertEquals(new ArrayList<>(), new Lane().getDirections());
+    }
+
+    /**
+     * Returns a sample {@link Lane} for testing.
+     */
+    public static Lane createSampleLane() {
+        return new Lane.Builder()
+                .addDirection(new LaneDirection.Builder()
+                        .setShape(LaneDirection.Shape.NORMAL_LEFT)
+                        .setHighlighted(true)
+                        .build())
+                .addDirection(new LaneDirection.Builder()
+                        .setShape(LaneDirection.Shape.STRAIGHT)
+                        .setHighlighted(true)
+                        .build())
+                .build();
     }
 }
diff --git a/car/cluster/src/androidTest/java/androidx/car/cluster/navigation/NavigationStateTest.java b/car/cluster/src/androidTest/java/androidx/car/cluster/navigation/NavigationStateTest.java
index 601b87f..f657b9b 100644
--- a/car/cluster/src/androidTest/java/androidx/car/cluster/navigation/NavigationStateTest.java
+++ b/car/cluster/src/androidTest/java/androidx/car/cluster/navigation/NavigationStateTest.java
@@ -16,6 +16,8 @@
 
 package androidx.car.cluster.navigation;
 
+import static androidx.car.cluster.navigation.utils.Assertions.assertImmutable;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
 
@@ -145,28 +147,19 @@
     }
 
     /**
-     * Tests that {@link NavigationState#mDestinations} is immutable.
+     * Tests that {@link NavigationState} is immutable.
      */
-    @Test(expected = UnsupportedOperationException.class)
-    public void immutableDestionationsLists() {
-        NavigationState state = createEmptyState();
-        state.getDestinations().add(new Destination.Builder().build());
-    }
-
-    /**
-     * Tests that {@link NavigationState#mSteps} is immutable.
-     */
-    @Test(expected = UnsupportedOperationException.class)
-    public void immutableStepsLists() {
-        NavigationState state = createEmptyState();
-        state.getSteps().add(new Step.Builder().build());
+    @Test
+    public void immutability() {
+        assertImmutable(createEmptyState().getDestinations());
+        assertImmutable(createEmptyState().getSteps());
     }
 
     /**
      * Test a few equality conditions
      */
     @Test
-    public void equalityTest() {
+    public void equality() {
         // Testing empty nav state cases
         assertEquals(new NavigationState(), new NavigationState.Builder().build());
         assertEquals(new NavigationState(), new NavigationState.Builder()
@@ -234,36 +227,7 @@
 
     private NavigationState createSampleState() {
         return new NavigationState.Builder()
-                .addStep(new Step.Builder()
-                        .setManeuver(new Maneuver.Builder()
-                                .setType(Maneuver.Type.DEPART).build())
-                        .setDistance(new Distance(10, "10", Distance.Unit.METERS))
-                        .addLane(new Lane.Builder()
-                                .addDirection(new LaneDirection.Builder()
-                                        .setShape(LaneDirection.Shape.NORMAL_LEFT)
-                                        .setHighlighted(true)
-                                        .build())
-                                .addDirection(new LaneDirection.Builder()
-                                        .setShape(LaneDirection.Shape.STRAIGHT)
-                                        .setHighlighted(true)
-                                        .build())
-                                .build())
-                        .addLane(new Lane.Builder()
-                                .addDirection(new LaneDirection.Builder()
-                                        .setShape(LaneDirection.Shape.SHARP_LEFT)
-                                        .build())
-                                .addDirection(new LaneDirection.Builder()
-                                        .setShape(LaneDirection.Shape.NORMAL_LEFT)
-                                        .build())
-                                .build())
-                        .build())
-                .addStep(new Step.Builder()
-                        .setManeuver(new Maneuver.Builder()
-                                .setType(Maneuver.Type.ROUNDABOUT_EXIT)
-                                .setRoundaboutExitNumber(2)
-                                .build())
-                        .setDistance(new Distance(15, "15", Distance.Unit.METERS))
-                        .build())
+                .addStep(StepTest.createSampleStep())
                 .addDestination(new Destination.Builder()
                         .setTitle("Home")
                         .setDistance(new Distance(1230, "1.2", Distance.Unit.KILOMETERS))
diff --git a/car/cluster/src/androidTest/java/androidx/car/cluster/navigation/RichTextElementTest.java b/car/cluster/src/androidTest/java/androidx/car/cluster/navigation/RichTextElementTest.java
new file mode 100644
index 0000000..fce32e19
--- /dev/null
+++ b/car/cluster/src/androidTest/java/androidx/car/cluster/navigation/RichTextElementTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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.car.cluster.navigation;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for {@link RichText} serialization
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class RichTextElementTest {
+    private static final String TEST_TEXT = "foo";
+    private static final ImageReference TEST_IMAGE = ImageReferenceTest.createSampleImage();
+
+    /**
+     * Test a few equality conditions
+     */
+    @Test
+    public void equality() {
+        RichTextElement element = createSampleElement();
+
+        assertEquals(element, createSampleElement());
+        assertNotEquals(element, new RichTextElement.Builder()
+                .setImage(TEST_IMAGE)
+                .build(""));
+        assertNotEquals(element, new RichTextElement.Builder()
+                .setImage(null)
+                .build(TEST_TEXT));
+
+        assertEquals(element.hashCode(), createSampleElement().hashCode());
+    }
+
+    /**
+     * Test that a text representation must be provided.
+     */
+    @Test(expected = NullPointerException.class)
+    public void builder_textIsMandatory() {
+        new RichTextElement.Builder().build(null);
+    }
+
+    /**
+     * Test that an empty string will be returned from {@link RichTextElement#getText()} to the
+     * consumer, even if no string was received.
+     */
+    @Test
+    public void text_isEmptyIfNotProvided() {
+        assertEquals("", new RichTextElement().getText());
+    }
+
+    /**
+     * Returns a sample {@link RichTextElement} for testing.
+     */
+    public RichTextElement createSampleElement() {
+        return new RichTextElement.Builder()
+                .setImage(TEST_IMAGE)
+                .build(TEST_TEXT);
+    }
+}
diff --git a/car/cluster/src/androidTest/java/androidx/car/cluster/navigation/RichTextTest.java b/car/cluster/src/androidTest/java/androidx/car/cluster/navigation/RichTextTest.java
new file mode 100644
index 0000000..488b650
--- /dev/null
+++ b/car/cluster/src/androidTest/java/androidx/car/cluster/navigation/RichTextTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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.car.cluster.navigation;
+
+import static androidx.car.cluster.navigation.utils.Assertions.assertImmutable;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+
+/**
+ * Tests for {@link RichText} serialization
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class RichTextTest {
+    private static final String TEST_TEXT = "foo";
+
+    /**
+     * Test a few equality conditions
+     */
+    @Test
+    public void equality() {
+        RichText expected = createSampleRichText();
+        RichTextElement element = new RichTextElement.Builder().build(TEST_TEXT);
+
+        assertEquals(expected, createSampleRichText());
+        assertNotEquals(expected, new RichText.Builder().build());
+        assertNotEquals(expected, new RichText.Builder()
+                .addElement(element)
+                .addElement(element)
+                .build());
+
+        assertEquals(expected.hashCode(), createSampleRichText().hashCode());
+    }
+
+    /**
+     * Tests that lists returned by {@link RichText} are immutable.
+     */
+    @Test
+    public void immutability() {
+        assertImmutable(new RichText.Builder().build().getElements());
+        assertImmutable(new RichText().getElements());
+    }
+
+    /**
+     * Tests that even if we receive a null list of {@link RichTextElement}s, we return an empty
+     * list to the consumers.
+     */
+    @Test
+    public void nullability_elementsListIsNeverNull() {
+        assertEquals(new ArrayList<>(), new RichText().getElements());
+    }
+
+    /**
+     * Builder doesn't accept null elements
+     */
+    @Test(expected = NullPointerException.class)
+    public void builder_elementsCantBeNull() {
+        new RichText.Builder().addElement(null);
+    }
+
+    /**
+     * Returns a sample {@link RichText} instance for testing.
+     */
+    public static RichText createSampleRichText() {
+        return new RichText.Builder()
+                .addElement(new RichTextElement.Builder().build(TEST_TEXT))
+                .build();
+    }
+}
diff --git a/car/cluster/src/androidTest/java/androidx/car/cluster/navigation/SegmentTest.java b/car/cluster/src/androidTest/java/androidx/car/cluster/navigation/SegmentTest.java
index a1f61a9..93a1a36 100644
--- a/car/cluster/src/androidTest/java/androidx/car/cluster/navigation/SegmentTest.java
+++ b/car/cluster/src/androidTest/java/androidx/car/cluster/navigation/SegmentTest.java
@@ -35,7 +35,7 @@
      * Test a few equality conditions
      */
     @Test
-    public void equalityTests() {
+    public void equality() {
         assertEquals(new Segment(), new Segment(""));
         assertEquals(new Segment("foo"), new Segment("foo"));
         assertNotEquals(new Segment("foo"), new Segment("bar"));
@@ -48,7 +48,7 @@
      * Test null on {@link Segment} constructor
      */
     @Test(expected = NullPointerException.class)
-    public void nullHandlingTests() {
+    public void nullability() {
         new Segment(null);
     }
 }
diff --git a/car/cluster/src/androidTest/java/androidx/car/cluster/navigation/StepTest.java b/car/cluster/src/androidTest/java/androidx/car/cluster/navigation/StepTest.java
index 12e6ce6..020bcd5 100644
--- a/car/cluster/src/androidTest/java/androidx/car/cluster/navigation/StepTest.java
+++ b/car/cluster/src/androidTest/java/androidx/car/cluster/navigation/StepTest.java
@@ -16,6 +16,11 @@
 
 package androidx.car.cluster.navigation;
 
+import static androidx.car.cluster.navigation.utils.Assertions.assertImmutable;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
@@ -29,11 +34,75 @@
 @SmallTest
 public class StepTest {
     /**
-     * Tests that lists returned by {@link Step} are immutable.
+     * Test a few equality conditions
      */
-    @Test(expected = UnsupportedOperationException.class)
-    public void immutableLists() {
-        Step step = new Step.Builder().build();
-        step.getLanes().add(new Lane.Builder().build());
+    @Test
+    public void equality() {
+        Step expected = createSampleStep();
+
+        assertEquals(expected, createSampleStep());
+        assertNotEquals(expected, new Step.Builder()
+                .setDistance(new Distance(10, "10", Distance.Unit.METERS))
+                .setManeuver(new Maneuver.Builder().setType(Maneuver.Type.DEPART).build())
+                .addLane(LaneTest.createSampleLane())
+                .setLanesImage(ImageReferenceTest.createSampleImage())
+                .build());
+        assertNotEquals(expected, new Step.Builder()
+                .setCue(RichTextTest.createSampleRichText())
+                .setManeuver(new Maneuver.Builder().setType(Maneuver.Type.DEPART).build())
+                .addLane(LaneTest.createSampleLane())
+                .setLanesImage(ImageReferenceTest.createSampleImage())
+                .build());
+        assertNotEquals(expected, new Step.Builder()
+                .setCue(RichTextTest.createSampleRichText())
+                .setDistance(new Distance(10, "10", Distance.Unit.METERS))
+                .addLane(LaneTest.createSampleLane())
+                .setLanesImage(ImageReferenceTest.createSampleImage())
+                .build());
+        assertNotEquals(expected, new Step.Builder()
+                .setCue(RichTextTest.createSampleRichText())
+                .setDistance(new Distance(10, "10", Distance.Unit.METERS))
+                .setManeuver(new Maneuver.Builder().setType(Maneuver.Type.DEPART).build())
+                .build());
+        assertNotEquals(expected, new Step.Builder()
+                .setCue(RichTextTest.createSampleRichText())
+                .setDistance(new Distance(10, "10", Distance.Unit.METERS))
+                .setManeuver(new Maneuver.Builder().setType(Maneuver.Type.DEPART).build())
+                .addLane(LaneTest.createSampleLane())
+                .addLane(LaneTest.createSampleLane())
+                .setLanesImage(ImageReferenceTest.createSampleImage())
+                .build());
+
+        assertEquals(expected.hashCode(), createSampleStep().hashCode());
+    }
+
+    /**
+     * Lists returned by {@link Step} are immutable.
+     */
+    @Test
+    public void immutability_lanesListIsNeverNull() {
+        assertImmutable(new Step.Builder().build().getLanes());
+        assertImmutable(new Step().getLanes());
+    }
+
+    /**
+     * Builder doesn't accept null lanes
+     */
+    @Test(expected = NullPointerException.class)
+    public void builder_lanesCantBeNull() {
+        new Step.Builder().addLane(null);
+    }
+
+    /**
+     * Returns a sample {@link Step} for testing
+     */
+    public static Step createSampleStep() {
+        return new Step.Builder()
+                .setCue(RichTextTest.createSampleRichText())
+                .setDistance(new Distance(10, "10", Distance.Unit.METERS))
+                .setManeuver(new Maneuver.Builder().setType(Maneuver.Type.DEPART).build())
+                .addLane(LaneTest.createSampleLane())
+                .setLanesImage(ImageReferenceTest.createSampleImage())
+                .build();
     }
 }
diff --git a/car/cluster/src/androidTest/java/androidx/car/cluster/navigation/utils/Assertions.java b/car/cluster/src/androidTest/java/androidx/car/cluster/navigation/utils/Assertions.java
new file mode 100644
index 0000000..51410ad
--- /dev/null
+++ b/car/cluster/src/androidTest/java/androidx/car/cluster/navigation/utils/Assertions.java
@@ -0,0 +1,53 @@
+/*
+ * 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.car.cluster.navigation.utils;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import java.util.List;
+
+/**
+ * A collection of utility methods that supports asserting conditions in tests.
+ */
+public class Assertions {
+
+    private Assertions() {
+        // No instantiable class.
+    }
+
+    /**
+     * Asserts the given block throws the given throwable.
+     */
+    public static void assertThrows(Class<? extends Throwable> throwableClass, Runnable runnable) {
+        try {
+            runnable.run();
+            fail();
+        } catch (Throwable e) {
+            assertEquals(e.getClass(), throwableClass);
+        }
+    }
+
+    /**
+     * Asserts the given list can not be mutated.
+     */
+    public static void assertImmutable(List<?> list) {
+        assertThrows(UnsupportedOperationException.class, () -> list.add(null));
+        assertThrows(UnsupportedOperationException.class, () -> list.set(0, null));
+        assertThrows(UnsupportedOperationException.class, () -> list.remove(0));
+    }
+}
diff --git a/car/cluster/src/main/java/androidx/car/cluster/navigation/Common.java b/car/cluster/src/main/java/androidx/car/cluster/navigation/Common.java
index 31b47c4..8c83496 100644
--- a/car/cluster/src/main/java/androidx/car/cluster/navigation/Common.java
+++ b/car/cluster/src/main/java/androidx/car/cluster/navigation/Common.java
@@ -48,14 +48,14 @@
     }
 
     /**
-     * Returns the given list, or an empty one if the list is null, or if any of its elements
-     * is null. The returned list should not be mutated.
+     * Returns an immutable view of the given list, or an empty one if the list is null, or if any
+     * of its elements is null.
      */
     @NonNull
-    public static <T> List<T> nonNullOrEmpty(@Nullable List<T> list) {
+    public static <T> List<T> immutableOrEmpty(@Nullable List<T> list) {
         if (list == null || list.stream().anyMatch(Objects::isNull)) {
             return Collections.emptyList();
         }
-        return list;
+        return Collections.unmodifiableList(list);
     }
 }
diff --git a/car/cluster/src/main/java/androidx/car/cluster/navigation/Destination.java b/car/cluster/src/main/java/androidx/car/cluster/navigation/Destination.java
index bd80c81..246ac94 100644
--- a/car/cluster/src/main/java/androidx/car/cluster/navigation/Destination.java
+++ b/car/cluster/src/main/java/androidx/car/cluster/navigation/Destination.java
@@ -71,11 +71,11 @@
      * Builder for creating a {@link Destination}
      */
     public static final class Builder {
-        String mTitle;
-        String mAddress;
-        Distance mDistance;
-        Time mEta;
-        LatLng mLatLng;
+        private String mTitle;
+        private String mAddress;
+        private Distance mDistance;
+        private Time mEta;
+        private LatLng mLatLng;
 
         /**
          * Sets the destination title (formatted for the current user's locale), or empty if there
diff --git a/car/cluster/src/main/java/androidx/car/cluster/navigation/ImageReference.java b/car/cluster/src/main/java/androidx/car/cluster/navigation/ImageReference.java
new file mode 100644
index 0000000..9e2ede7
--- /dev/null
+++ b/car/cluster/src/main/java/androidx/car/cluster/navigation/ImageReference.java
@@ -0,0 +1,271 @@
+/*
+ * 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.car.cluster.navigation;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.annotation.SuppressLint;
+import android.net.Uri;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.core.util.Preconditions;
+import androidx.versionedparcelable.ParcelField;
+import androidx.versionedparcelable.VersionedParcelable;
+import androidx.versionedparcelable.VersionedParcelize;
+
+import java.util.Objects;
+
+/**
+ * Reference to an image. This class encapsulates a 'content://' style URI plus metadata that allows
+ * consumers to know the image they will receive and how to handle it.
+ *
+ * <ul>
+ * <li><b>Sizing:</b> Producers will always provide an image "original" size which defines the image
+ * aspect ratio. When requesting these images, consumers must always specify a desired size (width
+ * and height) based on UI available space and the provided aspect ration. Producers can use this
+ * "requested" size to select the best version of the requested image, and producers can optionally
+ * resize the image to exactly match the "requested" size provided, but consumers should not assume
+ * that the received image will match such size. Instead, consumers should always assume that the
+ * image will require additional scaling.
+ * <li><b>Content:</b> Producers should avoid including margins around the image content.
+ * <li><b>Format:</b> Content URI must reference a file with MIME type 'image/png', 'image/jpeg'
+ * or 'image/bmp' (vector images are not supported).
+ * <li><b>Color:</b> Images can be either "tintable" or not. A "tintable" image is such that all its
+ * content is defined in its alpha channel, while its color (all other channels) can be altered
+ * without losing information (e.g.: icons). A non "tintable" images contains information in all its
+ * channels (e.g.: photos).
+ * <li><b>Caching:</b> Given the same image reference and the same requested size, producers must
+ * return the exact same image. This means that it should be safe for the consumer to cache an image
+ * once downloaded and use this image reference plus requested size as key, for as long as they
+ * need. If a producer needs to provide a different version of a certain image, they must provide a
+ * different image reference (e.g. producers can opt to include version information as part of the
+ * content URI).
+ * </ul>
+ */
+@VersionedParcelize
+public class ImageReference implements VersionedParcelable {
+    private static final String SCHEME = "content://";
+    private static final String WIDTH_HINT_PARAMETER = "w";
+    private static final String HEIGHT_HINT_PARAMETER = "h";
+
+    @ParcelField(1)
+    String mContentUri;
+    @ParcelField(2)
+    int mOriginalWidth;
+    @ParcelField(3)
+    int mOriginalHeight;
+    @ParcelField(4)
+    boolean mIsTintable;
+
+    /**
+     * Used by {@link VersionedParcelable}
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    ImageReference() {
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    ImageReference(@NonNull String contentUri,
+            @IntRange(from = 1, to = Integer.MAX_VALUE) int originalWidth,
+            @IntRange(from = 1, to = Integer.MAX_VALUE) int originalHeight,
+            boolean isTintable) {
+        mContentUri = Preconditions.checkNotNull(contentUri);
+        mOriginalWidth = Preconditions.checkArgumentInRange(originalWidth, 1,
+                Integer.MAX_VALUE, "originalWidth");
+        mOriginalHeight = Preconditions.checkArgumentInRange(originalHeight, 1,
+                Integer.MAX_VALUE, "originalHeight");
+        mIsTintable = isTintable;
+    }
+
+    /**
+     * Builder for creating an {@link ImageReference}.
+     */
+    public static final class Builder {
+        private String mContentUri;
+        private int mOriginalWidth;
+        private int mOriginalHeight;
+        private boolean mIsTintable;
+
+        /**
+         * Sets a 'content://' style URI
+         *
+         * @return this object for chaining
+         * @throws NullPointerException if the provided {@code contentUri} is null
+         * @throws IllegalArgumentException if the provided {@code contentUri} doesn't start with
+         *                                  'content://'.
+         */
+        @NonNull
+        public Builder setContentUri(@NonNull String contentUri) {
+            Preconditions.checkNotNull(contentUri);
+            Preconditions.checkArgument(contentUri.startsWith(SCHEME));
+            mContentUri = contentUri;
+            return this;
+        }
+
+        /**
+         * Sets the aspect ratio of this image, expressed as with and height sizes. Both dimensions
+         * must be greater than 0.
+         *
+         * @return this object for chaining
+         * @throws IllegalArgumentException if any of the dimensions is not positive.
+         */
+        @NonNull
+        public Builder setOriginalSize(@IntRange(from = 1, to = Integer.MAX_VALUE) int width,
+                @IntRange(from = 1, to = Integer.MAX_VALUE) int height) {
+            Preconditions.checkArgumentInRange(width, 1, Integer.MAX_VALUE, "width");
+            Preconditions.checkArgumentInRange(height, 1, Integer.MAX_VALUE, "height");
+            mOriginalWidth = width;
+            mOriginalHeight = height;
+            return this;
+        }
+
+        /**
+         * Sets whether this image is "tintable" or not. An image is "tintable" when all its
+         * content is defined in its alpha-channel, designed to be colorized (e.g. using
+         * {@link android.graphics.PorterDuff.Mode#SRC_ATOP} image composition).
+         * If this method is not used, images will be non "tintable" by default.
+         *
+         * @return this object for chaining
+         */
+        @NonNull
+        public Builder setIsTintable(boolean isTintable) {
+            mIsTintable = isTintable;
+            return this;
+        }
+
+        /**
+         * Returns a {@link ImageReference} built with the provided information. Calling
+         * {@link ImageReference.Builder#setContentUri(String)} and
+         * {@link ImageReference.Builder#setOriginalSize(int, int)} before calling this method is
+         * mandatory.
+         *
+         * @return an {@link ImageReference} instance
+         * @throws NullPointerException if content URI is not provided.
+         * @throws IllegalArgumentException if original size is not set.
+         */
+        @NonNull
+        public ImageReference build() {
+            return new ImageReference(mContentUri, mOriginalWidth, mOriginalHeight, mIsTintable);
+        }
+    }
+
+    /**
+     * Returns a 'content://' style URI that can be used to retrieve the actual image, or an empty
+     * string if the URI provided by the producer doesn't comply with the format requirements. If
+     * this URI is used as-is, the size of the resulting image is undefined.
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @NonNull
+    public String getRawContentUri() {
+        String value = Common.nonNullOrEmpty(mContentUri);
+        return value.startsWith(SCHEME) ? value : "";
+    }
+
+    /**
+     * Returns a fully formed {@link Uri} that can be used to retrieve the actual image, including
+     * size constraints, or null if this image reference is not properly formed.
+     * <p>
+     * Producers can optionally use these size constraints to provide an optimized version of the
+     * image, but the resulting image might still not match the requested size.
+     * <p>
+     * Consumers must confirm the size of the received image and scale it proportionally (
+     * maintaining the aspect ratio of the received image) if it doesn't match the desired
+     * dimensions.
+     *
+     * @param width desired maximum width (must be greater than 0)
+     * @param height desired maximum height (must be greater than 0)
+     * @return fully formed {@link Uri}, or null if this image reference can not be used.
+     */
+    @Nullable
+    public Uri getContentUri(@IntRange(from = 1, to = Integer.MAX_VALUE) int width,
+            @IntRange(from = 1, to = Integer.MAX_VALUE) int height) {
+        Preconditions.checkArgumentInRange(width, 1, Integer.MAX_VALUE, "width");
+        Preconditions.checkArgumentInRange(height, 1, Integer.MAX_VALUE, "height");
+        String contentUri = getRawContentUri();
+        if (contentUri.isEmpty()) {
+            // We have an invalid content URI.
+            return null;
+        }
+        return Uri.parse(contentUri).buildUpon()
+                .appendQueryParameter(WIDTH_HINT_PARAMETER, String.valueOf(width))
+                .appendQueryParameter(HEIGHT_HINT_PARAMETER, String.valueOf(height))
+                .build();
+    }
+
+    /**
+     * Returns the image width, which should only be used to determine the image aspect ratio.
+     */
+    public int getOriginalWidth() {
+        return mOriginalWidth;
+    }
+
+    /**
+     * Returns the image height, which should only be used to determine the image aspect ratio.
+     */
+    public int getOriginalHeight() {
+        return mOriginalHeight;
+    }
+
+    /**
+     * Returns whether this image is "tintable" or not. An image is "tintable" when all its
+     * content is defined in its alpha-channel, designed to be colorized (e.g. using
+     * {@link android.graphics.PorterDuff.Mode#SRC_ATOP} image composition).
+     */
+    public boolean isTintable() {
+        return mIsTintable;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        ImageReference image = (ImageReference) o;
+        return Objects.equals(getRawContentUri(), image.getRawContentUri())
+                && getOriginalWidth() == image.getOriginalWidth()
+                && getOriginalHeight() == image.getOriginalHeight()
+                && isTintable() == image.isTintable();
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(getRawContentUri(), getOriginalWidth(), getOriginalHeight(),
+                isTintable());
+    }
+
+    // DefaultLocale suppressed as this method is only offered for debugging purposes.
+    @SuppressLint("DefaultLocale")
+    @Override
+    public String toString() {
+        return String.format("{contentUri: '%s', originalWidth: %d, originalHeight: %d, "
+                        + "isTintable: %s}",
+                mContentUri, mOriginalWidth, mOriginalHeight, mIsTintable);
+    }
+}
diff --git a/car/cluster/src/main/java/androidx/car/cluster/navigation/Lane.java b/car/cluster/src/main/java/androidx/car/cluster/navigation/Lane.java
index 509a7bd..6d7e734 100644
--- a/car/cluster/src/main/java/androidx/car/cluster/navigation/Lane.java
+++ b/car/cluster/src/main/java/androidx/car/cluster/navigation/Lane.java
@@ -26,7 +26,6 @@
 import androidx.versionedparcelable.VersionedParcelize;
 
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
 
@@ -54,14 +53,14 @@
      */
     @RestrictTo(LIBRARY_GROUP)
     Lane(@NonNull List<LaneDirection> directions) {
-        mDirections = Collections.unmodifiableList(new ArrayList<>(directions));
+        mDirections = new ArrayList<>(directions);
     }
 
     /**
      * Builder for creating a {@link Lane}
      */
     public static final class Builder {
-        List<LaneDirection> mDirections = new ArrayList<>();
+        private List<LaneDirection> mDirections = new ArrayList<>();
 
         /**
          * Add a possible direction a driver can take from this lane.
@@ -86,7 +85,7 @@
      */
     @NonNull
     public List<LaneDirection> getDirections() {
-        return Common.nonNullOrEmpty(mDirections);
+        return Common.immutableOrEmpty(mDirections);
     }
 
     @Override
diff --git a/car/cluster/src/main/java/androidx/car/cluster/navigation/LaneDirection.java b/car/cluster/src/main/java/androidx/car/cluster/navigation/LaneDirection.java
index 85338a0..94c2976 100644
--- a/car/cluster/src/main/java/androidx/car/cluster/navigation/LaneDirection.java
+++ b/car/cluster/src/main/java/androidx/car/cluster/navigation/LaneDirection.java
@@ -108,8 +108,8 @@
      * Builder for creating a {@link LaneDirection}
      */
     public static final class Builder {
-        EnumWrapper<Shape> mShape;
-        boolean mHighlighted;
+        private EnumWrapper<Shape> mShape;
+        private boolean mHighlighted;
 
         /**
          * Sets the {@link Shape} of this lane direction, and any fallback values that could be used
diff --git a/car/cluster/src/main/java/androidx/car/cluster/navigation/Maneuver.java b/car/cluster/src/main/java/androidx/car/cluster/navigation/Maneuver.java
index cd6705b..c607ab2 100644
--- a/car/cluster/src/main/java/androidx/car/cluster/navigation/Maneuver.java
+++ b/car/cluster/src/main/java/androidx/car/cluster/navigation/Maneuver.java
@@ -21,6 +21,7 @@
 import android.annotation.SuppressLint;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.versionedparcelable.ParcelField;
 import androidx.versionedparcelable.VersionedParcelable;
@@ -286,6 +287,8 @@
     EnumWrapper<Type> mType;
     @ParcelField(2)
     int mRoundaboutExitNumber;
+    @ParcelField(3)
+    ImageReference mIcon;
 
     /**
      * Used by {@link VersionedParcelable}
@@ -300,17 +303,20 @@
      * @hide
      */
     @RestrictTo(LIBRARY_GROUP)
-    Maneuver(@NonNull EnumWrapper<Type> type, int roundaboutExitNumber) {
+    Maneuver(@NonNull EnumWrapper<Type> type, int roundaboutExitNumber,
+            @Nullable ImageReference icon) {
         mType = type;
         mRoundaboutExitNumber = roundaboutExitNumber;
+        mIcon = icon;
     }
 
     /**
      * Builder for creating a {@link Maneuver}
      */
     public static final class Builder {
-        EnumWrapper<Type> mType;
-        int mRoundaboutExitNumber;
+        private EnumWrapper<Type> mType;
+        private int mRoundaboutExitNumber;
+        private ImageReference mIcon;
 
 
         /**
@@ -346,11 +352,21 @@
         }
 
         /**
+         * Sets a reference to an image presenting this maneuver. The provided image must be
+         * optimized to be presented in a square canvas (aspect ratio of 1:1).
+         */
+        @NonNull
+        public Builder setIcon(@Nullable ImageReference icon) {
+            mIcon = icon;
+            return this;
+        }
+
+        /**
          * Returns a {@link Maneuver} built with the provided information.
          */
         @NonNull
         public Maneuver build() {
-            return new Maneuver(mType, mRoundaboutExitNumber);
+            return new Maneuver(mType, mRoundaboutExitNumber, mIcon);
         }
     }
 
@@ -375,6 +391,16 @@
         return mRoundaboutExitNumber;
     }
 
+    /**
+     * Returns a reference to an image representing this maneuver, or null if image representation
+     * is not available. This image is optimized to be displayed in a square canvas (aspect ratio of
+     * 1:1).
+     */
+    @Nullable
+    public ImageReference getIcon() {
+        return mIcon;
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) {
@@ -385,18 +411,20 @@
         }
         Maneuver maneuver = (Maneuver) o;
         return getRoundaboutExitNumber() == maneuver.getRoundaboutExitNumber()
-                && Objects.equals(getType(), maneuver.getType());
+                && Objects.equals(getType(), maneuver.getType())
+                && Objects.equals(getIcon(), maneuver.getIcon());
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(getType(), getRoundaboutExitNumber());
+        return Objects.hash(getType(), getRoundaboutExitNumber(), getIcon());
     }
 
     // DefaultLocale suppressed as this method is only offered for debugging purposes.
     @SuppressLint("DefaultLocale")
     @Override
     public String toString() {
-        return String.format("{type: %s, roundaboutExitNumer: %d}", mType, mRoundaboutExitNumber);
+        return String.format("{type: %s, roundaboutExitNumer: %d, icon: %s}", mType,
+                mRoundaboutExitNumber, mIcon);
     }
 }
diff --git a/car/cluster/src/main/java/androidx/car/cluster/navigation/NavigationState.java b/car/cluster/src/main/java/androidx/car/cluster/navigation/NavigationState.java
index 0074fbe..93f3888 100644
--- a/car/cluster/src/main/java/androidx/car/cluster/navigation/NavigationState.java
+++ b/car/cluster/src/main/java/androidx/car/cluster/navigation/NavigationState.java
@@ -31,7 +31,6 @@
 import androidx.versionedparcelable.VersionedParcelize;
 
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
 
@@ -87,8 +86,8 @@
             @NonNull List<Destination> destinations,
             @Nullable Segment currentSegment,
             @NonNull EnumWrapper<ServiceStatus> serviceStatus) {
-        mSteps = Collections.unmodifiableList(new ArrayList<>(steps));
-        mDestinations = Collections.unmodifiableList(new ArrayList<>(destinations));
+        mSteps = new ArrayList<>(steps);
+        mDestinations = new ArrayList<>(destinations);
         mCurrentSegment = currentSegment;
         mServiceStatus = Preconditions.checkNotNull(serviceStatus);
     }
@@ -97,10 +96,10 @@
      * Builder for creating a {@link NavigationState}
      */
     public static final class Builder {
-        List<Step> mSteps = new ArrayList<>();
-        List<Destination> mDestinations = new ArrayList<>();
-        Segment mCurrentSegment;
-        EnumWrapper<ServiceStatus> mServiceStatus = new EnumWrapper<>();
+        private List<Step> mSteps = new ArrayList<>();
+        private List<Destination> mDestinations = new ArrayList<>();
+        private Segment mCurrentSegment;
+        private EnumWrapper<ServiceStatus> mServiceStatus = new EnumWrapper<>();
 
         /**
          * Add a navigation step. Steps should be provided in order of execution. It is up to the
@@ -168,7 +167,7 @@
      */
     @NonNull
     public List<Step> getSteps() {
-        return Common.nonNullOrEmpty(mSteps);
+        return Common.immutableOrEmpty(mSteps);
     }
 
     /**
@@ -177,7 +176,7 @@
      */
     @NonNull
     public List<Destination> getDestinations() {
-        return Common.nonNullOrEmpty(mDestinations);
+        return Common.immutableOrEmpty(mDestinations);
     }
 
     /**
diff --git a/car/cluster/src/main/java/androidx/car/cluster/navigation/RichText.java b/car/cluster/src/main/java/androidx/car/cluster/navigation/RichText.java
new file mode 100644
index 0000000..ab488d6
--- /dev/null
+++ b/car/cluster/src/main/java/androidx/car/cluster/navigation/RichText.java
@@ -0,0 +1,114 @@
+/*
+ * 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.car.cluster.navigation;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.core.util.Preconditions;
+import androidx.versionedparcelable.ParcelField;
+import androidx.versionedparcelable.VersionedParcelable;
+import androidx.versionedparcelable.VersionedParcelize;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Immutable sequence of graphic elements (e.g.: text, images) to be displayed one after another in
+ * the same way as a {@link CharSequence} would. Elements in this sequence are represented by
+ * {@link RichTextElement} instances.
+ */
+@VersionedParcelize
+public class RichText implements VersionedParcelable {
+    @ParcelField(1)
+    List<RichTextElement> mElements;
+
+    /**
+     * Used by {@link VersionedParcelable}
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    RichText() {
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    RichText(@NonNull List<RichTextElement> elements) {
+        mElements = new ArrayList<>(elements);
+    }
+
+    /**
+     * Builder for creating a {@link RichText}
+     */
+    public static final class Builder {
+        private List<RichTextElement> mElements = new ArrayList<>();
+
+        /**
+         * Adds a graphic element to the rich text sequence.
+         *
+         * @param element a graphic element to add to the sequence.
+         * @return this object for chaining
+         */
+        @NonNull
+        public Builder addElement(@NonNull RichTextElement element) {
+            mElements.add(Preconditions.checkNotNull(element));
+            return this;
+        }
+
+        /**
+         * Returns a {@link RichText} built with the provided information.
+         */
+        @NonNull
+        public RichText build() {
+            return new RichText(mElements);
+        }
+    }
+
+    /**
+     * Returns the sequence of graphic elements
+     */
+    @NonNull
+    List<RichTextElement> getElements() {
+        return Common.immutableOrEmpty(mElements);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        RichText richText = (RichText) o;
+        return Objects.equals(getElements(), richText.getElements());
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(getElements());
+    }
+
+    @Override
+    public String toString() {
+        return String.format("{elements: %s}", mElements);
+    }
+}
diff --git a/car/cluster/src/main/java/androidx/car/cluster/navigation/RichTextElement.java b/car/cluster/src/main/java/androidx/car/cluster/navigation/RichTextElement.java
new file mode 100644
index 0000000..1343444
--- /dev/null
+++ b/car/cluster/src/main/java/androidx/car/cluster/navigation/RichTextElement.java
@@ -0,0 +1,142 @@
+/*
+ * 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.car.cluster.navigation;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.core.util.Preconditions;
+import androidx.versionedparcelable.ParcelField;
+import androidx.versionedparcelable.VersionedParcelable;
+import androidx.versionedparcelable.VersionedParcelize;
+
+import java.util.Objects;
+
+/**
+ * An item in a {@link RichText} sequence, acting as a union of different graphic elements that can
+ * be displayed one after another.
+ * <p>
+ * All {@link RichTextElement} must contain a textual representation of its content, which will be
+ * used by consumers incapable of rendering the desired graphic element. A {@link RichTextElement}
+ * can only contain one other graphic element. Consumers must attempt to render such element and
+ * only fallback to text if needed.
+ * <p>
+ * New graphic element types might be added in the future. If such elements are unknown to the
+ * consumer, they will be delivered to the consumer as just text.
+ */
+@VersionedParcelize
+public class RichTextElement implements VersionedParcelable {
+    @ParcelField(1)
+    String mText;
+    @ParcelField(2)
+    ImageReference mImage;
+
+    /**
+     * Used by {@link VersionedParcelable}
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    RichTextElement() {
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public RichTextElement(@NonNull String text, @Nullable ImageReference image) {
+        mText = Preconditions.checkNotNull(text, "A textual representation of this "
+                + "element must be provided.");
+        mImage = image;
+    }
+
+    /**
+     * Builder for creating a {@link RichTextElement}
+     */
+    public static class Builder {
+        private ImageReference mImage;
+
+        /**
+         * Sets an image to be displayed as part of the {@link RichText} sequence. Images in the
+         * same {@link RichText} sequence are expected to be rendered with equal height but variable
+         * width.
+         *
+         * @param image image reference to be used to represent this element, or null if only the
+         *              textual representation should be used.
+         * @return this object for chaining
+         */
+        public Builder setImage(@Nullable ImageReference image) {
+            // Note: if new graphic element types are added in the future, this API should enforce
+            // that no more than one of them is set at each moment.
+            mImage = image;
+            return this;
+        }
+
+        /**
+         * Builds a {@link RichTextElement} with the given textual representation, and any other
+         * optional representation provided to this builder. If no other graphic element is provided
+         * or if such graphic element cannot be rendered by the consumer, this text will be used
+         * instead.
+         *
+         * @param text textual representation to use
+         */
+        public RichTextElement build(@NonNull String text) {
+            return new RichTextElement(Preconditions.checkNotNull(text), mImage);
+        }
+    }
+
+    /**
+     * Returns the textual representation of this element
+     */
+    @NonNull
+    String getText() {
+        return Common.nonNullOrEmpty(mText);
+    }
+
+    /**
+     * Returns an image representing this element, or null if the textual representation should be
+     * used instead.
+     */
+    @Nullable
+    ImageReference getImage() {
+        return mImage;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        RichTextElement element = (RichTextElement) o;
+        return Objects.equals(getText(), element.getText())
+                && Objects.equals(getImage(), element.getImage());
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(getText(), getImage());
+    }
+
+    @Override
+    public String toString() {
+        return String.format("{text: '%s', image: %s}", mText, mImage);
+    }
+}
diff --git a/car/cluster/src/main/java/androidx/car/cluster/navigation/Step.java b/car/cluster/src/main/java/androidx/car/cluster/navigation/Step.java
index 78c89b7..096f29d 100644
--- a/car/cluster/src/main/java/androidx/car/cluster/navigation/Step.java
+++ b/car/cluster/src/main/java/androidx/car/cluster/navigation/Step.java
@@ -27,7 +27,6 @@
 import androidx.versionedparcelable.VersionedParcelize;
 
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
 
@@ -44,6 +43,10 @@
     Maneuver mManeuver;
     @ParcelField(3)
     List<Lane> mLanes;
+    @ParcelField(4)
+    ImageReference mLanesImage;
+    @ParcelField(5)
+    RichText mCue;
 
     /**
      * Used by {@link VersionedParcelable}
@@ -58,19 +61,24 @@
      * @hide
      */
     @RestrictTo(LIBRARY_GROUP)
-    Step(@Nullable Distance distance, @Nullable Maneuver maneuver, @NonNull List<Lane> lanes) {
+    Step(@Nullable Distance distance, @Nullable Maneuver maneuver, @NonNull List<Lane> lanes,
+            @Nullable ImageReference lanesImage, @Nullable RichText cue) {
         mDistance = distance;
         mManeuver = maneuver;
-        mLanes = Collections.unmodifiableList(new ArrayList<>(lanes));
+        mLanes = new ArrayList<>(lanes);
+        mLanesImage = lanesImage;
+        mCue = cue;
     }
 
     /**
      * Builder for creating a {@link Step}
      */
     public static final class Builder {
-        Distance mDistance;
-        Maneuver mManeuver;
-        List<Lane> mLanes = new ArrayList<>();
+        private Distance mDistance;
+        private Maneuver mManeuver;
+        private List<Lane> mLanes = new ArrayList<>();
+        private ImageReference mLanesImage;
+        private RichText mCue;
 
         /**
          * Sets the distance from the current position to the point where this navigation step
@@ -98,6 +106,11 @@
 
         /**
          * Adds a road lane configuration to this step. Lanes should be added from left to right.
+         * <p>
+         * If lanes configuration information is available, producers should provide both image (see
+         * {@link #setLanesImage(ImageReference)}) and metadata (through this method) for maximum
+         * interoperability, as some consumers might use images while others might use metadata, or
+         * both.
          *
          * @return this object for chaining
          */
@@ -108,11 +121,59 @@
         }
 
         /**
+         * Sets a reference to an image that represents a complete lanes configuration at this point
+         * in the navigation. The image, if provided, is expected to contain:
+         *
+         * <ul>
+         * <li>A representation of all lanes, one next to the other in a single row.
+         * <li>For each lane, a set of arrows, representing each possible driving directions
+         * (e.g.: straight, left turn, right turn, etc.) within such lane.
+         * <li>Each of such driving directions that would keep the driver within the navigation
+         * route should be highlighted.
+         * </ul>
+         *
+         * Lane configuration images are expected to be displayed in a canvas with fixed height and
+         * variable width.
+         * <p>
+         * If lanes configuration information is available, producers should provide both metadata
+         * (see {@link #addLane(Lane)}) and image (through this method) for maximum
+         * interoperability, as some consumers might use images while others might use metadata, or
+         * both.
+         *
+         * @return this object for chaining
+         */
+        @NonNull
+        public Builder setLanesImage(@Nullable ImageReference lanesImage) {
+            mLanesImage = lanesImage;
+            return this;
+        }
+
+        /**
+         * Sets auxiliary instructions on how complete this navigation step, described as a
+         * {@link RichText} object containing a sequence of texts (e.g.: "towards", "Wallaby way")
+         * and images (e.g.: road badge of a highway).
+         * <p>
+         * If consumers don't have enough space to display the complete content of this
+         * {@link RichText} instance, it is expected they will truncate these instructions by
+         * cutting its end.
+         * <p>
+         * Because of this, it is expected the most important part of these instructions to be
+         * located at the beginning of the sequence.
+         *
+         * @return this object for chaining
+         */
+        @NonNull
+        public Builder setCue(@Nullable RichText cue) {
+            mCue = cue;
+            return this;
+        }
+
+        /**
          * Returns a {@link Step} built with the provided information.
          */
         @NonNull
         public Step build() {
-            return new Step(mDistance, mManeuver, mLanes);
+            return new Step(mDistance, mManeuver, mLanes, mLanesImage, mCue);
         }
     }
 
@@ -136,11 +197,47 @@
 
     /**
      * Returns an unmodifiable list containing the configuration of road lanes at the point where
-     * the driver should execute this step. Lane configurations are listed from left to right.
+     * the driver should execute this step, or an empty list if lane configuration metadata is not
+     * available. Lane configurations are listed from left to right.
      */
     @NonNull
     public List<Lane> getLanes() {
-        return Common.nonNullOrEmpty(mLanes);
+        return Common.immutableOrEmpty(mLanes);
+    }
+
+    /**
+     * Returns an image representing the lanes configuration at this point in the navigation, or
+     * null if the lanes configuration image was not provided. The image, if provided, is expected
+     * to contain:
+     *
+     * <ul>
+     * <li>A representation of all lanes, one next to the other in a single row.
+     * <li>For each lane, a set of arrows, representing each possible driving directions
+     * (e.g.: straight, left turn, right turn, etc.) within such lane.
+     * <li>Each of such driving directions that would keep the driver within the navigation
+     * route should be highlighted.
+     * </ul>
+     *
+     * Lane configuration images are expected to be displayed in a canvas with fixed height and
+     * variable width.
+     */
+    @Nullable
+    public ImageReference getLanesImage() {
+        return mLanesImage;
+    }
+
+    /**
+     * Returns auxiliary instructions on how complete this navigation step, described as a
+     * {@link RichText} object containing a sequence of texts (e.g.: "towards", "Wallaby way")
+     * and images (e.g.: road badge of a highway).
+     * <p>
+     * If space is not enough to display the complete content of this {@link RichText} instance,
+     * consumers must display the beginning of these instructions, cutting as much from the end
+     * as needed.
+     */
+    @Nullable
+    public RichText getCue() {
+        return mCue;
     }
 
     @Override
@@ -154,17 +251,19 @@
         Step step = (Step) o;
         return Objects.equals(getManeuver(), step.getManeuver())
                 && Objects.equals(getDistance(), step.getDistance())
-                && Objects.equals(getLanes(), step.getLanes());
+                && Objects.equals(getLanes(), step.getLanes())
+                && Objects.equals(getLanesImage(), step.getLanesImage())
+                && Objects.equals(getCue(), step.getCue());
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(getManeuver(), getDistance(), getLanes());
+        return Objects.hash(getManeuver(), getDistance(), getLanes(), getLanesImage(), getCue());
     }
 
     @Override
     public String toString() {
-        return String.format("{maneuver: %s, distance: %s, lanes: %s}", mManeuver, mDistance,
-                mLanes);
+        return String.format("{maneuver: %s, distance: %s, lanes: %s, lanesImage: %s, cue: %s}",
+                mManeuver, mDistance, mLanes, mLanesImage, mCue);
     }
 }
diff --git a/car/core/res/drawable/car_spinner_background.xml b/car/core/res/drawable/car_spinner_background.xml
index 8a14fd2..c03d263c 100644
--- a/car/core/res/drawable/car_spinner_background.xml
+++ b/car/core/res/drawable/car_spinner_background.xml
@@ -18,11 +18,6 @@
 
 <ripple xmlns:android="http://schemas.android.com/apk/res/android"
     android:color="?android:attr/colorControlHighlight">
-    <item>
-        <shape>
-            <solid android:color="?android:attr/colorPrimary"/>
-        </shape>
-    </item>
     <item
         android:width="@dimen/car_spinner_dropdown_arrow_size"
         android:height="@dimen/car_spinner_dropdown_arrow_size"
diff --git a/car/core/res/layout/preference_material_car.xml b/car/core/res/layout/preference_material_car.xml
index 03abfc9..0c4b20c 100644
--- a/car/core/res/layout/preference_material_car.xml
+++ b/car/core/res/layout/preference_material_car.xml
@@ -36,10 +36,11 @@
         android:layout_marginBottom="@dimen/car_padding_2"/>
 
     <LinearLayout
-        android:layout_width="wrap_content"
+        android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:orientation="vertical"
         android:layout_toEndOf="@android:id/icon"
+        android:layout_toStartOf="@android:id/widget_frame"
         android:layout_centerVertical="true"
         android:layout_marginTop="@dimen/car_padding_2"
         android:layout_marginBottom="@dimen/car_padding_2">
diff --git a/car/core/res/values/styles.xml b/car/core/res/values/styles.xml
index c72daf8..3770347 100644
--- a/car/core/res/values/styles.xml
+++ b/car/core/res/values/styles.xml
@@ -210,6 +210,21 @@
         <item name="navigationIcon">@drawable/ic_nav_arrow_back</item>
     </style>
 
+    <!-- The styling for the default action bar. -->
+    <style name="Widget.Car.ActionBar" parent="Widget.AppCompat.Light.ActionBar.Solid">
+        <item name="titleTextStyle">@style/TextAppearance.Car.Body1</item>
+        <item name="subtitleTextStyle">@style/TextAppearance.Car.Body3</item>
+    </style>
+
+    <!-- The styling for the default action bar that has a dark background. -->
+    <style name="Widget.Car.ActionBar.Dark">
+        <item name="background">@android:color/black</item>
+        <item name="backgroundStacked">@android:color/black</item>
+        <item name="backgroundSplit">@android:color/black</item>
+        <item name="titleTextStyle">@style/TextAppearance.Car.Body1.Light</item>
+        <item name="subtitleTextStyle">@style/TextAppearance.Car.Body3.Light</item>
+    </style>
+
     <!-- ================ -->
     <!-- CarToolbar Style -->
     <!-- ================ -->
diff --git a/car/core/res/values/themes.xml b/car/core/res/values/themes.xml
index e6c232c..cbbaeac 100644
--- a/car/core/res/values/themes.xml
+++ b/car/core/res/values/themes.xml
@@ -21,7 +21,7 @@
 
     <!-- Base style for the Car that will have dark colors for card backgrounds and light color for
          text. The colors will not change for night mode. -->
-    <style name="Theme.Car.NoActionBar" parent="Theme.AppCompat.NoActionBar">
+    <style name="Theme.Car" parent="Theme.AppCompat.Light.DarkActionBar">
         <item name="android:alertDialogTheme">@style/Theme.Car.Dark.Dialog.Alert</item>
         <item name="android:colorAccent">@color/car_accent_light</item>
         <item name="android:colorForeground">@color/car_body2_light</item>
@@ -55,6 +55,7 @@
         <item name="android:spinnerItemStyle">@style/CarSpinnerItem</item>
         <item name="android:spinnerDropDownItemStyle">@style/CarSpinnerItem</item>
         <item name="actionBarItemBackground">@drawable/car_card_ripple_background</item>
+        <item name="actionBarStyle">@style/Widget.Car.ActionBar.Dark</item>
         <item name="actionBarSize">@dimen/car_app_bar_height</item>
         <item name="actionButtonStyle">@style/Widget.Car.ActionButton.Light</item>
         <item name="actionMenuTextAppearance">@style/TextAppearance.Car.ActionBar.Menu</item>
@@ -86,6 +87,12 @@
         <item name="toolbarStyle">@style/Widget.Car.Toolbar.Light</item>
     </style>
 
+    <!-- Base style for the car that removes the action bar. -->
+    <style name="Theme.Car.NoActionBar">
+        <item name="windowActionBar">false</item>
+        <item name="windowNoTitle">true</item>
+    </style>
+
     <!-- Light theme for the Car. This theme has light colors for card backgrounds and dark
          text. These colors will invert for night. -->
     <style name="Theme.Car.Light.NoActionBar" parent="Theme.Car.NoActionBar">
diff --git a/car/core/src/main/java/androidx/car/widget/ActionBar.java b/car/core/src/main/java/androidx/car/widget/ActionBar.java
index a2497ad..21aaadb 100644
--- a/car/core/src/main/java/androidx/car/widget/ActionBar.java
+++ b/car/core/src/main/java/androidx/car/widget/ActionBar.java
@@ -341,7 +341,7 @@
         TransitionManager.beginDelayedTransition(mRowsContainer, mIsExpanded ? mFadeIn : mFadeOut);
 
         for (int i = 0; i < mNumExtraRowsInUse; i++) {
-            mRowsContainer.getChildAt(i).setVisibility(mIsExpanded ? View.VISIBLE : View.GONE);
+            mRowsContainer.getChildAt(i).setVisibility(mIsExpanded ? VISIBLE : GONE);
         }
     }
 
diff --git a/car/core/src/main/java/androidx/car/widget/AlphaJumpOverlayView.java b/car/core/src/main/java/androidx/car/widget/AlphaJumpOverlayView.java
index 1546828..8afce1e 100644
--- a/car/core/src/main/java/androidx/car/widget/AlphaJumpOverlayView.java
+++ b/car/core/src/main/java/androidx/car/widget/AlphaJumpOverlayView.java
@@ -109,7 +109,7 @@
      */
     public void show() {
         mAdapter.onAlphaJumpEnter();
-        setVisibility(View.VISIBLE);
+        setVisibility(VISIBLE);
         startAnimation(mOpenAnimation);
     }
 
@@ -118,6 +118,6 @@
      */
     public void hide() {
         startAnimation(mCloseAnimation);
-        setVisibility(View.GONE);
+        setVisibility(GONE);
     }
 }
\ No newline at end of file
diff --git a/car/core/src/main/java/androidx/car/widget/CarToolbar.java b/car/core/src/main/java/androidx/car/widget/CarToolbar.java
index 0f281c1..b941f65 100644
--- a/car/core/src/main/java/androidx/car/widget/CarToolbar.java
+++ b/car/core/src/main/java/androidx/car/widget/CarToolbar.java
@@ -135,7 +135,7 @@
                 desiredHeight, MeasureSpec.AT_MOST);
 
         int width = 0;
-        if (mNavButtonView.getVisibility() != View.GONE) {
+        if (mNavButtonView.getVisibility() != GONE) {
             // Size of nav button is fixed.
             int measureSpec = MeasureSpec.makeMeasureSpec(mNavButtonIconSize, MeasureSpec.EXACTLY);
             mNavButtonView.measure(measureSpec, measureSpec);
@@ -144,7 +144,7 @@
             int navWidth = Math.max(mNavButtonContainerWidth, mNavButtonView.getMeasuredWidth());
             width += navWidth + getHorizontalMargins(mNavButtonView);
         }
-        if (mTitleTextView.getVisibility() != View.GONE) {
+        if (mTitleTextView.getVisibility() != GONE) {
             measureChild(mTitleTextView, widthMeasureSpec, width, childHeightMeasureSpec, 0);
             width += mTitleTextView.getMeasuredWidth() + getHorizontalMargins(mTitleTextView);
         }
@@ -158,7 +158,7 @@
         int height = bottom - top;
         int layoutLeft = getPaddingLeft();
 
-        if (mNavButtonView.getVisibility() != View.GONE) {
+        if (mNavButtonView.getVisibility() != GONE) {
             // Nav button is centered in container.
             int navButtonWidth = mNavButtonView.getMeasuredWidth();
             int containerWidth = Math.max(mNavButtonContainerWidth, navButtonWidth);
@@ -168,7 +168,7 @@
             layoutLeft += containerWidth;
         }
 
-        if (mTitleTextView.getVisibility() != View.GONE) {
+        if (mTitleTextView.getVisibility() != GONE) {
             layoutViewVerticallyCentered(mTitleTextView, layoutLeft, height);
         }
     }
@@ -199,11 +199,11 @@
      */
     public void setNavigationIcon(@Nullable Icon icon) {
         if (icon == null) {
-            mNavButtonView.setVisibility(View.GONE);
+            mNavButtonView.setVisibility(GONE);
             mNavButtonView.setImageDrawable(null);
             return;
         }
-        mNavButtonView.setVisibility(View.VISIBLE);
+        mNavButtonView.setVisibility(VISIBLE);
         mNavButtonView.setImageDrawable(icon.loadDrawable(getContext()));
     }
 
@@ -267,7 +267,7 @@
     public void setTitle(CharSequence title) {
         mTitleText = title;
         mTitleTextView.setText(title);
-        mTitleTextView.setVisibility(TextUtils.isEmpty(title) ? View.GONE : View.VISIBLE);
+        mTitleTextView.setVisibility(TextUtils.isEmpty(title) ? GONE : VISIBLE);
     }
 
     /**
diff --git a/car/core/src/main/java/androidx/car/widget/PagedListView.java b/car/core/src/main/java/androidx/car/widget/PagedListView.java
index 10ffded..e832b46 100644
--- a/car/core/src/main/java/androidx/car/widget/PagedListView.java
+++ b/car/core/src/main/java/androidx/car/widget/PagedListView.java
@@ -982,7 +982,7 @@
             return;
         }
 
-        mScrollBarView.setVisibility(View.VISIBLE);
+        mScrollBarView.setVisibility(VISIBLE);
         mScrollBarView.setUpEnabled(!isAtStart);
         mScrollBarView.setDownEnabled(!isAtEnd);
 
@@ -1022,7 +1022,7 @@
         if ((isAtStart && isAtEnd) || layoutManager == null || layoutManager.getItemCount() == 0) {
             mScrollBarView.setVisibility(View.INVISIBLE);
         } else {
-            mScrollBarView.setVisibility(View.VISIBLE);
+            mScrollBarView.setVisibility(VISIBLE);
         }
         mScrollBarView.setUpEnabled(!isAtStart);
         mScrollBarView.setDownEnabled(!isAtEnd);
diff --git a/car/core/src/main/java/androidx/car/widget/PagedScrollBarView.java b/car/core/src/main/java/androidx/car/widget/PagedScrollBarView.java
index f33b8f7..3542965 100644
--- a/car/core/src/main/java/androidx/car/widget/PagedScrollBarView.java
+++ b/car/core/src/main/java/androidx/car/widget/PagedScrollBarView.java
@@ -178,7 +178,7 @@
     }
 
     void setShowAlphaJump(boolean show) {
-        mAlphaJumpButton.setVisibility(show ? View.VISIBLE : View.GONE);
+        mAlphaJumpButton.setVisibility(show ? VISIBLE : GONE);
     }
 
     /** Returns {@code true} if the scroll bar thumb is visible */
@@ -192,7 +192,7 @@
      */
     public void setScrollbarThumbEnabled(boolean show) {
         mShowScrollBarThumb = show;
-        mScrollThumb.setVisibility(mShowScrollBarThumb ? View.VISIBLE : View.GONE);
+        mScrollThumb.setVisibility(mShowScrollBarThumb ? VISIBLE : GONE);
     }
 
     /**
@@ -223,7 +223,7 @@
         }
 
         // If the scroll bars aren't visible, then no need to update.
-        if (getVisibility() == View.GONE || range == 0) {
+        if (getVisibility() == GONE || range == 0) {
             return;
         }
 
@@ -257,7 +257,7 @@
      */
     void setParametersInLayout(int range, int offset, int extent) {
         // If the scroll bars aren't visible, then no need to update.
-        if (getVisibility() == View.GONE || range == 0) {
+        if (getVisibility() == GONE || range == 0) {
             return;
         }
 
diff --git a/cardview/build.gradle b/cardview/build.gradle
index c950dac..7df2ca3 100644
--- a/cardview/build.gradle
+++ b/cardview/build.gradle
@@ -18,7 +18,7 @@
 supportLibrary {
     name = "Android Support CardView v7"
     publish = true
-    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenVersion = LibraryVersions.CARDVIEW
     mavenGroup = LibraryGroups.CARDVIEW
     inceptionYear = "2011"
     description = "Android Support CardView v7"
diff --git a/collection/build.gradle b/collection/build.gradle
index 11fac32..4bc2d3a 100644
--- a/collection/build.gradle
+++ b/collection/build.gradle
@@ -23,7 +23,7 @@
 }
 
 dependencies {
-    compile(project(":annotation"))
+    compile("androidx.annotation:annotation:1.0.0")
     annotationProcessor(NULLAWAY)
     testCompile(JUNIT)
 }
diff --git a/content/build.gradle b/content/build.gradle
index abc6fe7..c2a5c78 100644
--- a/content/build.gradle
+++ b/content/build.gradle
@@ -35,7 +35,7 @@
 supportLibrary {
     name = "Android Support Content"
     publish = true
-    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenVersion = LibraryVersions.CONTENTPAGER
     mavenGroup = LibraryGroups.CONTENTPAGER
     inceptionYear = "2017"
     description = "Library providing support for paging across content exposed via a ContentProvider. Use of this library allows a client to avoid expensive interprocess \"cursor window swaps\" on the UI thread."
diff --git a/core/build.gradle b/core/build.gradle
index 6f57be3..2fb9f51 100644
--- a/core/build.gradle
+++ b/core/build.gradle
@@ -11,6 +11,8 @@
     api("androidx.collection:collection:1.0.0")
     api(ARCH_LIFECYCLE_RUNTIME, libs.exclude_annotations_transitive)
     api project(':versionedparcelable')
+    api(GUAVA_LISTENABLE_FUTURE)
+    implementation(project(":concurrent:concurrent-futures"))
 
     androidTestImplementation(TEST_RUNNER)
     androidTestImplementation(TEST_RULES)
diff --git a/core/ktx/build.gradle b/core/ktx/build.gradle
index b627dec..0d3473e 100644
--- a/core/ktx/build.gradle
+++ b/core/ktx/build.gradle
@@ -25,12 +25,13 @@
     androidTestImplementation(TEST_RULES)
     androidTestImplementation(TRUTH)
     androidTestImplementation(project(":internal-testutils-ktx"))
+    androidTestImplementation(GUAVA_LISTENABLE_FUTURE_AVOID_CONFLICT)
 }
 
 supportLibrary {
     name = "Core Kotlin Extensions"
     publish = true
-    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenVersion = LibraryVersions.CORE_KTX
     mavenGroup = LibraryGroups.CORE
     inceptionYear = "2018"
     description = "Kotlin extensions for 'core' artifact"
diff --git a/core/src/androidTest/java/androidx/core/content/pm/ShareTargetXmlParserTest.java b/core/src/androidTest/java/androidx/core/content/pm/ShareTargetXmlParserTest.java
new file mode 100644
index 0000000..692b84d
--- /dev/null
+++ b/core/src/androidTest/java/androidx/core/content/pm/ShareTargetXmlParserTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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.core.content.pm;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.spy;
+
+import android.content.Context;
+import android.content.ContextWrapper;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ShareTargetXmlParserTest {
+
+    private static final ArrayList<ShareTargetCompat> sExpectedValues = new ArrayList<>();
+
+    private Context mContext;
+
+    @Before
+    public void setup() {
+        mContext = spy(new ContextWrapper(InstrumentationRegistry.getContext()));
+        initExpectedValues();
+    }
+
+    private void initExpectedValues() {
+        // These values must exactly match the content of shortcuts.xml resource
+        sExpectedValues.add(new ShareTargetCompat(
+                new ShareTargetCompat.TargetData[]{
+                        new ShareTargetCompat.TargetData("http", "www.google.com", "1234",
+                                "somePath", "somePathPattern", "somePathPrefix", "text/plain")},
+                "com.test.googlex.directshare.TestActivity1",
+                new String[]{"com.test.googlex.category.CATEGORY1",
+                        "com.test.googlex.category.CATEGORY2"}));
+        sExpectedValues.add(new ShareTargetCompat(
+                new ShareTargetCompat.TargetData[]{
+                        new ShareTargetCompat.TargetData(null, null, null, null, null, null,
+                                "video/mp4"),
+                        new ShareTargetCompat.TargetData("content", null, null, null, null, null,
+                                "video/*")},
+                "com.test.googlex.directshare.TestActivity5",
+                new String[]{"com.test.googlex.category.CATEGORY5",
+                        "com.test.googlex.category.CATEGORY6"}));
+    }
+
+    private void assertShareTargetEquals(ShareTargetCompat expected, ShareTargetCompat actual) {
+        assertEquals(expected.mTargetData.length, actual.mTargetData.length);
+        for (int i = 0; i < expected.mTargetData.length; i++) {
+            assertEquals(expected.mTargetData[i].mScheme, actual.mTargetData[i].mScheme);
+            assertEquals(expected.mTargetData[i].mHost, actual.mTargetData[i].mHost);
+            assertEquals(expected.mTargetData[i].mPort, actual.mTargetData[i].mPort);
+            assertEquals(expected.mTargetData[i].mPath, actual.mTargetData[i].mPath);
+            assertEquals(expected.mTargetData[i].mPathPrefix, actual.mTargetData[i].mPathPrefix);
+            assertEquals(expected.mTargetData[i].mPathPattern, actual.mTargetData[i].mPathPattern);
+            assertEquals(expected.mTargetData[i].mMimeType, actual.mTargetData[i].mMimeType);
+        }
+
+        assertEquals(expected.mTargetClass, actual.mTargetClass);
+
+        assertEquals(expected.mCategories.length, actual.mCategories.length);
+        for (int i = 0; i < expected.mCategories.length; i++) {
+            assertEquals(expected.mCategories[i], actual.mCategories[i]);
+        }
+    }
+
+    /**
+     * Tests if ShareTargetXmlParser is able to:
+     * a) locate the xml resource and read it
+     * b) ignore the legacy shortcut definitions if any
+     * c) drop incomplete share-target definitions
+     * d) read and return the expected share-targets
+     */
+    @Test
+    public void testGetShareTargets() {
+        ArrayList<ShareTargetCompat> shareTargets = ShareTargetXmlParser.getShareTargets(mContext);
+
+        assertNotNull(shareTargets);
+        assertEquals(sExpectedValues.size(), shareTargets.size());
+        for (int i = 0; i < sExpectedValues.size(); i++) {
+            assertShareTargetEquals(sExpectedValues.get(i), shareTargets.get(i));
+        }
+    }
+}
diff --git a/core/src/androidTest/java/androidx/core/content/pm/ShortcutInfoCompatSaverTest.java b/core/src/androidTest/java/androidx/core/content/pm/ShortcutInfoCompatSaverTest.java
new file mode 100644
index 0000000..3cbb9553
--- /dev/null
+++ b/core/src/androidTest/java/androidx/core/content/pm/ShortcutInfoCompatSaverTest.java
@@ -0,0 +1,417 @@
+/*
+ * 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.core.content.pm;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.spy;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.drawable.Icon;
+
+import androidx.core.app.Person;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.core.test.R;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class ShortcutInfoCompatSaverTest {
+
+    private static final String ID_SHORTCUT_RESOURCE_ICON = "shortcut-resource-icon";
+    private static final String ID_SHORTCUT_BITMAP_ICON = "shortcut-bitmap-icon";
+    private static final String ID_SHORTCUT_ADAPTIVE_BITMAP_ICON = "shortcut-adaptive-bitmap-icon";
+    private static final String ID_SHORTCUT_NO_ICON = "shortcut-no-icon";
+
+    private Context mContext;
+    private ShortcutInfoCompatSaver mShortcutInfoSaver;
+    private ExecutorService mCacheUpdateService;
+    private ExecutorService mDiskIoService;
+
+    private List<ShortcutInfoCompat> mTestShortcuts = new ArrayList<>();
+
+    private IconCompat mTestResourceIcon;
+    private IconCompat mTestBitmapIcon;
+    private IconCompat mTestAdaptiveBitmapIcon;
+
+    @Before
+    public void setup() {
+        mContext = spy(new ContextWrapper(InstrumentationRegistry.getContext()));
+        mCacheUpdateService = ShortcutInfoCompatSaver.createExecutorService();
+        mDiskIoService = ShortcutInfoCompatSaver.createExecutorService();
+        mShortcutInfoSaver = new ShortcutInfoCompatSaver(mContext, mCacheUpdateService,
+                mDiskIoService);
+        catchAsyncExceptions(mShortcutInfoSaver.removeAllShortcuts());
+
+        HashSet<String> testCategories = new HashSet<>();
+        testCategories.add("TestCategory1");
+        testCategories.add("TestCategory2");
+
+        Person[] testPersons = {
+                new Person.Builder().setName("test person 1").build(),
+                new Person.Builder().setName("test person 2").build()};
+
+        Intent[] testIntents = {new Intent("TestAction1"), new Intent("TestAction2")};
+        testIntents[1].setClassName("test package", "test class");
+
+        Bitmap redBitmap = Bitmap.createBitmap(200, 150, Bitmap.Config.ARGB_8888);
+        redBitmap.eraseColor(Color.RED);
+        Bitmap blueBitmap = Bitmap.createBitmap(150, 200, Bitmap.Config.ARGB_8888);
+        blueBitmap.eraseColor(Color.BLUE);
+
+        mTestResourceIcon = IconCompat.createWithResource(mContext, R.drawable.bmp_test);
+        mTestBitmapIcon = IconCompat.createWithBitmap(redBitmap);
+        mTestAdaptiveBitmapIcon = IconCompat.createWithAdaptiveBitmap(blueBitmap);
+
+        mTestShortcuts.add(new ShortcutInfoCompat.Builder(mContext, ID_SHORTCUT_RESOURCE_ICON)
+                .setShortLabel("test short label 1")
+                .setLongLabel("test long label 1")
+                .setDisabledMessage("test disabled message 1")
+                .setLongLived()
+                .setPersons(testPersons)
+                .setCategories(testCategories)
+                .setActivity(new ComponentName("test package", "test class"))
+                .setAlwaysBadged()
+                .setIcon(mTestResourceIcon)
+                .setIntents(testIntents)
+                .build());
+        mTestShortcuts.add(new ShortcutInfoCompat.Builder(mContext, ID_SHORTCUT_BITMAP_ICON)
+                .setShortLabel("test short label 2")
+                .setCategories(testCategories)
+                .setActivity(new ComponentName("test package", "test class"))
+                .setIcon(mTestBitmapIcon)
+                .setIntent(testIntents[0])
+                .build());
+        mTestShortcuts.add(new ShortcutInfoCompat.Builder(mContext,
+                ID_SHORTCUT_ADAPTIVE_BITMAP_ICON)
+                .setShortLabel("test short label 3")
+                .setLongLabel("test long label 3")
+                .setDisabledMessage("test disabled message 3")
+                .setCategories(testCategories)
+                .setActivity(new ComponentName("test package", "test class"))
+                .setAlwaysBadged()
+                .setIcon(mTestAdaptiveBitmapIcon)
+                .setIntent(testIntents[1])
+                .build());
+        mTestShortcuts.add(new ShortcutInfoCompat.Builder(mContext, ID_SHORTCUT_NO_ICON)
+                .setShortLabel("test short label 4")
+                .setLongLabel("test long label 4")
+                .setPerson(testPersons[0])
+                .setCategories(testCategories)
+                .setIntents(testIntents)
+                .build());
+        mTestShortcuts.add(new ShortcutInfoCompat.Builder(mContext, "shortcut-no-category")
+                .setShortLabel("test short label 5")
+                .setActivity(new ComponentName("test package", "test class"))
+                .setIcon(mTestResourceIcon)
+                .setIntents(testIntents)
+                .build());
+    }
+
+    @After
+    public void tearDown() {
+        try {
+            mShortcutInfoSaver.removeAllShortcuts().get();
+        } catch (Exception e) {
+            /* Ignore */
+        }
+    }
+
+    private List<ShortcutInfoCompat> testShortcutsWithCategories() {
+        List<ShortcutInfoCompat> shortcuts = new ArrayList<>();
+        for (ShortcutInfoCompat item : mTestShortcuts) {
+            Set<String> categories = item.getCategories();
+            if (categories != null && !categories.isEmpty()) {
+                shortcuts.add(item);
+            }
+        }
+        return shortcuts;
+    }
+
+    private void assertShortcutsListEquals(List<ShortcutInfoCompat> expected,
+            List<ShortcutInfoCompat> actual) {
+        assertNotNull(expected);
+        assertNotNull(actual);
+        assertEquals(expected.size(), actual.size());
+
+        // The order in the lists is not important
+        for (ShortcutInfoCompat expectedShortcut : expected) {
+            boolean exists = false;
+            for (ShortcutInfoCompat actualShortcut : actual) {
+                if (expectedShortcut.getId().equals(actualShortcut.getId())) {
+                    assertShortcutEquals(expectedShortcut, actualShortcut);
+                    exists = true;
+                }
+            }
+            assertTrue(exists);
+        }
+    }
+
+    private void assertShortcutEquals(ShortcutInfoCompat expected, ShortcutInfoCompat actual) {
+        assertNotNull(expected);
+        assertNotNull(actual);
+
+        assertEquals(expected.getId(), actual.getId());
+        assertEquals(expected.getDisabledMessage(), actual.getDisabledMessage());
+        assertEquals(expected.getLongLabel(), actual.getLongLabel());
+        assertEquals(expected.getShortLabel(), actual.getShortLabel());
+
+        if (expected.getActivity() == null) {
+            assertNull(actual.getActivity());
+        } else {
+            assertNotNull(actual.getActivity());
+            assertEquals(expected.getActivity().getPackageName(),
+                    actual.getActivity().getPackageName());
+            assertEquals(expected.getActivity().getClassName(),
+                    actual.getActivity().getClassName());
+        }
+
+        if (expected.getCategories() == null) {
+            assertNull(actual.getCategories());
+        } else {
+            assertNotNull(actual.getCategories());
+            Set<String> expectedCats = expected.getCategories();
+            Set<String> actualCats = actual.getCategories();
+            assertEquals(expectedCats.size(), actualCats.size());
+            assertTrue(actualCats.containsAll(expectedCats));
+        }
+
+        Intent[] expIntents = expected.getIntents();
+        Intent[] actIntents = actual.getIntents();
+        assertEquals(expIntents.length, actIntents.length);
+        for (int i = 0; i < expIntents.length; i++) {
+            assertEquals(expIntents[i].getAction(), actIntents[i].getAction());
+            if (expIntents[i].getComponent() == null) {
+                assertNull(actIntents[i].getComponent());
+            } else {
+                assertEquals(expIntents[i].getComponent().flattenToString(),
+                        actIntents[i].getComponent().flattenToString());
+            }
+        }
+    }
+
+    @Test
+    public void testGetInstance() {
+        ShortcutInfoCompatSaver saver = ShortcutInfoCompatSaver.getInstance(mContext);
+        assertNotNull(saver);
+        assertEquals(saver, ShortcutInfoCompatSaver.getInstance(mContext));
+    }
+
+    @Test
+    public void testGetShortcuts_noShortcuts() throws Exception {
+        List<ShortcutInfoCompat> shortcuts = mShortcutInfoSaver.getShortcuts();
+
+        assertNotNull(shortcuts);
+        assertTrue(shortcuts.isEmpty());
+    }
+
+    @Test
+    public void testAddShortcuts_skipShortcutsWithNoCategories() throws Exception {
+        catchAsyncExceptions(mShortcutInfoSaver.addShortcuts(mTestShortcuts));
+        assertShortcutsListEquals(testShortcutsWithCategories(), mShortcutInfoSaver.getShortcuts());
+    }
+
+    @Test
+    public void testAddShortcuts_forceReload() throws Exception {
+        ListenableFuture<?> future = mShortcutInfoSaver.addShortcuts(mTestShortcuts);
+        catchAsyncExceptions(future);
+        forceReloadFromDisk(future);
+
+        assertShortcutsListEquals(testShortcutsWithCategories(), mShortcutInfoSaver.getShortcuts());
+    }
+
+    @Test
+    public void testAddShortcuts_incrementalAddWithIdOverlap() throws Exception {
+        List<ShortcutInfoCompat> firstBatch = new ArrayList<>();
+        firstBatch.add(mTestShortcuts.get(0));
+        firstBatch.add(mTestShortcuts.get(1));
+        List<ShortcutInfoCompat> secondBatch = new ArrayList<>();
+        secondBatch.add(mTestShortcuts.get(1));
+        secondBatch.add(mTestShortcuts.get(2));
+        List<ShortcutInfoCompat> allShortcuts = new ArrayList<>();
+        allShortcuts.add(mTestShortcuts.get(0));
+        allShortcuts.add(mTestShortcuts.get(1));
+        allShortcuts.add(mTestShortcuts.get(2));
+
+        catchAsyncExceptions(mShortcutInfoSaver.addShortcuts(firstBatch));
+        ListenableFuture<Void> future = mShortcutInfoSaver.addShortcuts(secondBatch);
+        catchAsyncExceptions(future);
+        assertShortcutsListEquals(allShortcuts, mShortcutInfoSaver.getShortcuts());
+
+        forceReloadFromDisk(future);
+        assertShortcutsListEquals(allShortcuts, mShortcutInfoSaver.getShortcuts());
+    }
+
+    @Test
+    public void testRemoveShortcuts() throws Exception {
+        catchAsyncExceptions(mShortcutInfoSaver.addShortcuts(mTestShortcuts));
+        ArrayList<String> removeIds = new ArrayList<>();
+        removeIds.add(mTestShortcuts.get(1).getId());
+        removeIds.add(mTestShortcuts.get(3).getId());
+
+        ListenableFuture<?> future = mShortcutInfoSaver.removeShortcuts(removeIds);
+        catchAsyncExceptions(future);
+
+        mTestShortcuts.remove(3);
+        mTestShortcuts.remove(1);
+        assertShortcutsListEquals(testShortcutsWithCategories(), mShortcutInfoSaver.getShortcuts());
+
+        forceReloadFromDisk(future);
+        assertShortcutsListEquals(testShortcutsWithCategories(), mShortcutInfoSaver.getShortcuts());
+    }
+
+    @Test
+    public void testRemoveAllShortcuts() throws Exception {
+        catchAsyncExceptions(mShortcutInfoSaver.addShortcuts(mTestShortcuts));
+        assertShortcutsListEquals(testShortcutsWithCategories(), mShortcutInfoSaver.getShortcuts());
+
+        ListenableFuture<?> future = mShortcutInfoSaver.removeAllShortcuts();
+        catchAsyncExceptions(future);
+        assertTrue(mShortcutInfoSaver.getShortcuts().isEmpty());
+
+        forceReloadFromDisk(future);
+        assertTrue(mShortcutInfoSaver.getShortcuts().isEmpty());
+    }
+
+    @Test
+    public void verifyIconsAreNotKeptInMemory() throws Exception {
+        ListenableFuture<?> future = mShortcutInfoSaver.addShortcuts(mTestShortcuts);
+        catchAsyncExceptions(future);
+        forceReloadFromDisk(future);
+
+        List<ShortcutInfoCompat> shortcuts = mShortcutInfoSaver.getShortcuts();
+        for (ShortcutInfoCompat item : shortcuts) {
+            assertNull(item.mIcon);
+        }
+    }
+
+    @Test
+    public void testGetShortcutIcon() throws Exception {
+        catchAsyncExceptions(mShortcutInfoSaver.addShortcuts(mTestShortcuts));
+
+        List<ShortcutInfoCompat> shortcuts = mShortcutInfoSaver.getShortcuts();
+        for (ShortcutInfoCompat item : shortcuts) {
+            verifyCorrectIconLoaded(item.getId(), mShortcutInfoSaver.getShortcutIcon(item.getId()));
+        }
+    }
+
+    @Test
+    public void testGetShortcutIcon_forceReload() throws Exception {
+        ListenableFuture<?> future = mShortcutInfoSaver.addShortcuts(mTestShortcuts);
+        catchAsyncExceptions(future);
+        forceReloadFromDisk(future);
+
+        List<ShortcutInfoCompat> shortcuts = mShortcutInfoSaver.getShortcuts();
+        for (ShortcutInfoCompat item : shortcuts) {
+            verifyCorrectIconLoaded(item.getId(), mShortcutInfoSaver.getShortcutIcon(item.getId()));
+        }
+    }
+
+    @Test
+    public void testGetShortcutIcon_unknownId() throws Exception {
+        catchAsyncExceptions(mShortcutInfoSaver.addShortcuts(mTestShortcuts));
+
+        assertNull(mShortcutInfoSaver.getShortcutIcon("unknown-id"));
+    }
+
+    private void verifyCorrectIconLoaded(String id, IconCompat icon) throws Exception {
+        switch (id) {
+            case ID_SHORTCUT_RESOURCE_ICON:
+                assertNotNull(icon);
+                assertEquals(Icon.TYPE_RESOURCE, icon.getType());
+                assertEquals(mTestResourceIcon.getResId(), icon.getResId());
+                break;
+            case ID_SHORTCUT_BITMAP_ICON:
+                assertNotNull(icon);
+                assertEquals(Icon.TYPE_BITMAP, icon.getType());
+                assertEquals(getCenterColor(mTestBitmapIcon), getCenterColor(icon));
+                break;
+            case ID_SHORTCUT_ADAPTIVE_BITMAP_ICON:
+                assertNotNull(icon);
+                // Adaptive icons are restored from disk as legacy (non-adaptive) icons. If icon is
+                // still waiting to be saved, the original Adaptive Icon will be returned.
+                assertTrue(icon.getType() == Icon.TYPE_BITMAP
+                        || icon.getType() == Icon.TYPE_ADAPTIVE_BITMAP);
+                assertEquals(getCenterColor(mTestAdaptiveBitmapIcon), getCenterColor(icon));
+                break;
+            case ID_SHORTCUT_NO_ICON:
+                assertNull(icon);
+                break;
+            default:
+                throw new Exception("Unknown shortcut Id: " + id);
+        }
+    }
+
+    private int getCenterColor(IconCompat icon) {
+        assertNotNull(icon);
+        Bitmap bitmap = icon.getBitmap();
+        assertNotNull(bitmap);
+        return bitmap.getPixel(bitmap.getWidth() / 2, bitmap.getHeight() / 2);
+    }
+
+    private void forceReloadFromDisk(ListenableFuture<?> lastFuture) throws Exception {
+        // Wait until the last async operation is finished.
+        lastFuture.get();
+
+        mCacheUpdateService = ShortcutInfoCompatSaver.createExecutorService();
+        mDiskIoService = ShortcutInfoCompatSaver.createExecutorService();
+        mShortcutInfoSaver = new ShortcutInfoCompatSaver(mContext, mCacheUpdateService,
+                mDiskIoService);
+    }
+
+    private void catchAsyncExceptions(final ListenableFuture<?> future) {
+        future.addListener(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    future.get();
+                } catch (Exception e) {
+                    throw new RuntimeException("Async operation failed", e);
+                }
+            }
+        }, new Executor() {
+            @Override
+            public void execute(Runnable command) {
+                // Run in the current thread
+                command.run();
+            }
+        });
+    }
+}
diff --git a/core/src/androidTest/java/androidx/core/content/pm/ShortcutInfoCompatTest.java b/core/src/androidTest/java/androidx/core/content/pm/ShortcutInfoCompatTest.java
index a603f80..2fa6579 100644
--- a/core/src/androidTest/java/androidx/core/content/pm/ShortcutInfoCompatTest.java
+++ b/core/src/androidTest/java/androidx/core/content/pm/ShortcutInfoCompatTest.java
@@ -133,6 +133,31 @@
     }
 
     @Test
+    public void testBuilder_copyConstructor() {
+        String longLabel = "Test long label";
+        ComponentName activity = new ComponentName("Package name", "Class name");
+        String disabledMessage = "Test disabled message";
+        Set<String> categories = new HashSet<>();
+        categories.add("cat1");
+        categories.add("cat2");
+        ShortcutInfoCompat compat = mBuilder
+                .setActivity(activity)
+                .setCategories(categories)
+                .setDisabledMessage(disabledMessage)
+                .setLongLabel(longLabel)
+                .build();
+
+        ShortcutInfoCompat copyCompat = new ShortcutInfoCompat.Builder(compat).build();
+        assertEquals(TEST_SHORTCUT_ID, copyCompat.getId());
+        assertEquals(TEST_SHORTCUT_SHORT_LABEL, copyCompat.getShortLabel());
+        assertEquals(mAction, copyCompat.getIntent());
+        assertEquals(longLabel, copyCompat.getLongLabel());
+        assertEquals(disabledMessage, copyCompat.getDisabledMessage());
+        assertEquals(activity, copyCompat.getActivity());
+        assertEquals(categories, copyCompat.getCategories());
+    }
+
+    @Test
     public void testBuilder_getters() {
         String longLabel = "Test long label";
         ComponentName activity = new ComponentName("Package name", "Class name");
diff --git a/core/src/androidTest/java/androidx/core/text/PrecomputedTextCompatTest.java b/core/src/androidTest/java/androidx/core/text/PrecomputedTextCompatTest.java
index 999f950..4e36e59 100644
--- a/core/src/androidTest/java/androidx/core/text/PrecomputedTextCompatTest.java
+++ b/core/src/androidTest/java/androidx/core/text/PrecomputedTextCompatTest.java
@@ -158,6 +158,57 @@
     }
 
     @Test
+    @SdkSuppress(minSdkVersion = 23)
+    public void testParams_equalsWithoutTextDirection() {
+        final Params base = new Params.Builder(PAINT)
+                .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY)
+                .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
+                .setTextDirection(LTR).build();
+
+        assertTrue(base.equalsWithoutTextDirection(base));
+
+        Params other = new Params.Builder(PAINT)
+                .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY)
+                .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
+                .setTextDirection(LTR).build();
+        assertTrue(base.equalsWithoutTextDirection(other));
+        assertTrue(other.equalsWithoutTextDirection(base));
+        assertEquals(base.hashCode(), other.hashCode());
+
+        other = new Params.Builder(PAINT)
+                .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE)
+                .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
+                .setTextDirection(LTR).build();
+        assertFalse(base.equalsWithoutTextDirection(other));
+        assertFalse(other.equalsWithoutTextDirection(base));
+
+        other = new Params.Builder(PAINT)
+                .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY)
+                .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE)
+                .setTextDirection(LTR).build();
+        assertFalse(base.equalsWithoutTextDirection(other));
+        assertFalse(other.equalsWithoutTextDirection(base));
+
+
+        other = new Params.Builder(PAINT)
+                .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY)
+                .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
+                .setTextDirection(RTL).build();
+        assertTrue(base.equalsWithoutTextDirection(other));
+        assertTrue(other.equalsWithoutTextDirection(base));
+
+
+        TextPaint anotherPaint = new TextPaint(PAINT);
+        anotherPaint.setTextSize(PAINT.getTextSize() * 2.0f);
+        other = new Params.Builder(anotherPaint)
+                .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY)
+                .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
+                .setTextDirection(LTR).build();
+        assertFalse(base.equalsWithoutTextDirection(other));
+        assertFalse(other.equalsWithoutTextDirection(base));
+    }
+
+    @Test
     public void testParams_equals2() {
         final Params base = new Params.Builder(PAINT).build();
 
@@ -178,6 +229,24 @@
     }
 
     @Test
+    public void testParams_equalsWithoutTextDirection2() {
+        final Params base = new Params.Builder(PAINT).build();
+
+        assertTrue(base.equalsWithoutTextDirection(base));
+
+        Params other = new Params.Builder(PAINT).build();
+        assertTrue(base.equalsWithoutTextDirection(other));
+        assertTrue(other.equalsWithoutTextDirection(base));
+        assertEquals(base.hashCode(), other.hashCode());
+
+        TextPaint paint = new TextPaint(PAINT);
+        paint.setTextSize(paint.getTextSize() * 2.0f + 1.0f);
+        other = new Params.Builder(paint).build();
+        assertFalse(base.equalsWithoutTextDirection(other));
+        assertFalse(other.equalsWithoutTextDirection(base));
+    }
+
+    @Test
     public void testCreate_withNull() {
         final Params param = new Params.Builder(PAINT).build();
         try {
diff --git a/core/src/androidTest/res/xml/shortcuts.xml b/core/src/androidTest/res/xml/shortcuts.xml
new file mode 100644
index 0000000..547da7e
--- /dev/null
+++ b/core/src/androidTest/res/xml/shortcuts.xml
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2018 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<!-- Test XML resource to read from, used in ShareTargetXmlParserTest.java -->
+<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
+    <!-- Legacy shortcut, ignored by ShareTargetXmlParser -->
+    <shortcut>
+            android:shortcutId="dummy_shortcut1"
+            android:enabled="true">
+        <intent
+                android:action="android.intent.action.VIEW"
+                android:targetPackage="com.test.googlex.somepackage"
+                android:targetClass="com.test.googlex.somepackage.someclass" />
+        <categories android:name="android.shortcut.conversation" />
+    </shortcut>
+
+    <!-- Valid share target definition example -->
+    <share-target android:targetClass="com.test.googlex.directshare.TestActivity1">
+        <data
+                android:scheme="http"
+                android:host="www.google.com"
+                android:port="1234"
+                android:path="somePath"
+                android:pathPrefix="somePathPrefix"
+                android:pathPattern="somePathPattern"
+                android:mimeType="text/plain"/>
+        <category android:name="com.test.googlex.category.CATEGORY1"/>
+        <category android:name="com.test.googlex.category.CATEGORY2"/>
+    </share-target>
+
+    <!-- Share target missing data tag, will be dropped -->
+    <share-target android:targetClass="com.test.googlex.directshare.TestActivity">
+        <category android:name="com.test.googlex.category.CATEGORY2"/>
+    </share-target>
+
+    <!-- Share target missing target class, will be dropped -->
+    <share-target>
+        <data
+                android:scheme="file"
+                android:host="www.somehost.com"
+                android:port="1234"
+                android:mimeType="video/*"/>
+        <category android:name="com.test.googlex.category.CATEGORY3"/>
+    </share-target>
+
+    <!-- Legacy shortcut, ignored by ShareTargetXmlParser -->
+    <shortcut>
+            android:shortcutId="dummy_shortcut2"
+            android:enabled="true">
+        <intent
+                android:action="android.intent.action.VIEW"
+                android:targetPackage="com.test.googlex.somepackage"
+                android:targetClass="com.test.googlex.somepackage.someclass" />
+        <categories android:name="android.shortcut.conversation" />
+    </shortcut>
+
+    <!-- Share target missing category, will be dropped -->
+    <share-target android:targetClass="com.test.googlex.directshare.TestActivity">
+        <data
+                android:scheme="content"
+                android:mimeType="text/plain"/>
+    </share-target>
+
+    <!-- Valid share target definition example -->
+    <share-target android:targetClass="com.test.googlex.directshare.TestActivity5">
+        <category android:name="com.test.googlex.category.CATEGORY5"/>
+        <category android:name="com.test.googlex.category.CATEGORY6"/>
+        <data android:mimeType="video/mp4"/>
+        <data
+                android:scheme="content"
+                android:mimeType="video/*"/>
+    </share-target>
+</shortcuts>
+
diff --git a/core/src/main/java/androidx/core/content/pm/ShareTargetCompat.java b/core/src/main/java/androidx/core/content/pm/ShareTargetCompat.java
new file mode 100644
index 0000000..019878e
--- /dev/null
+++ b/core/src/main/java/androidx/core/content/pm/ShareTargetCompat.java
@@ -0,0 +1,60 @@
+/*
+ * 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.core.content.pm;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Represents a Share Target definition read from the app's manifest.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+class ShareTargetCompat {
+    static class TargetData {
+        final String mScheme;
+        final String mHost;
+        final String mPort;
+        final String mPath;
+        final String mPathPattern;
+        final String mPathPrefix;
+        final String mMimeType;
+
+        TargetData(String scheme, String host, String port, String path, String pathPattern,
+                String pathPrefix, String mimeType) {
+            mScheme = scheme;
+            mHost = host;
+            mPort = port;
+            mPath = path;
+            mPathPattern = pathPattern;
+            mPathPrefix = pathPrefix;
+            mMimeType = mimeType;
+        }
+    }
+
+    final TargetData[] mTargetData;
+    final String mTargetClass;
+    final String[] mCategories;
+
+    ShareTargetCompat(TargetData[] data, String targetClass, String[] categories) {
+        mTargetData = data;
+        mTargetClass = targetClass;
+        mCategories = categories;
+    }
+}
diff --git a/core/src/main/java/androidx/core/content/pm/ShareTargetXmlParser.java b/core/src/main/java/androidx/core/content/pm/ShareTargetXmlParser.java
new file mode 100644
index 0000000..a3a7f9e
--- /dev/null
+++ b/core/src/main/java/androidx/core/content/pm/ShareTargetXmlParser.java
@@ -0,0 +1,150 @@
+/*
+ * 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.core.content.pm;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
+import android.util.Log;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.WorkerThread;
+
+import org.xmlpull.v1.XmlPullParser;
+
+import java.util.ArrayList;
+
+/**
+ * Utility class to parse the list of {@link ShareTargetCompat} from app's Xml resource.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+class ShareTargetXmlParser {
+
+    static final String TAG = "ShareTargetXmlParser";
+
+    private static final String TAG_SHARE_TARGET = "share-target";
+    private static final String ATTR_TARGET_CLASS = "targetClass";
+
+    private static final String TAG_DATA = "data";
+    private static final String ATTR_SCHEME = "scheme";
+    private static final String ATTR_HOST = "host";
+    private static final String ATTR_PORT = "port";
+    private static final String ATTR_PATH = "path";
+    private static final String ATTR_PATH_PATTERN = "pathPattern";
+    private static final String ATTR_PATH_PREFIX = "pathPrefix";
+    private static final String ATTR_MIME_TYPE = "mimeType";
+
+    private static final String TAG_CATEGORY = "category";
+    private static final String ATTR_NAME = "name";
+
+    // List of share targets loaded from app's manifest. Will not change while the app is running.
+    private static ArrayList<ShareTargetCompat> sShareTargets;
+
+    @WorkerThread
+    static ArrayList<ShareTargetCompat> getShareTargets(Context context) {
+        if (sShareTargets == null) {
+            sShareTargets = parseShareTargets(context);
+        }
+        return sShareTargets;
+    }
+
+    private ShareTargetXmlParser() {
+        /* Hide the constructor */
+    }
+
+    private static ArrayList<ShareTargetCompat> parseShareTargets(Context context) {
+        ArrayList<ShareTargetCompat> targets = new ArrayList<>();
+        XmlResourceParser parser = getXmlResourceParser(context);
+
+        try {
+            int type;
+            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
+                if (type == XmlPullParser.START_TAG && parser.getName().equals(TAG_SHARE_TARGET)) {
+                    ShareTargetCompat target = parseShareTarget(parser);
+                    if (target != null) {
+                        targets.add(target);
+                    }
+                }
+            }
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to parse the Xml resource: ", e);
+        }
+
+        parser.close();
+        return targets;
+    }
+
+    private static XmlResourceParser getXmlResourceParser(Context context) {
+        // TODO: Parse the main manifest to find the right Xml resource for share targets
+        Resources res = context.getResources();
+        return res.getXml(res.getIdentifier("shortcuts", "xml", context.getPackageName()));
+    }
+
+    private static ShareTargetCompat parseShareTarget(XmlResourceParser parser) throws Exception {
+        String targetClass = getAttributeValue(parser, ATTR_TARGET_CLASS);
+        ArrayList<ShareTargetCompat.TargetData> targetData = new ArrayList<>();
+        ArrayList<String> categories = new ArrayList<>();
+
+        int type;
+        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
+            if (type == XmlPullParser.START_TAG) {
+                switch (parser.getName()) {
+                    case TAG_DATA:
+                        targetData.add(parseTargetData(parser));
+                        break;
+                    case TAG_CATEGORY:
+                        categories.add(getAttributeValue(parser, ATTR_NAME));
+                        break;
+                }
+            } else if (type == XmlPullParser.END_TAG && parser.getName().equals(TAG_SHARE_TARGET)) {
+                break;
+            }
+        }
+        if (targetData.isEmpty() || targetClass == null || categories.isEmpty()) {
+            return null;
+        }
+        return new ShareTargetCompat(
+                targetData.toArray(new ShareTargetCompat.TargetData[targetData.size()]),
+                targetClass, categories.toArray(new String[categories.size()]));
+    }
+
+    private static ShareTargetCompat.TargetData parseTargetData(XmlResourceParser parser) {
+        String scheme = getAttributeValue(parser, ATTR_SCHEME);
+        String host = getAttributeValue(parser, ATTR_HOST);
+        String port = getAttributeValue(parser, ATTR_PORT);
+        String path = getAttributeValue(parser, ATTR_PATH);
+        String pathPattern = getAttributeValue(parser, ATTR_PATH_PATTERN);
+        String pathPrefix = getAttributeValue(parser, ATTR_PATH_PREFIX);
+        String mimeType = getAttributeValue(parser, ATTR_MIME_TYPE);
+
+        return new ShareTargetCompat.TargetData(scheme, host, port, path, pathPattern, pathPrefix,
+                mimeType);
+    }
+
+    private static String getAttributeValue(XmlResourceParser parser, String attribute) {
+        String value = parser.getAttributeValue("http://schemas.android.com/apk/res/android",
+                attribute);
+        if (value == null) {
+            value = parser.getAttributeValue(null, attribute);
+        }
+        return value;
+    }
+}
diff --git a/core/src/main/java/androidx/core/content/pm/ShortcutInfoCompat.java b/core/src/main/java/androidx/core/content/pm/ShortcutInfoCompat.java
index bcea227..fdb3948 100644
--- a/core/src/main/java/androidx/core/content/pm/ShortcutInfoCompat.java
+++ b/core/src/main/java/androidx/core/content/pm/ShortcutInfoCompat.java
@@ -35,6 +35,7 @@
 import androidx.core.graphics.drawable.IconCompat;
 
 import java.util.Arrays;
+import java.util.HashSet;
 import java.util.Set;
 
 /**
@@ -272,6 +273,30 @@
         }
 
         /**
+         * @hide
+         */
+        @RestrictTo(LIBRARY_GROUP)
+        public Builder(@NonNull ShortcutInfoCompat shortcutInfo) {
+            mInfo = new ShortcutInfoCompat();
+            mInfo.mContext = shortcutInfo.mContext;
+            mInfo.mId = shortcutInfo.mId;
+            mInfo.mIntents = Arrays.copyOf(shortcutInfo.mIntents, shortcutInfo.mIntents.length);
+            mInfo.mActivity = shortcutInfo.mActivity;
+            mInfo.mLabel = shortcutInfo.mLabel;
+            mInfo.mLongLabel = shortcutInfo.mLongLabel;
+            mInfo.mDisabledMessage = shortcutInfo.mDisabledMessage;
+            mInfo.mIcon = shortcutInfo.mIcon;
+            mInfo.mIsAlwaysBadged = shortcutInfo.mIsAlwaysBadged;
+            mInfo.mIsLongLived = shortcutInfo.mIsLongLived;
+            if (shortcutInfo.mPersons != null) {
+                mInfo.mPersons = Arrays.copyOf(shortcutInfo.mPersons, shortcutInfo.mPersons.length);
+            }
+            if (shortcutInfo.mCategories != null) {
+                mInfo.mCategories = new HashSet<>(shortcutInfo.mCategories);
+            }
+        }
+
+        /**
          * Sets the short title of a shortcut.
          *
          * <p>This is a mandatory field when publishing a new shortcut.
diff --git a/core/src/main/java/androidx/core/content/pm/ShortcutInfoCompatSaver.java b/core/src/main/java/androidx/core/content/pm/ShortcutInfoCompatSaver.java
new file mode 100644
index 0000000..d8eee12
--- /dev/null
+++ b/core/src/main/java/androidx/core/content/pm/ShortcutInfoCompatSaver.java
@@ -0,0 +1,417 @@
+/*
+ * 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.core.content.pm;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.Icon;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.WorkerThread;
+import androidx.collection.ArrayMap;
+import androidx.concurrent.futures.ResolvableFuture;
+import androidx.core.content.pm.ShortcutsInfoSerialization.ShortcutContainer;
+import androidx.core.graphics.drawable.IconCompat;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Provides APIs to access and update a persistable list of {@link ShortcutInfoCompat}. This class
+ * keeps an up-to-date cache of the complete list in memory for quick access, except shortcuts'
+ * Icons, which are stored on the disk and only loaded from disk separately if necessary.
+ */
+@RequiresApi(19)
+//TODO: we need Futures.addCallback and CallbackToFutureAdapter, update once they're available
+class ShortcutInfoCompatSaver {
+
+    static final String TAG = "ShortcutInfoCompatSaver";
+
+    private static final String DIRECTORY_TARGETS = "ShortcutInfoCompatSaver_share_targets";
+    private static final String DIRECTORY_BITMAPS = "ShortcutInfoCompatSaver_share_targets_bitmaps";
+    private static final String FILENAME_XML = "targets.xml";
+    // The maximum time background idle threads will wait for new tasks before terminating
+    private static final int EXECUTOR_KEEP_ALIVE_TIME_SECS = 20;
+    private static final Object GET_INSTANCE_LOCK = new Object();
+
+    private static volatile ShortcutInfoCompatSaver sINSTANCE;
+
+    @SuppressWarnings("WeakerAccess")
+    final Context mContext;
+    // mShortcutsMap is strictly only accessed by mCacheUpdateService
+    @SuppressWarnings("WeakerAccess")
+    final Map<String, ShortcutContainer> mShortcutsMap = new ArrayMap<>();
+    @SuppressWarnings("WeakerAccess")
+    final Map<String, ListenableFuture<?>> mScheduledBitmapTasks = new ArrayMap<>();
+
+    @SuppressWarnings("WeakerAccess")
+    final ExecutorService mCacheUpdateService;
+    // Single threaded tasks queue for IO operations on disk
+    private final ExecutorService mDiskIoService;
+
+    @SuppressWarnings("WeakerAccess")
+    final File mTargetsXmlFile;
+    @SuppressWarnings("WeakerAccess")
+    final File mBitmapsDir;
+
+    @AnyThread
+    static ShortcutInfoCompatSaver getInstance(Context context) {
+        if (sINSTANCE == null) {
+            synchronized (GET_INSTANCE_LOCK) {
+                if (sINSTANCE == null) {
+                    sINSTANCE = new ShortcutInfoCompatSaver(context,
+                            createExecutorService(),
+                            createExecutorService());
+                }
+            }
+        }
+        return sINSTANCE;
+    }
+
+    @AnyThread
+    static ExecutorService createExecutorService() {
+        return new ThreadPoolExecutor(
+                // Set to 0 to avoid persistent background thread when idle
+                0, /* core pool size */
+                // Set to 1 to ensure tasks will run strictly in the submit order
+                1, /* max pool size */
+                EXECUTOR_KEEP_ALIVE_TIME_SECS, /* keep alive time */
+                TimeUnit.SECONDS, /* keep alive time unit */
+                new LinkedBlockingQueue<Runnable>() /* Not used */);
+    }
+
+    @AnyThread
+    ShortcutInfoCompatSaver(Context context, ExecutorService cacheUpdateService,
+            ExecutorService diskIoService) {
+        mContext = context.getApplicationContext();
+        mCacheUpdateService = cacheUpdateService;
+        mDiskIoService = diskIoService;
+        final File workingDirectory = new File(context.getFilesDir(), DIRECTORY_TARGETS);
+        mBitmapsDir = new File(workingDirectory, DIRECTORY_BITMAPS);
+        mTargetsXmlFile = new File(workingDirectory, FILENAME_XML);
+        // we trying to recover from errors during following submit:
+        // if xml was corrupted it is removed and saver is started clean
+        // however it is still not great and there is chance to swallow an exception
+        mCacheUpdateService.submit(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    ensureDir(workingDirectory);
+                    ensureDir(mBitmapsDir);
+                    mShortcutsMap.putAll(ShortcutsInfoSerialization.loadFromXml(mTargetsXmlFile,
+                            mContext));
+                    deleteDanglingBitmaps(new ArrayList<>(mShortcutsMap.values()));
+                } catch (Exception e) {
+                    Log.w(TAG, "ShortcutInfoCompatSaver started with an exceptions ", e);
+                }
+            }
+        });
+    }
+
+    @AnyThread
+    ListenableFuture<Void> removeShortcuts(List<String> shortcutIds) {
+        final List<String> idList = new ArrayList<>(shortcutIds);
+        final ResolvableFuture<Void> result = ResolvableFuture.create();
+        mCacheUpdateService.submit(new Runnable() {
+            @Override
+            public void run() {
+                for (String id : idList) {
+                    mShortcutsMap.remove(id);
+                    ListenableFuture<?> removed = mScheduledBitmapTasks.remove(id);
+                    if (removed != null) {
+                        removed.cancel(false);
+                    }
+                }
+                scheduleSyncCurrentState(result);
+            }
+        });
+        return result;
+    }
+
+    @AnyThread
+    ListenableFuture<Void> removeAllShortcuts() {
+        final ResolvableFuture<Void> result =
+                ResolvableFuture.create();
+        mCacheUpdateService.submit(new Runnable() {
+            @Override
+            public void run() {
+                mShortcutsMap.clear();
+                for (ListenableFuture<?> task : mScheduledBitmapTasks.values()) {
+                    task.cancel(false);
+                }
+                mScheduledBitmapTasks.clear();
+                scheduleSyncCurrentState(result);
+            }
+        });
+
+        return result;
+    }
+
+    @WorkerThread
+    List<ShortcutInfoCompat> getShortcuts() throws Exception {
+        return mCacheUpdateService.submit(new Callable<ArrayList<ShortcutInfoCompat>>() {
+            @Override
+            public ArrayList<ShortcutInfoCompat> call() {
+                ArrayList<ShortcutInfoCompat> shortcuts = new ArrayList<>();
+                for (ShortcutContainer item : mShortcutsMap.values()) {
+                    shortcuts.add(new ShortcutInfoCompat.Builder(item.mShortcutInfo).build());
+                }
+                return shortcuts;
+            }
+        }).get();
+    }
+
+    @WorkerThread
+    IconCompat getShortcutIcon(final String shortcutId) throws Exception {
+        final ShortcutContainer container = mCacheUpdateService.submit(
+                new Callable<ShortcutContainer>() {
+                    @Override
+                    public ShortcutContainer call() {
+                        return mShortcutsMap.get(shortcutId);
+                    }
+                }).get();
+        if (container == null) {
+            return null;
+        }
+        if (!TextUtils.isEmpty(container.mResourceName)) {
+            int id = 0;
+            try {
+                id = mContext.getResources().getIdentifier(container.mResourceName, null, null);
+            } catch (Exception e) {
+                /* Do nothing, continue and try mBitmapPath */
+            }
+            if (id != 0) {
+                return IconCompat.createWithResource(mContext, id);
+            }
+        }
+        if (!TextUtils.isEmpty(container.mBitmapPath)) {
+            Bitmap bitmap = mDiskIoService.submit(new Callable<Bitmap>() {
+                @Override
+                public Bitmap call() {
+                    return BitmapFactory.decodeFile(container.mBitmapPath);
+                }
+            }).get();
+            // TODO: Re-create an adaptive icon if the original icon was adaptive
+            return bitmap != null ? IconCompat.createWithBitmap(bitmap) : null;
+        }
+        return null;
+    }
+
+    /**
+     * Delete bitmap files from the disk if they are not associated with any shortcuts in the list.
+     *
+     * Strictly called by mDiskIoService only
+     */
+    @SuppressWarnings("WeakerAccess")
+    void deleteDanglingBitmaps(List<ShortcutContainer> shortcutsList) {
+        List<String> bitmapPaths = new ArrayList<>();
+        for (ShortcutContainer item : shortcutsList) {
+            if (!TextUtils.isEmpty(item.mBitmapPath)) {
+                bitmapPaths.add(item.mBitmapPath);
+            }
+        }
+        for (File bitmap : mBitmapsDir.listFiles()) {
+            if (!bitmapPaths.contains(bitmap.getAbsolutePath())) {
+                bitmap.delete();
+            }
+        }
+    }
+
+    ListenableFuture<Void> addShortcuts(List<ShortcutInfoCompat> shortcuts) {
+        final List<ShortcutInfoCompat> copy = new ArrayList<>(shortcuts.size());
+        for (ShortcutInfoCompat infoCompat : shortcuts) {
+            copy.add(new ShortcutInfoCompat.Builder(infoCompat).build());
+        }
+        final ResolvableFuture<Void> result = ResolvableFuture.create();
+        mCacheUpdateService.submit(new Runnable() {
+            @Override
+            public void run() {
+                for (final ShortcutInfoCompat info : copy) {
+                    Set<String> categories = info.getCategories();
+                    if (categories == null || categories.isEmpty()) {
+                        continue;
+                    }
+                    ShortcutContainer container = containerFrom(info);
+                    IconCompat icon = info.mIcon;
+                    // not null only if it is safe to call getBitmap
+                    Bitmap bitmap = container.mBitmapPath != null ? icon.getBitmap() : null;
+                    final String id = info.getId();
+                    mShortcutsMap.put(id, container);
+                    if (bitmap != null) {
+                        final ListenableFuture<Void> future = scheduleBitmapSaving(bitmap,
+                                container.mBitmapPath);
+                        ListenableFuture<?> old = mScheduledBitmapTasks.put(id, future);
+                        if (old != null) {
+                            old.cancel(false);
+                        }
+                        future.addListener(new Runnable() {
+                            @Override
+                            public void run() {
+                                mScheduledBitmapTasks.remove(id);
+                                // saving bitmap was skipped, but it is okay
+                                if (future.isCancelled()) {
+                                    return;
+                                }
+                                try {
+                                    future.get();
+                                } catch (Exception e) {
+                                    // propagate an exception up to the chain.
+                                    result.setException(e);
+                                }
+                            }
+                        }, mCacheUpdateService);
+                    }
+                }
+                scheduleSyncCurrentState(result);
+            }
+        });
+        return result;
+    }
+
+    @SuppressWarnings("WeakerAccess")
+    ListenableFuture<Void> scheduleBitmapSaving(final Bitmap bitmap, final String path) {
+        return submitDiskOperation(new Runnable() {
+            @Override
+            public void run() {
+                saveBitmap(bitmap, path);
+            }
+        });
+    }
+
+    private ListenableFuture<Void> submitDiskOperation(final Runnable runnable) {
+        final ResolvableFuture<Void> result = ResolvableFuture.create();
+        mDiskIoService.submit(new Runnable() {
+            @Override
+            public void run() {
+                if (result.isCancelled()) {
+                    return;
+                }
+                try {
+                    runnable.run();
+                    result.set(null);
+                } catch (Exception e) {
+                    result.setException(e);
+                }
+            }
+        });
+        return result;
+    }
+
+    // must be called on mCacheUpdateService
+    @SuppressWarnings("WeakerAccess")
+    void scheduleSyncCurrentState(final ResolvableFuture<Void> output) {
+        final List<ShortcutContainer> containers = new ArrayList<>(mShortcutsMap.values());
+        final ListenableFuture<Void> future = submitDiskOperation(new Runnable() {
+            @Override
+            public void run() {
+                deleteDanglingBitmaps(containers);
+                ShortcutsInfoSerialization.saveAsXml(containers, mTargetsXmlFile);
+            }
+        });
+        future.addListener(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    future.get();
+                    output.set(null);
+                } catch (Exception e) {
+                    output.setException(e);
+                }
+            }
+        }, mCacheUpdateService);
+    }
+
+    @SuppressWarnings("WeakerAccess")
+    ShortcutContainer containerFrom(ShortcutInfoCompat shortcut) {
+        String resourceName = null;
+        String bitmapPath = null;
+        if (shortcut.mIcon != null) {
+            IconCompat icon = shortcut.mIcon;
+            switch (icon.getType()) {
+                case Icon.TYPE_RESOURCE:
+                    resourceName = mContext.getResources().getResourceName(icon.getResId());
+                    break;
+                case Icon.TYPE_BITMAP:
+                case Icon.TYPE_ADAPTIVE_BITMAP:
+                    // Choose a unique file name to serialize the bitmap
+                    bitmapPath = new File(mBitmapsDir, UUID.randomUUID().toString())
+                            .getAbsolutePath();
+                    break;
+                case Icon.TYPE_DATA:
+                case Icon.TYPE_URI:
+                case IconCompat.TYPE_UNKNOWN:
+                    break;
+            }
+        }
+        ShortcutInfoCompat shortcutCopy = new ShortcutInfoCompat.Builder(shortcut)
+                .setIcon(null).build();
+        return new ShortcutContainer(shortcutCopy, resourceName, bitmapPath);
+    }
+
+    /*
+     * Suppress wrong thread warning since Bitmap.compress() and saveBitmap() are both annotated
+     * @WorkerThread, but from different packages.
+     * androidx.annotation.WorkerThread vs android.annotation.WorkerThread
+     */
+    @WorkerThread
+    @SuppressWarnings("WrongThread")
+    void saveBitmap(Bitmap bitmap, String path) {
+        if (bitmap == null) {
+            throw new IllegalArgumentException("bitmap is null");
+        }
+        if (TextUtils.isEmpty(path)) {
+            throw new IllegalArgumentException("path is empty");
+        }
+
+        try (FileOutputStream fileStream = new FileOutputStream(new File(path))) {
+            if (!bitmap.compress(Bitmap.CompressFormat.PNG, 100 /* quality */, fileStream)) {
+                Log.wtf(TAG, "Unable to compress bitmap");
+                throw new RuntimeException("Unable to compress bitmap for saving " + path);
+            }
+        } catch (IOException | RuntimeException | OutOfMemoryError e) {
+            Log.wtf(TAG, "Unable to write bitmap to file", e);
+            throw new RuntimeException("Unable to write bitmap to file " + path, e);
+        }
+    }
+
+    static boolean ensureDir(File directory) {
+        if (directory.exists() && !directory.isDirectory() && !directory.delete()) {
+            return false;
+        }
+        if (!directory.exists()) {
+            return directory.mkdirs();
+        }
+        return true;
+    }
+}
diff --git a/core/src/main/java/androidx/core/content/pm/ShortcutsInfoSerialization.java b/core/src/main/java/androidx/core/content/pm/ShortcutsInfoSerialization.java
new file mode 100644
index 0000000..5be685a
--- /dev/null
+++ b/core/src/main/java/androidx/core/content/pm/ShortcutsInfoSerialization.java
@@ -0,0 +1,312 @@
+/*
+ * 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.core.content.pm;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Xml;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.WorkerThread;
+import androidx.collection.ArrayMap;
+import androidx.core.util.AtomicFile;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+class ShortcutsInfoSerialization {
+    private static final String TAG = "ShortcutInfoCompatSaver";
+
+    private static final String TAG_ROOT = "share_targets";
+    private static final String TAG_TARGET = "target";
+    private static final String TAG_INTENT = "intent";
+    private static final String TAG_CATEGORY = "categories";
+
+    private static final String ATTR_ID = "id";
+    private static final String ATTR_COMPONENT = "component";
+    private static final String ATTR_SHORT_LABEL = "short_label";
+    private static final String ATTR_LONG_LABEL = "long_label";
+    private static final String ATTR_DISABLED_MSG = "disabled_message";
+
+    private static final String ATTR_ICON_RES_NAME = "icon_resource_name";
+    private static final String ATTR_ICON_BMP_PATH = "icon_bitmap_path";
+
+    private static final String ATTR_ACTION = "action";
+    private static final String ATTR_TARGET_PACKAGE = "targetPackage";
+    private static final String ATTR_TARGET_CLASS = "targetClass";
+    private static final String ATTR_NAME = "name";
+
+    private ShortcutsInfoSerialization() {
+    }
+
+    /**
+     * Class to keep {@link ShortcutInfoCompat}s with extra info (resource name or file path) about
+     * their serialized Icon for lazy loading.
+     */
+    static class ShortcutContainer {
+        final String mResourceName;
+        final String mBitmapPath;
+        final ShortcutInfoCompat mShortcutInfo;
+
+        @AnyThread
+        ShortcutContainer(ShortcutInfoCompat shortcut, String resourceName, String bitmapPath) {
+            mShortcutInfo = shortcut;
+            mResourceName = resourceName;
+            mBitmapPath = bitmapPath;
+        }
+
+    }
+
+    static void saveAsXml(List<ShortcutContainer> shortcutsList, File output) {
+        final AtomicFile atomicFile = new AtomicFile(output);
+        FileOutputStream fileStream = null;
+        try {
+            fileStream = atomicFile.startWrite();
+            final BufferedOutputStream stream = new BufferedOutputStream(fileStream);
+
+            XmlSerializer serializer = Xml.newSerializer();
+            serializer.setOutput(stream, "UTF_8");
+            serializer.startDocument(null, true);
+            serializer.startTag(null, TAG_ROOT);
+
+            for (ShortcutContainer shortcut : shortcutsList) {
+                serializeShortcutContainer(serializer, shortcut);
+            }
+
+            serializer.endTag(null, TAG_ROOT);
+            serializer.endDocument();
+
+            stream.flush();
+            fileStream.flush();
+            atomicFile.finishWrite(fileStream);
+
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to write to file " + atomicFile.getBaseFile(), e);
+            atomicFile.failWrite(fileStream);
+            throw new RuntimeException("Failed to write to file " + atomicFile.getBaseFile(), e);
+        }
+    }
+
+    @WorkerThread
+    static Map<String, ShortcutContainer> loadFromXml(File input, Context context) {
+        Map<String, ShortcutContainer> shortcutsList = new ArrayMap<>();
+        try {
+            if (!input.exists()) {
+                return shortcutsList;
+            }
+            final FileInputStream stream = new FileInputStream(input);
+            final XmlPullParser parser = Xml.newPullParser();
+            parser.setInput(stream, "UTF_8");
+            int type;
+            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
+                if (type == XmlPullParser.START_TAG && parser.getName().equals(TAG_TARGET)) {
+                    ShortcutContainer shortcut = parseShortcutContainer(parser, context);
+                    if (shortcut != null && shortcut.mShortcutInfo != null) {
+                        shortcutsList.put(shortcut.mShortcutInfo.getId(), shortcut);
+                    }
+                }
+            }
+        } catch (Exception e) {
+            // trying to recover by deleting input file.
+            input.delete();
+            Log.e(TAG, "Failed to load saved values from file " + input.getAbsolutePath()
+                    + ". Old state removed, new added", e);
+        }
+        return shortcutsList;
+    }
+
+    @WorkerThread
+    private static ShortcutContainer parseShortcutContainer(XmlPullParser parser,
+            Context context) throws Exception {
+        if (!parser.getName().equals(TAG_TARGET)) {
+            return null;
+        }
+
+        String id = getAttributeValue(parser, ATTR_ID);
+        CharSequence label = getAttributeValue(parser, ATTR_SHORT_LABEL);
+        if (TextUtils.isEmpty(id) || TextUtils.isEmpty(label)) {
+            return null;
+        }
+
+        CharSequence longLabel = getAttributeValue(parser, ATTR_LONG_LABEL);
+        CharSequence disabledMessage = getAttributeValue(parser, ATTR_DISABLED_MSG);
+        ComponentName activity = parseComponentName(parser);
+        String iconResourceName = getAttributeValue(parser, ATTR_ICON_RES_NAME);
+        String iconBitmapPath = getAttributeValue(parser, ATTR_ICON_BMP_PATH);
+
+        ArrayList<Intent> intents = new ArrayList<>();
+        Set<String> categories = new HashSet<>();
+        int type;
+        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
+            if (type == XmlPullParser.START_TAG) {
+                switch (parser.getName()) {
+                    case TAG_INTENT:
+                        Intent intent = parseIntent(parser);
+                        if (intent != null) {
+                            intents.add(intent);
+                        }
+                        break;
+                    case TAG_CATEGORY:
+                        String category = getAttributeValue(parser, ATTR_NAME);
+                        if (!TextUtils.isEmpty(category)) {
+                            categories.add(category);
+                        }
+                        break;
+                }
+            } else if (type == XmlPullParser.END_TAG && parser.getName().equals(TAG_TARGET)) {
+                break;
+            }
+        }
+
+        ShortcutInfoCompat.Builder builder = new ShortcutInfoCompat.Builder(context, id)
+                .setShortLabel(label);
+        if (!TextUtils.isEmpty(longLabel)) {
+            builder.setLongLabel(longLabel);
+        }
+        if (!TextUtils.isEmpty(disabledMessage)) {
+            builder.setDisabledMessage(disabledMessage);
+        }
+        if (activity != null) {
+            builder.setActivity(activity);
+        }
+        if (!intents.isEmpty()) {
+            builder.setIntents(intents.toArray(new Intent[0]));
+        }
+        if (!categories.isEmpty()) {
+            builder.setCategories(categories);
+        }
+        return new ShortcutContainer(builder.build(), iconResourceName, iconBitmapPath);
+    }
+
+    @WorkerThread
+    private static ComponentName parseComponentName(XmlPullParser parser) {
+        String value = getAttributeValue(parser, ATTR_COMPONENT);
+        return TextUtils.isEmpty(value) ? null : ComponentName.unflattenFromString(value);
+    }
+
+    @WorkerThread
+    private static Intent parseIntent(XmlPullParser parser) {
+        String action = getAttributeValue(parser, ATTR_ACTION);
+        String targetPackage = getAttributeValue(parser, ATTR_TARGET_PACKAGE);
+        String targetClass = getAttributeValue(parser, ATTR_TARGET_CLASS);
+
+        if (action == null) {
+            return null;
+        }
+        Intent intent = new Intent(action);
+        if (!TextUtils.isEmpty(targetPackage) && !TextUtils.isEmpty(targetClass)) {
+            intent.setClassName(targetPackage, targetClass);
+        }
+        return intent;
+    }
+
+    @WorkerThread
+    private static String getAttributeValue(XmlPullParser parser, String attribute) {
+        String value = parser.getAttributeValue("http://schemas.android.com/apk/res/android",
+                attribute);
+        if (value == null) {
+            value = parser.getAttributeValue(null, attribute);
+        }
+        return value;
+    }
+
+    @WorkerThread
+    private static void serializeShortcutContainer(XmlSerializer serializer,
+            ShortcutContainer container)
+            throws IOException {
+        serializer.startTag(null, TAG_TARGET);
+
+        ShortcutInfoCompat shortcut = container.mShortcutInfo;
+        serializeAttribute(serializer, ATTR_ID, shortcut.getId());
+        serializeAttribute(serializer, ATTR_SHORT_LABEL, shortcut.getShortLabel().toString());
+        if (!TextUtils.isEmpty(shortcut.getLongLabel())) {
+            serializeAttribute(serializer, ATTR_LONG_LABEL, shortcut.getLongLabel().toString());
+        }
+        if (!TextUtils.isEmpty(shortcut.getDisabledMessage())) {
+            serializeAttribute(serializer, ATTR_DISABLED_MSG,
+                    shortcut.getDisabledMessage().toString());
+        }
+        if (shortcut.getActivity() != null) {
+            serializeAttribute(serializer, ATTR_COMPONENT,
+                    shortcut.getActivity().flattenToString());
+        }
+        if (!TextUtils.isEmpty(container.mResourceName)) {
+            serializeAttribute(serializer, ATTR_ICON_RES_NAME, container.mResourceName);
+        }
+        if (!TextUtils.isEmpty(container.mBitmapPath)) {
+            serializeAttribute(serializer, ATTR_ICON_BMP_PATH, container.mBitmapPath);
+        }
+        for (Intent intent : shortcut.getIntents()) {
+            serializeIntent(serializer, intent);
+        }
+        for (String category : shortcut.mCategories) {
+            serializeCategory(serializer, category);
+        }
+
+        serializer.endTag(null, TAG_TARGET);
+    }
+
+    @WorkerThread
+    private static void serializeIntent(XmlSerializer serializer, Intent intent)
+            throws IOException {
+        serializer.startTag(null, TAG_INTENT);
+
+        serializeAttribute(serializer, ATTR_ACTION, intent.getAction());
+        if (intent.getComponent() != null) {
+            serializeAttribute(serializer, ATTR_TARGET_PACKAGE,
+                    intent.getComponent().getPackageName());
+            serializeAttribute(serializer, ATTR_TARGET_CLASS, intent.getComponent().getClassName());
+        }
+
+        serializer.endTag(null, TAG_INTENT);
+    }
+
+    @WorkerThread
+    private static void serializeCategory(XmlSerializer serializer, String category)
+            throws IOException {
+        if (TextUtils.isEmpty(category)) {
+            return;
+        }
+        serializer.startTag(null, TAG_CATEGORY);
+        serializeAttribute(serializer, ATTR_NAME, category);
+        serializer.endTag(null, TAG_CATEGORY);
+    }
+
+    @WorkerThread
+    private static void serializeAttribute(XmlSerializer serializer, String attribute, String value)
+            throws IOException {
+        if (TextUtils.isEmpty(value)) {
+            return;
+        }
+        serializer.attribute(null, attribute, value);
+    }
+
+}
diff --git a/core/src/main/java/androidx/core/text/PrecomputedTextCompat.java b/core/src/main/java/androidx/core/text/PrecomputedTextCompat.java
index 6e079ddb..9ac55aa7 100644
--- a/core/src/main/java/androidx/core/text/PrecomputedTextCompat.java
+++ b/core/src/main/java/androidx/core/text/PrecomputedTextCompat.java
@@ -37,6 +37,7 @@
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.UiThread;
+import androidx.core.os.BuildCompat;
 import androidx.core.os.TraceCompat;
 import androidx.core.util.ObjectsCompat;
 import androidx.core.util.Preconditions;
@@ -58,7 +59,7 @@
  * layout information will be included in this instance, {@link android.widget.TextView} or
  * {@link StaticLayout} will not have to recalculate this information.
  *
- * On API 28 or later, there is full PrecomputedText support by framework. From API 21 to API 27,
+ * On API 29 or later, there is full PrecomputedText support by framework. From API 21 to API 27,
  * PrecomputedTextCompat relies on internal text layout cache. PrecomputedTextCompat immediately
  * computes the text layout in the constuctor to warm up the internal text layout cache. On API 20
  * or before, PrecomputedTextCompat does nothing.
@@ -193,7 +194,7 @@
 
         Params(@NonNull TextPaint paint, @NonNull TextDirectionHeuristic textDir,
                 int strategy, int frequency) {
-            if (Build.VERSION.SDK_INT >= 28) {
+            if (BuildCompat.isAtLeastQ()) {
                 mWrapped = new PrecomputedText.Params.Builder(paint).setBreakStrategy(strategy)
                         .setHyphenationFrequency(frequency).setTextDirection(textDir).build();
             } else {
@@ -211,8 +212,7 @@
             mTextDir = wrapped.getTextDirection();
             mBreakStrategy = wrapped.getBreakStrategy();
             mHyphenationFrequency = wrapped.getHyphenationFrequency();
-            mWrapped = wrapped;
-
+            mWrapped = (BuildCompat.isAtLeastQ()) ? wrapped : null;
         }
 
         /**
@@ -261,20 +261,14 @@
             return mHyphenationFrequency;
         }
 
+
         /**
-         * Check if the same text layout.
-         *
-         * @return true if this and the given param result in the same text layout
+         * Similar to equals but don't compare text direction
+         * @hide
          */
-        @Override
-        public boolean equals(@Nullable Object o) {
-            if (o == this) {
-                return true;
-            }
-            if (!(o instanceof Params)) {
-                return false;
-            }
-            Params other = (Params) o;
+        @RestrictTo(LIBRARY_GROUP)
+        public boolean equalsWithoutTextDirection(@NonNull Params other) {
+
             if (mWrapped != null) {
                 return mWrapped.equals(other.mWrapped);
             }
@@ -288,12 +282,6 @@
                 }
             }
 
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
-                if (mTextDir != other.getTextDirection()) {
-                    return false;
-                }
-            }
-
             if (mPaint.getTextSize() != other.getTextPaint().getTextSize()) {
                 return false;
             }
@@ -335,6 +323,31 @@
             return true;
         }
 
+        /**
+         * Check if the same text layout.
+         *
+         * @return true if this and the given param result in the same text layout
+         */
+        @Override
+        public boolean equals(@Nullable Object o) {
+            if (o == this) {
+                return true;
+            }
+            if (!(o instanceof Params)) {
+                return false;
+            }
+            Params other = (Params) o;
+            if (!equalsWithoutTextDirection(other)) {
+                return false;
+            }
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+                if (mTextDir != other.getTextDirection()) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
         @Override
         public int hashCode() {
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
@@ -397,7 +410,7 @@
     // The list of measured paragraph info.
     private final @NonNull int[] mParagraphEnds;
 
-    // null on API 27 or before. Non-null on API 28 or later
+    // null on API 27 or before. Non-null on API 29 or later
     private final @Nullable PrecomputedText mWrapped;
 
     /**
@@ -422,7 +435,7 @@
         try {
             TraceCompat.beginSection("PrecomputedText");
 
-            if (Build.VERSION.SDK_INT >= 28 && params.mWrapped != null) {
+            if (BuildCompat.isAtLeastQ() && params.mWrapped != null) {
                 return new PrecomputedTextCompat(
                         PrecomputedText.create(text, params.mWrapped), params);
             }
@@ -486,7 +499,7 @@
         mText = precomputed;
         mParams = params;
         mParagraphEnds = null;
-        mWrapped = precomputed;
+        mWrapped = (BuildCompat.isAtLeastQ()) ? precomputed : null;
     }
 
     /**
@@ -514,7 +527,7 @@
      * Returns the count of paragraphs.
      */
     public @IntRange(from = 0) int getParagraphCount() {
-        if (Build.VERSION.SDK_INT >= 28) {
+        if (BuildCompat.isAtLeastQ()) {
             return mWrapped.getParagraphCount();
         } else {
             return mParagraphEnds.length;
@@ -526,7 +539,7 @@
      */
     public @IntRange(from = 0) int getParagraphStart(@IntRange(from = 0) int paraIndex) {
         Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex");
-        if (Build.VERSION.SDK_INT >= 28) {
+        if (BuildCompat.isAtLeastQ()) {
             return mWrapped.getParagraphStart(paraIndex);
         } else {
             return paraIndex == 0 ? 0 : mParagraphEnds[paraIndex - 1];
@@ -538,7 +551,7 @@
      */
     public @IntRange(from = 0) int getParagraphEnd(@IntRange(from = 0) int paraIndex) {
         Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex");
-        if (Build.VERSION.SDK_INT >= 28) {
+        if (BuildCompat.isAtLeastQ()) {
             return mWrapped.getParagraphEnd(paraIndex);
         } else {
             return mParagraphEnds[paraIndex];
@@ -662,7 +675,7 @@
             throw new IllegalArgumentException(
                     "MetricAffectingSpan can not be set to PrecomputedText.");
         }
-        if (Build.VERSION.SDK_INT >= 28) {
+        if (BuildCompat.isAtLeastQ()) {
             mWrapped.setSpan(what, start, end, flags);
         } else {
             mText.setSpan(what, start, end, flags);
@@ -678,7 +691,7 @@
             throw new IllegalArgumentException(
                     "MetricAffectingSpan can not be removed from PrecomputedText.");
         }
-        if (Build.VERSION.SDK_INT >= 28) {
+        if (BuildCompat.isAtLeastQ()) {
             mWrapped.removeSpan(what);
         } else {
             mText.removeSpan(what);
@@ -692,7 +705,7 @@
 
     @Override
     public <T> T[] getSpans(int start, int end, Class<T> type) {
-        if (Build.VERSION.SDK_INT >= 28) {
+        if (BuildCompat.isAtLeastQ()) {
             return mWrapped.getSpans(start, end, type);
         } else {
             return mText.getSpans(start, end, type);
diff --git a/core/src/main/java/androidx/core/widget/TextViewCompat.java b/core/src/main/java/androidx/core/widget/TextViewCompat.java
index 8a0c125..d5b3c01 100644
--- a/core/src/main/java/androidx/core/widget/TextViewCompat.java
+++ b/core/src/main/java/androidx/core/widget/TextViewCompat.java
@@ -58,6 +58,7 @@
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.StyleRes;
+import androidx.core.os.BuildCompat;
 import androidx.core.text.PrecomputedTextCompat;
 import androidx.core.util.Preconditions;
 
@@ -879,13 +880,13 @@
     public static void setPrecomputedText(@NonNull TextView textView,
                                           @NonNull PrecomputedTextCompat precomputed) {
 
-        if (Build.VERSION.SDK_INT >= 28) {
+        if (BuildCompat.isAtLeastQ()) {
             // Framework can not understand PrecomptedTextCompat. Pass underlying PrecomputedText.
             // Parameter check is also done by framework.
             textView.setText(precomputed.getPrecomputedText());
         } else {
             PrecomputedTextCompat.Params param = TextViewCompat.getTextMetricsParams(textView);
-            if (!param.equals(precomputed.getParams())) {
+            if (!param.equalsWithoutTextDirection(precomputed.getParams())) {
                 throw new IllegalArgumentException("Given text can not be applied to TextView.");
             }
             textView.setText(precomputed);
diff --git a/cursoradapter/build.gradle b/cursoradapter/build.gradle
index 2493a2b..3e3cdbb 100644
--- a/cursoradapter/build.gradle
+++ b/cursoradapter/build.gradle
@@ -12,7 +12,7 @@
 supportLibrary {
     name = "Android Support Library Cursor Adapter"
     publish = true
-    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenVersion = LibraryVersions.CURSORADAPTER
     mavenGroup = LibraryGroups.CURSORADAPTER
     inceptionYear = "2018"
     description = "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren't a part of the framework APIs. Compatible on devices running API 14 or later."
diff --git a/customview/build.gradle b/customview/build.gradle
index 8c8237c..49bab6f 100644
--- a/customview/build.gradle
+++ b/customview/build.gradle
@@ -18,7 +18,7 @@
 supportLibrary {
     name = "Android Support Library Custom View"
     publish = true
-    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenVersion = LibraryVersions.CUSTOMVIEW
     mavenGroup = LibraryGroups.CUSTOMVIEW
     inceptionYear = "2018"
     description = "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren't a part of the framework APIs. Compatible on devices running API 14 or later."
diff --git a/development/importMaven/build.gradle.kts b/development/importMaven/build.gradle.kts
index 9c87fa2..ee51f38 100644
--- a/development/importMaven/build.gradle.kts
+++ b/development/importMaven/build.gradle.kts
@@ -14,8 +14,94 @@
  * limitations under the License.
  */
 
+import org.apache.maven.model.Dependency
+import org.apache.maven.model.Parent
+import org.apache.maven.model.Repository
+import org.apache.maven.model.building.DefaultModelBuilderFactory
+import org.apache.maven.model.building.DefaultModelBuildingRequest
+import org.apache.maven.model.building.ModelBuildingException
+import org.apache.maven.model.building.ModelBuildingRequest
+import org.apache.maven.model.building.ModelSource
+import org.apache.maven.model.resolution.ModelResolver
 import java.security.MessageDigest
 import org.gradle.api.artifacts.result.ResolvedArtifactResult
+import java.io.InputStream
+
+buildscript {
+    repositories {
+        jcenter()
+        mavenCentral()
+        google()
+    }
+
+    dependencies {
+        classpath(gradleApi())
+        classpath("org.apache.maven:maven-model:3.5.4")
+        classpath("org.apache.maven:maven-model-builder:3.5.4")
+    }
+}
+
+typealias MavenListener = (String, String, String, File) -> Unit
+
+/**
+ * A Maven module resolver which uses Gradle.
+ */
+class MavenModuleResolver(val project: Project, val listener: MavenListener) : ModelResolver {
+
+    override fun resolveModel(dependency: Dependency): ModelSource? {
+        return resolveModel(dependency.groupId, dependency.artifactId, dependency.version)
+    }
+
+    override fun resolveModel(parent: Parent): ModelSource? {
+        return resolveModel(parent.groupId, parent.artifactId, parent.version)
+    }
+
+    override fun resolveModel(
+        groupId: String,
+        artifactId: String,
+        version: String
+    ): ModelSource? {
+        val pomQuery = project.dependencies.createArtifactResolutionQuery()
+        val pomQueryResult = pomQuery.forModule(groupId, artifactId, version)
+            .withArtifacts(
+                MavenModule::class.java,
+                MavenPomArtifact::class.java
+            )
+            .execute()
+        var result: File? = null
+        for (component in pomQueryResult.resolvedComponents) {
+            val pomArtifacts = component.getArtifacts(MavenPomArtifact::class.java)
+            for (pomArtifact in pomArtifacts) {
+                val pomFile = pomArtifact as? ResolvedArtifactResult
+                if (pomFile != null) {
+                    result = pomFile.file
+                    listener.invoke(groupId, artifactId, version, result)
+                }
+            }
+        }
+        return object : ModelSource {
+            override fun getInputStream(): InputStream {
+                return result!!.inputStream()
+            }
+
+            override fun getLocation(): String {
+                return result!!.absolutePath
+            }
+        }
+    }
+
+    override fun addRepository(repository: Repository?) {
+        // We don't need to support this
+    }
+
+    override fun addRepository(repository: Repository?, replace: Boolean) {
+        // We don't need to support this
+    }
+
+    override fun newCopy(): ModelResolver {
+        return this
+    }
+}
 
 // The output folder inside prebuilts
 val prebuiltsLocation = file("../../../../prebuilts/androidx")
@@ -28,17 +114,17 @@
 val artifactName = project.findProperty("artifactName")
 
 val internalArtifacts = listOf(
-        "android.arch(.*)?".toRegex(),
-        "com.android.support(.*)?".toRegex()
+    "android.arch(.*)?".toRegex(),
+    "com.android.support(.*)?".toRegex()
 )
 
 val potentialInternalArtifacts = listOf(
-        "androidx(.*)?".toRegex()
+    "androidx(.*)?".toRegex()
 )
 
 // Need to exclude androidx.databinding
 val forceExternal = setOf(
-        ".databinding"
+    ".databinding"
 )
 
 plugins {
@@ -91,22 +177,39 @@
 /**
  * Returns the supporting files (POM, Source files) for a given artifact.
  */
-fun supportingArtifacts(artifact: ResolvedArtifact): List<ResolvedArtifactResult> {
+fun supportingArtifacts(
+    artifact: ResolvedArtifact,
+    internal: Boolean = false
+): List<ResolvedArtifactResult> {
     val supportingArtifacts = mutableListOf<ResolvedArtifactResult>()
     val pomQuery = project.dependencies.createArtifactResolutionQuery()
+    val modelBuilderFactory = DefaultModelBuilderFactory()
+    val builder = modelBuilderFactory.newInstance()
+    val resolver = MavenModuleResolver(project) { groupId, artifactId, version, pomFile ->
+        copyPomFile(groupId, artifactId, version, pomFile, internal)
+    }
     val pomQueryResult = pomQuery.forComponents(artifact.id.componentIdentifier)
-            .withArtifacts(
-                    MavenModule::class.java,
-                    MavenPomArtifact::class.java)
-            .execute()
+        .withArtifacts(
+            MavenModule::class.java,
+            MavenPomArtifact::class.java
+        )
+        .execute()
 
     for (component in pomQueryResult.resolvedComponents) {
-        // DefaultResolvedArtifactResult is an internal Gradle class.
-        // However, it's being widely used anyway.
         val pomArtifacts = component.getArtifacts(MavenPomArtifact::class.java)
         for (pomArtifact in pomArtifacts) {
             val pomFile = pomArtifact as? ResolvedArtifactResult
             if (pomFile != null) {
+                try {
+                    val request: ModelBuildingRequest = DefaultModelBuildingRequest()
+                    request.modelResolver = resolver
+                    request.pomFile = pomFile.file
+                    // Turn off validations becuase there are lots of bad POM files out there.
+                    request.validationLevel = ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL
+                    builder.build(request).effectiveModel
+                } catch (exception: ModelBuildingException) {
+                    println("Error building model request for $pomArtifact")
+                }
                 supportingArtifacts.add(pomFile)
             }
         }
@@ -116,10 +219,11 @@
     // So if artifacts only have a distributable without a source, we still want to copy the POM file.
     val sourcesQuery = project.dependencies.createArtifactResolutionQuery()
     val sourcesQueryResult = sourcesQuery.forComponents(artifact.id.componentIdentifier)
-            .withArtifacts(
-                    MavenModule::class.java,
-                    SourcesArtifact::class.java)
-            .execute()
+        .withArtifacts(
+            MavenModule::class.java,
+            SourcesArtifact::class.java
+        )
+        .execute()
 
     for (component in sourcesQueryResult.resolvedComponents) {
         val sourcesArtifacts = component.getArtifacts(SourcesArtifact::class.java)
@@ -155,18 +259,20 @@
 /**
  * Copies artifacts to the right locations.
  */
-fun copyLibrary(artifact: ResolvedArtifact, internal: Boolean = false) {
+fun copyArtifact(artifact: ResolvedArtifact, internal: Boolean = false) {
     val folder = if (internal) internalFolder else externalFolder
     val moduleVersionId = artifact.moduleVersion.id
     val group = moduleVersionId.group
     val groupPath = group.split(".").joinToString("/")
-    val pathComponents = listOf(prebuiltsLocation,
-            folder,
-            groupPath,
-            moduleVersionId.name,
-            moduleVersionId.version)
+    val pathComponents = listOf(
+        prebuiltsLocation,
+        folder,
+        groupPath,
+        moduleVersionId.name,
+        moduleVersionId.version
+    )
     val location = pathComponents.joinToString("/")
-    val supportingArtifacts = supportingArtifacts(artifact)
+    val supportingArtifacts = supportingArtifacts(artifact, internal = internal)
     // Copy main artifact
     println("Copying $artifact to $location")
     copy {
@@ -194,33 +300,69 @@
     }
 }
 
+/**
+ * Copies associated POM files to the right location.
+ */
+fun copyPomFile(
+    group: String,
+    name: String,
+    version: String,
+    pomFile: File,
+    internal: Boolean = false
+) {
+    val folder = if (internal) internalFolder else externalFolder
+    val groupPath = group.split(".").joinToString("/")
+    val pathComponents = listOf(
+        prebuiltsLocation,
+        folder,
+        groupPath,
+        name,
+        version
+    )
+    val location = pathComponents.joinToString("/")
+    // Copy associated POM files.
+    println("Copying ${pomFile.name} to $location")
+    copy {
+        from(
+            pomFile,
+            digest(pomFile, "MD5"),
+            digest(pomFile, "SHA1")
+        )
+        into(location)
+    }
+    copy {
+        into(location)
+    }
+}
+
 tasks {
     val fetchArtifacts by creating {
         doLast {
             // Collect all the internal and external dependencies.
             // Copy the jar/aar's and their respective POM files.
             val internalLibraries =
-                    filterInternalLibraries(
-                        fetchArtifactsContainer
-                                    .resolvedConfiguration
-                                    .resolvedArtifacts)
+                filterInternalLibraries(
+                    fetchArtifactsContainer
+                        .resolvedConfiguration
+                        .resolvedArtifacts
+                )
 
             val externalLibraries =
-                    fetchArtifactsContainer
-                            .resolvedConfiguration
-                            .resolvedArtifacts.filter {
-                        val isInternal = internalLibraries.contains(it)
-                        !isInternal
-                    }
+                fetchArtifactsContainer
+                    .resolvedConfiguration
+                    .resolvedArtifacts.filter {
+                    val isInternal = internalLibraries.contains(it)
+                    !isInternal
+                }
 
             println("\r\nInternal Libraries")
             internalLibraries.forEach { library ->
-                copyLibrary(library, internal = true)
+                copyArtifact(library, internal = true)
             }
 
             println("\r\nExternal Libraries")
             externalLibraries.forEach { library ->
-                copyLibrary(library, internal = false)
+                copyArtifact(library, internal = false)
             }
             println("\r\nResolved artifacts for $artifactName.")
         }
diff --git a/development/triage-guesser.py b/development/triage-guesser.py
new file mode 100755
index 0000000..e421bd7
--- /dev/null
+++ b/development/triage-guesser.py
@@ -0,0 +1,274 @@
+#!/usr/bin/python
+
+import sys, re, subprocess, os
+
+def usage():
+  print("""Usage: cat <issues> | triage-guesser.py
+triage-guesser.py attempts to guess the assignee based on the title of the bug
+
+triage-guesser reads issues from stdin
+""")
+  sys.exit(1)
+
+class Issue(object):
+  def __init__(self, issueId, description):
+    self.issueId = issueId
+    self.description = description
+
+class AssigneeRecommendation(object):
+  def __init__(self, usernames, justification):
+    self.usernames = usernames
+    self.justification = justification
+
+  def intersect(self, other):
+    names = []
+    for name in self.usernames:
+      if name in other.usernames:
+        names.append(name)
+    justification = self.justification + ", " + other.justification
+    return AssigneeRecommendation(names, justification)
+
+class RecommenderRule(object):
+  def __init__(self):
+    return
+
+  def recommend(self, bug):
+    return
+
+class ShellRunner(object):
+  def __init__(self):
+    return
+
+  def runAndGetOutput(self, args):
+    return subprocess.check_output(args)
+shellRunner = ShellRunner()
+
+class WordRule(RecommenderRule):
+  def __init__(self, word, assignees):
+    super(WordRule, self).__init__()
+    self.word = word
+    self.assignees = assignees
+
+  def recommend(self, bug):
+    if self.word.lower() in bug.description.lower():
+      return AssigneeRecommendation(self.assignees, '"' + self.word + '"')
+    return None
+
+class FileFinder(object):
+  def __init__(self, rootPath):
+    self.rootPath = rootPath
+    self.resultsCache = {}
+
+  def findIname(self, name):
+    if name not in self.resultsCache:
+      text = shellRunner.runAndGetOutput(["find", self.rootPath , "-type", "f", "-iname", name])
+      filePaths = [path.strip() for path in text.split("\n")]
+      filePaths = [path for path in filePaths if path != ""]
+      self.resultsCache[name] = filePaths
+    return self.resultsCache[name]
+
+class InterestingFileFinder(object):
+  def __init__(self):
+    return
+
+  def findInterestingWords(self, text):
+    words = re.split("#| |\.", text)
+    words = [word for word in words if len(word) >= 4]
+    words.sort(key=len, reverse=True)
+    return words
+interestingFileFinder = InterestingFileFinder()
+
+class GitLogger(object):
+  def __init__(self):
+    return
+
+  def gitLog1Author(self, filePath):
+    text = shellRunner.runAndGetOutput(["bash", "-c", "cd " + os.path.dirname(filePath) + " && git log --no-merges -1 --format='%ae' -- " + filePath]).strip().replace("@google.com", "")
+    return text
+gitLogger = GitLogger()
+
+class LastTouchedBy_Rule(RecommenderRule):
+  def __init__(self, fileFinder):
+    super(LastTouchedBy_Rule, self).__init__()
+    self.fileFinder = fileFinder
+
+  def recommend(self, bug):
+    interestingWords = interestingFileFinder.findInterestingWords(bug.description)
+    for word in interestingWords:
+      for queryString in [word + "*", word + ".*"]:
+        filePaths = self.fileFinder.findIname(queryString)
+        if len(filePaths) > 0 and len(filePaths) <= 4:
+          candidateAuthors = []
+          for path in filePaths:
+            thisAuthor = gitLogger.gitLog1Author(path)
+            if len(candidateAuthors) == 0 or thisAuthor != candidateAuthors[-1]:
+              candidateAuthors.append(thisAuthor)
+          if len(candidateAuthors) == 1:
+             return AssigneeRecommendation(candidateAuthors, "last touched " + os.path.basename(filePaths[0]))
+    return None
+
+class OwnersRule(RecommenderRule):
+  def __init__(self, fileFinder):
+    super(OwnersRule, self).__init__()
+    self.fileFinder = fileFinder
+
+  def recommend(self, bug):
+    interestingWords = interestingFileFinder.findInterestingWords(bug.description)
+    for word in interestingWords:
+      for queryString in [word + "*", word + ".*"]:
+        filePaths = self.fileFinder.findIname(queryString)
+        commonPrefix = os.path.commonprefix(filePaths)
+        dirToCheck = commonPrefix
+        if len(dirToCheck) < 1:
+          continue
+        while True:
+          if dirToCheck[-1] == "/":
+            dirToCheck = dirToCheck[:-1]
+          if len(dirToCheck) <= len(self.fileFinder.rootPath):
+            break
+          ownerFilePath = os.path.join(dirToCheck, "OWNERS")
+          if os.path.isfile(ownerFilePath):
+            with open(ownerFilePath) as ownerFile:
+              lines = ownerFile.readlines()
+              names = [line.replace("@google.com", "").strip() for line in lines]
+              relOwnersPath = os.path.relpath(ownerFilePath, self.fileFinder.rootPath)
+              justification = relOwnersPath + " (" + os.path.basename(filePaths[0] + ' ("' + word + '")')
+              if len(filePaths) > 1:
+                justification += "..."
+              justification += ")"
+              return AssigneeRecommendation(names, justification)
+          else:
+            parent = os.path.dirname(dirToCheck)
+            if len(parent) >= len(dirToCheck):
+              break
+            dirToCheck = parent
+
+
+class Triager(object):
+  def __init__(self, fileFinder):
+    self.recommenderRules = self.parseKnownOwners({
+      "fragment": ["ilake", "mount", "adamp"],
+      "animation": ["chet", "mount", "tianlu"],
+      "transition": ["chet", "mount"],
+      "theme": ["alanv"],
+      "style": ["alanv"],
+      "preferences": ["pavlis", "lpf"],
+      "ViewPager": ["jgielzak", "aurimas"],
+      "DrawerLayout": ["kirillg"],
+      "RecyclerView": ["shepshapard", "yboyar"],
+      "Loaders": ["ilake"],
+      "VectorDrawableCompat": ["tianliu"],
+      "AppCompat": ["kirillg"],
+      "Design Library": ["dcarlsson"],
+      "android.support.design": ["dcarlsson"],
+      "RenderThread": ["jreck"],
+      "VectorDrawable": ["tianliu"],
+      "drawable": ["alanv"],
+      "colorstatelist": ["alanv"],
+      "multilocale": ["nona", "mnita"],
+      "TextView": ["siyamed", "clarabayarri"],
+      "Linkify": ["siyamed", "toki"],
+      "Spannable": ["siyamed"],
+      "Minikin": ["nona"],
+      "Fonts": ["clarabayarri", "dougfelt"],
+      "freetype": ["nona", "junkshik"],
+      "harfbuzz": ["nona", "junkshik"],
+      "slice": ["jmonk", "madym"]
+    })
+    self.recommenderRules.append(OwnersRule(fileFinder))
+    self.recommenderRules.append(LastTouchedBy_Rule(fileFinder))
+
+  def parseKnownOwners(self, ownersDict):
+    rules = []
+    keywords = sorted(ownersDict.keys())
+    for keyword in keywords:
+      assignees = ownersDict[keyword]
+      rules.append(WordRule(keyword, assignees))
+    return rules
+
+  def process(self, lines):
+    issues = self.parseIssues(lines)
+    outputs = []
+    print("Analyzing " + str(len(issues)) + " issues")
+    for issue in issues:
+      print(".")
+      assigneeRecommendation = self.recommendAssignees(issue)
+      recommendationText = "?"
+      if assigneeRecommendation is not None:
+        usernames = assigneeRecommendation.usernames
+        if len(usernames) > 2:
+          usernames = usernames[:2]
+        recommendationText = str(usernames) + " (" + assigneeRecommendation.justification + ")"
+      outputs.append(("(" + issue.issueId + ") " + issue.description.replace("\t", "...."), recommendationText, ))
+    maxColumnWidth = 0
+    for item in outputs:
+      maxColumnWidth = max(maxColumnWidth, len(item[0]))
+    for item in outputs:
+      print(str(item[0]) + (" " * (maxColumnWidth - len(item[0]))) + " -> " + str(item[1]))
+
+  def parseIssues(self, lines):
+    priority = ""
+    issueType = ""
+    description = ""
+    when = ""
+
+    lines = [line.strip() for line in lines]
+    fields = [line for line in lines if line != ""]
+    linesPerIssue = 5
+    if len(fields) % linesPerIssue != 0:
+      raise Exception("Parse error, number of lines must be divisible by " + str(linesPerIssue) + ", not " + str(len(fields)) + ". Last line: " + fields[-1])
+    issues = []
+    while len(fields) > 0:
+      priority = fields[0]
+      issueType = fields[1]
+
+      middle = fields[2].split("\t")
+      expectedNumTabComponents = 3
+      if len(middle) != expectedNumTabComponents:
+        raise Exception("Parse error: wrong number of tabs in " + str(middle) + ", got " + str(len(middle) - 1) + ", expected " + str(expectedNumTabComponents - 1))
+      description = middle[0]
+      currentAssignee = middle[1]
+      status = middle[2]
+
+
+      middle2 = fields[3].split("\t")
+      expectedNumTabComponents = 2
+      if len(middle2) != expectedNumTabComponents:
+        raise Exception("Parse error: wrong number of tabs in " + str(middle2) + ", got " + str(len(middle2) - 1) + ", expected " + str(expectedNumTabComponents - 1))
+      issueId = middle2[1]
+      
+      when = fields[4]
+
+      issues.append(Issue(issueId, description))
+      fields = fields[linesPerIssue:]
+    return issues
+
+  def recommendAssignees(self, issue):
+    overallRecommendation = None
+    for rule in self.recommenderRules:
+      thisRecommendation = rule.recommend(issue)
+      if thisRecommendation is not None:
+        if overallRecommendation is None:
+          overallRecommendation = thisRecommendation
+        else:
+          newRecommendation = overallRecommendation.intersect(thisRecommendation)
+          count = len(newRecommendation.usernames)
+          if count > 0 and count < len(overallRecommendation.usernames):
+            overallRecommendation = newRecommendation
+    return overallRecommendation
+    
+   
+
+def main(args):
+  if len(args) != 1:
+    usage()
+  fileFinder = FileFinder(os.path.dirname(args[0]))
+  print("Reading issues from stdin")
+  lines = sys.stdin.readlines()
+  triager = Triager(fileFinder)
+  triager.process(lines)
+
+  
+  
+
+main(sys.argv)
diff --git a/docs-fake/build.gradle b/docs-fake/build.gradle
index e73ba9b..685a75c 100644
--- a/docs-fake/build.gradle
+++ b/docs-fake/build.gradle
@@ -37,6 +37,7 @@
 android {
     defaultConfig {
         minSdkVersion SupportConfig.CURRENT_SDK_VERSION
+        javaCompileOptions.annotationProcessorOptions.includeCompileClasspath = false
     }
 
     sourceSets {
diff --git a/documentfile/build.gradle b/documentfile/build.gradle
index ab03322..5d57db5 100644
--- a/documentfile/build.gradle
+++ b/documentfile/build.gradle
@@ -15,7 +15,7 @@
 supportLibrary {
     name = "Android Support Library Document File"
     publish = true
-    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenVersion = LibraryVersions.DOCUMENTFILE
     mavenGroup = LibraryGroups.DOCUMENTFILE
     inceptionYear = "2018"
     description = "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren't a part of the framework APIs. Compatible on devices running API 14 or later."
diff --git a/drawerlayout/build.gradle b/drawerlayout/build.gradle
index a30723a..875e4a0 100644
--- a/drawerlayout/build.gradle
+++ b/drawerlayout/build.gradle
@@ -14,7 +14,7 @@
 supportLibrary {
     name = "Android Support Library Drawer Layout"
     publish = true
-    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenVersion = LibraryVersions.DRAWERLAYOUT
     mavenGroup = LibraryGroups.DRAWERLAYOUT
     inceptionYear = "2018"
     description = "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren't a part of the framework APIs. Compatible on devices running API 14 or later."
diff --git a/dynamic-animation/build.gradle b/dynamic-animation/build.gradle
index 0f8fca9..015290f 100644
--- a/dynamic-animation/build.gradle
+++ b/dynamic-animation/build.gradle
@@ -21,7 +21,7 @@
 supportLibrary {
     name = "Android Support DynamicAnimation"
     publish = true
-    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenVersion = LibraryVersions.DYNAMICANIMATION
     mavenGroup = LibraryGroups.DYNAMICANIMATION
     inceptionYear = "2017"
     description = "Physics-based animation in support library, where the animations are driven by physics force. You can use this Animation library to create smooth and realistic animations."
diff --git a/dynamic-animation/ktx/api/1.0.0-alpha01.txt b/dynamic-animation/ktx/api/1.0.0-alpha01.txt
index 01c8728..fe2c248 100644
--- a/dynamic-animation/ktx/api/1.0.0-alpha01.txt
+++ b/dynamic-animation/ktx/api/1.0.0-alpha01.txt
@@ -3,8 +3,8 @@
 
   public final class DynamicAnimationKt {
     ctor public DynamicAnimationKt();
-    method public static <K extends android.view.View> androidx.dynamicanimation.animation.FlingAnimation flingAnimationOf(K, androidx.dynamicanimation.animation.FloatPropertyCompat<K> property);
-    method public static <K extends android.view.View> androidx.dynamicanimation.animation.SpringAnimation springAnimationOf(K, androidx.dynamicanimation.animation.FloatPropertyCompat<K> property, float finalPosition = Float.NaN);
+    method public static <K> androidx.dynamicanimation.animation.FlingAnimation flingAnimationOf(K!, androidx.dynamicanimation.animation.FloatPropertyCompat<K> property);
+    method public static <K> androidx.dynamicanimation.animation.SpringAnimation springAnimationOf(K!, androidx.dynamicanimation.animation.FloatPropertyCompat<K> property, float finalPosition = Float.NaN);
     method public static androidx.dynamicanimation.animation.SpringAnimation withSpringForceProperties(androidx.dynamicanimation.animation.SpringAnimation, kotlin.jvm.functions.Function1<? super androidx.dynamicanimation.animation.SpringForce,kotlin.Unit> func);
   }
 
diff --git a/dynamic-animation/ktx/api/current.txt b/dynamic-animation/ktx/api/current.txt
new file mode 100644
index 0000000..01c8728
--- /dev/null
+++ b/dynamic-animation/ktx/api/current.txt
@@ -0,0 +1,12 @@
+// Signature format: 2.0
+package androidx.dynamicanimation.animation {
+
+  public final class DynamicAnimationKt {
+    ctor public DynamicAnimationKt();
+    method public static <K extends android.view.View> androidx.dynamicanimation.animation.FlingAnimation flingAnimationOf(K, androidx.dynamicanimation.animation.FloatPropertyCompat<K> property);
+    method public static <K extends android.view.View> androidx.dynamicanimation.animation.SpringAnimation springAnimationOf(K, androidx.dynamicanimation.animation.FloatPropertyCompat<K> property, float finalPosition = Float.NaN);
+    method public static androidx.dynamicanimation.animation.SpringAnimation withSpringForceProperties(androidx.dynamicanimation.animation.SpringAnimation, kotlin.jvm.functions.Function1<? super androidx.dynamicanimation.animation.SpringForce,kotlin.Unit> func);
+  }
+
+}
+
diff --git a/dynamic-animation/ktx/build.gradle b/dynamic-animation/ktx/build.gradle
index d2c0d63..95c9d3e 100644
--- a/dynamic-animation/ktx/build.gradle
+++ b/dynamic-animation/ktx/build.gradle
@@ -17,11 +17,7 @@
 import androidx.build.LibraryGroups
 import androidx.build.LibraryVersions
 
-import static androidx.build.dependencies.DependenciesKt.JUNIT
-import static androidx.build.dependencies.DependenciesKt.KOTLIN_STDLIB
-import static androidx.build.dependencies.DependenciesKt.TEST_RULES
-import static androidx.build.dependencies.DependenciesKt.TEST_RUNNER
-import static androidx.build.dependencies.DependenciesKt.TRUTH
+import static androidx.build.dependencies.DependenciesKt.*
 
 plugins {
     id("SupportAndroidLibraryPlugin")
@@ -46,6 +42,7 @@
     androidTestImplementation(TEST_RUNNER)
     androidTestImplementation(TEST_RULES)
     androidTestImplementation(TRUTH)
+    androidTestImplementation(GUAVA_LISTENABLE_FUTURE_AVOID_CONFLICT)
 }
 
 supportLibrary {
diff --git a/dynamic-animation/ktx/src/androidTest/java/androidx/dynamicanimation/animation/DynamicAnimationTest.kt b/dynamic-animation/ktx/src/androidTest/java/androidx/dynamicanimation/animation/DynamicAnimationTest.kt
new file mode 100644
index 0000000..1cce0dd
--- /dev/null
+++ b/dynamic-animation/ktx/src/androidTest/java/androidx/dynamicanimation/animation/DynamicAnimationTest.kt
@@ -0,0 +1,112 @@
+/*
+ * 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.dynamicanimation.animation
+
+import androidx.test.filters.SmallTest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+
+@SmallTest
+class DynamicAnimationTest {
+
+    private val animObject = Any()
+    private lateinit var floatPropertyCompat: FloatPropertyCompat<Any>
+
+    /**
+     * Setup [FloatPropertyCompat] before test start
+     */
+    @Before fun setup() {
+        floatPropertyCompat = object : FloatPropertyCompat<Any>("") {
+
+            private var value = 0f
+
+            override fun getValue(`object`: Any?): Float {
+                return value
+            }
+
+            override fun setValue(`object`: Any?, value: Float) {
+                this.value = value
+            }
+        }
+    }
+
+    /**
+     * Test extension for creating [FlingAnimation]
+     */
+    @Test fun testCreateFlingAnimation() {
+        val flingAnimation = animObject.flingAnimationOf(floatPropertyCompat)
+        assertNotNull(flingAnimation)
+        assertEquals(flingAnimation.mTarget, animObject)
+        assertEquals(flingAnimation.mProperty, floatPropertyCompat)
+        assertTrue(flingAnimation.friction == 1f)
+        flingAnimation.friction = 1.5f
+        assertTrue(flingAnimation.friction == 1.5f)
+    }
+
+    /**
+     * Test extension for creating [SpringAnimation]
+     */
+    @Test fun testCreateSpringAnimation() {
+        val springAnimationWithoutFinalPosition = animObject.springAnimationOf(floatPropertyCompat)
+        assertNotNull(springAnimationWithoutFinalPosition)
+        assertEquals(springAnimationWithoutFinalPosition.mTarget, animObject)
+        assertEquals(springAnimationWithoutFinalPosition.mProperty, floatPropertyCompat)
+        assertNull(springAnimationWithoutFinalPosition.spring)
+        val springAnimationWithFinalPosition = animObject.springAnimationOf(floatPropertyCompat,
+            100f)
+        assertNotNull(springAnimationWithFinalPosition)
+        assertEquals(springAnimationWithFinalPosition.mTarget, animObject)
+        assertEquals(springAnimationWithFinalPosition.mProperty, floatPropertyCompat)
+        assertNotNull(springAnimationWithFinalPosition.spring)
+        assertEquals(springAnimationWithFinalPosition.spring.finalPosition, 100f)
+    }
+
+    /**
+     * Test extension for setting up [SpringForce]
+     */
+    @Test fun testSpringForce() {
+        val springAnimationWithoutFinalPosition = animObject
+            .springAnimationOf(floatPropertyCompat)
+            .withSpringForceProperties {
+                finalPosition = 100f
+                dampingRatio = SpringForce.DAMPING_RATIO_HIGH_BOUNCY
+                stiffness = SpringForce.STIFFNESS_HIGH
+            }
+        assertNotNull(springAnimationWithoutFinalPosition.spring)
+        assertEquals(springAnimationWithoutFinalPosition.spring.finalPosition, 100f)
+        assertEquals(springAnimationWithoutFinalPosition.spring.dampingRatio,
+            SpringForce.DAMPING_RATIO_HIGH_BOUNCY)
+        assertEquals(springAnimationWithoutFinalPosition.spring.stiffness,
+            SpringForce.STIFFNESS_HIGH)
+        val springAnimationWithFinalPosition = animObject
+            .springAnimationOf(floatPropertyCompat, 120f)
+            .withSpringForceProperties {
+                dampingRatio = SpringForce.DAMPING_RATIO_LOW_BOUNCY
+                stiffness = SpringForce.STIFFNESS_LOW
+            }
+        assertNotNull(springAnimationWithFinalPosition.spring)
+        assertEquals(springAnimationWithFinalPosition.spring.finalPosition, 120f)
+        assertEquals(springAnimationWithFinalPosition.spring.dampingRatio,
+            SpringForce.DAMPING_RATIO_LOW_BOUNCY)
+        assertEquals(springAnimationWithFinalPosition.spring.stiffness,
+            SpringForce.STIFFNESS_LOW)
+    }
+}
\ No newline at end of file
diff --git a/dynamic-animation/ktx/src/main/java/androidx/dynamicanimation/animation/DynamicAnimation.kt b/dynamic-animation/ktx/src/main/java/androidx/dynamicanimation/animation/DynamicAnimation.kt
index 7a25b3e..bf0eaef 100644
--- a/dynamic-animation/ktx/src/main/java/androidx/dynamicanimation/animation/DynamicAnimation.kt
+++ b/dynamic-animation/ktx/src/main/java/androidx/dynamicanimation/animation/DynamicAnimation.kt
@@ -16,28 +16,26 @@
 
 package androidx.dynamicanimation.animation
 
-import android.view.View
-
 /**
- * Creates [FlingAnimation] for view.
+ * Creates [FlingAnimation] for object.
  *
- * @param property View property to be animated.
+ * @param property object's property to be animated.
  * @return [FlingAnimation]
  */
-inline fun <K : View> K.flingAnimationOf(property: FloatPropertyCompat<K>): FlingAnimation {
+inline fun <K> K.flingAnimationOf(property: FloatPropertyCompat<K>): FlingAnimation {
     return FlingAnimation(this, property)
 }
 
 /**
- * Creates [SpringAnimation] for view.
+ * Creates [SpringAnimation] for object.
  * If finalPosition is not [Float.NaN] then create [SpringAnimation] with
  * [SpringForce.mFinalPosition].
  *
- * @param property View property to be animated.
+ * @param property object's property to be animated.
  * @param finalPosition [SpringForce.mFinalPosition] Final position of spring.
  * @return [SpringAnimation]
  */
-inline fun <K : View> K.springAnimationOf(
+inline fun <K> K.springAnimationOf(
     property: FloatPropertyCompat<K>,
     finalPosition: Float = Float.NaN
 ): SpringAnimation {
diff --git a/dynamic-animation/src/androidTest/java/androidx/dynamicanimation/tests/FlingTests.java b/dynamic-animation/src/androidTest/java/androidx/dynamicanimation/tests/FlingTests.java
index b872bdc..6683eaf 100644
--- a/dynamic-animation/src/androidTest/java/androidx/dynamicanimation/tests/FlingTests.java
+++ b/dynamic-animation/src/androidTest/java/androidx/dynamicanimation/tests/FlingTests.java
@@ -102,7 +102,7 @@
     }
 
     /**
-     * Test that spring animation can work with a single property without an object.
+     * Test that fling animation can work with a single property without an object.
      */
     @Test
     public void testFloatValueHolder() {
diff --git a/emoji/appcompat/build.gradle b/emoji/appcompat/build.gradle
index 332c667..2e8e6d6 100644
--- a/emoji/appcompat/build.gradle
+++ b/emoji/appcompat/build.gradle
@@ -29,7 +29,7 @@
 supportLibrary {
     name = "Android Emoji AppCompat"
     publish = true
-    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenVersion = LibraryVersions.EMOJI
     mavenGroup = LibraryGroups.EMOJI
     inceptionYear = "2017"
     description = "EmojiCompat Widgets for AppCompat integration"
diff --git a/emoji/bundled/build.gradle b/emoji/bundled/build.gradle
index 51be3c6..8b72f6f 100644
--- a/emoji/bundled/build.gradle
+++ b/emoji/bundled/build.gradle
@@ -22,7 +22,7 @@
 supportLibrary {
     name = "Android Emoji Compat"
     publish = true
-    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenVersion = LibraryVersions.EMOJI
     mavenGroup = LibraryGroups.EMOJI
     inceptionYear = "2017"
     description = "Library bundled with assets to enable emoji compatibility in Kitkat and newer devices to avoid the empty emoji characters."
diff --git a/emoji/core/build.gradle b/emoji/core/build.gradle
index 2c892ff..253b91c 100644
--- a/emoji/core/build.gradle
+++ b/emoji/core/build.gradle
@@ -56,7 +56,7 @@
 supportLibrary {
     name = "Android Emoji Compat"
     publish = true
-    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenVersion = LibraryVersions.EMOJI
     mavenGroup = LibraryGroups.EMOJI
     inceptionYear = "2017"
     description = "Core library to enable emoji compatibility in Kitkat and newer devices to avoid the empty emoji characters."
diff --git a/exifinterface/build.gradle b/exifinterface/build.gradle
index ffe28d3..df7161f 100644
--- a/exifinterface/build.gradle
+++ b/exifinterface/build.gradle
@@ -15,7 +15,7 @@
 supportLibrary {
     name = "Android Support ExifInterface"
     publish = true
-    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenVersion = LibraryVersions.EXIFINTERFACE
     mavenGroup = LibraryGroups.EXIFINTERFACE
     inceptionYear = "2016"
     description = "Android Support ExifInterface"
diff --git a/fragment/build.gradle b/fragment/build.gradle
index 5978565..0eacb2a 100644
--- a/fragment/build.gradle
+++ b/fragment/build.gradle
@@ -31,6 +31,7 @@
     androidTestImplementation project(':internal-testutils'), {
         exclude group: 'androidx.fragment', module: 'fragment'
     }
+    androidTestImplementation(GUAVA_LISTENABLE_FUTURE_AVOID_CONFLICT)
 }
 
 supportLibrary {
diff --git a/fragment/ktx/build.gradle b/fragment/ktx/build.gradle
index feffd87..afe98c1 100644
--- a/fragment/ktx/build.gradle
+++ b/fragment/ktx/build.gradle
@@ -41,6 +41,7 @@
     androidTestImplementation(TRUTH)
     androidTestImplementation(TEST_RUNNER)
     androidTestImplementation(TEST_RULES)
+    androidTestImplementation(GUAVA_LISTENABLE_FUTURE_AVOID_CONFLICT)
 }
 
 supportLibrary {
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/FragmentLifecycleTest.java b/fragment/src/androidTest/java/androidx/fragment/app/FragmentLifecycleTest.java
index eebbe55..434170f 100644
--- a/fragment/src/androidTest/java/androidx/fragment/app/FragmentLifecycleTest.java
+++ b/fragment/src/androidTest/java/androidx/fragment/app/FragmentLifecycleTest.java
@@ -17,6 +17,8 @@
 
 package androidx.fragment.app;
 
+import static com.google.common.truth.Truth.assertWithMessage;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
@@ -1025,6 +1027,201 @@
         fc2.dispatchDestroy();
     }
 
+    /**
+     * Test the availability of getTargetFragment() when the target fragment is
+     * not retained and the referrer fragment is not retained.
+     */
+    @Test
+    @UiThreadTest
+    public void targetFragmentNonRetainedNonRetained() {
+        final FragmentController fc = FragmentController.createController(
+                new HostCallbacks(mActivityRule.getActivity()));
+
+        final FragmentManager fm = fc.getSupportFragmentManager();
+
+        fc.attachHost(null);
+        fc.dispatchCreate();
+        fc.dispatchActivityCreated();
+        fc.noteStateNotSaved();
+        fc.execPendingActions();
+        fc.dispatchStart();
+        fc.dispatchResume();
+        fc.execPendingActions();
+
+        final Fragment target = new TargetFragment();
+        final Fragment referrer = new ReferrerFragment();
+        referrer.setTargetFragment(target, 0);
+
+        assertWithMessage("Target Fragment should be accessible before being added")
+                .that(referrer.getTargetFragment())
+                .isSameAs(target);
+
+        fm.beginTransaction().add(target, "target").add(referrer, "referrer").commitNow();
+
+        assertWithMessage("Target Fragment should be accessible after being added")
+                .that(referrer.getTargetFragment())
+                .isSameAs(target);
+
+        fm.beginTransaction()
+                .remove(referrer)
+                .commitNow();
+
+        assertWithMessage("Target Fragment should be accessible after being removed")
+                .that(referrer.getTargetFragment())
+                .isSameAs(target);
+
+        fc.dispatchPause();
+        fc.dispatchStop();
+        fc.dispatchDestroy();
+    }
+
+    /**
+     * Test the availability of getTargetFragment() when the target fragment is
+     * retained and the referrer fragment is not retained.
+     */
+    @Test
+    @UiThreadTest
+    public void targetFragmentRetainedNonRetained() {
+        final FragmentController fc = FragmentController.createController(
+                new HostCallbacks(mActivityRule.getActivity()));
+
+        final FragmentManager fm = fc.getSupportFragmentManager();
+
+        fc.attachHost(null);
+        fc.dispatchCreate();
+        fc.dispatchActivityCreated();
+        fc.noteStateNotSaved();
+        fc.execPendingActions();
+        fc.dispatchStart();
+        fc.dispatchResume();
+        fc.execPendingActions();
+
+        final Fragment target = new TargetFragment();
+        target.setRetainInstance(true);
+        final Fragment referrer = new ReferrerFragment();
+        referrer.setTargetFragment(target, 0);
+
+        assertWithMessage("Target Fragment should be accessible before being added")
+                .that(referrer.getTargetFragment())
+                .isSameAs(target);
+
+        fm.beginTransaction().add(target, "target").add(referrer, "referrer").commitNow();
+
+        assertWithMessage("Target Fragment should be accessible after being added")
+                .that(referrer.getTargetFragment())
+                .isSameAs(target);
+
+        fm.beginTransaction()
+                .remove(referrer)
+                .commitNow();
+
+        assertWithMessage("Target Fragment should be accessible after being removed")
+                .that(referrer.getTargetFragment())
+                .isSameAs(target);
+
+        fc.dispatchPause();
+        fc.dispatchStop();
+        fc.dispatchDestroy();
+    }
+
+    /**
+     * Test the availability of getTargetFragment() when the target fragment is
+     * not retained and the referrer fragment is retained.
+     */
+    @Test
+    @UiThreadTest
+    public void targetFragmentNonRetainedRetained() {
+        final FragmentController fc = FragmentController.createController(
+                new HostCallbacks(mActivityRule.getActivity()));
+
+        final FragmentManager fm = fc.getSupportFragmentManager();
+
+        fc.attachHost(null);
+        fc.dispatchCreate();
+        fc.dispatchActivityCreated();
+        fc.noteStateNotSaved();
+        fc.execPendingActions();
+        fc.dispatchStart();
+        fc.dispatchResume();
+        fc.execPendingActions();
+
+        final Fragment target = new TargetFragment();
+        final Fragment referrer = new ReferrerFragment();
+        referrer.setTargetFragment(target, 0);
+        referrer.setRetainInstance(true);
+
+        assertWithMessage("Target Fragment should be accessible before being added")
+                .that(referrer.getTargetFragment())
+                .isSameAs(target);
+
+        fm.beginTransaction().add(target, "target").add(referrer, "referrer").commitNow();
+
+        assertWithMessage("Target Fragment should be accessible after being added")
+                .that(referrer.getTargetFragment())
+                .isSameAs(target);
+
+        // Save the state
+        fc.dispatchPause();
+        fc.saveAllState();
+        fc.retainNestedNonConfig();
+        fc.dispatchStop();
+        fc.dispatchDestroy();
+
+
+        assertWithMessage("Target Fragment should be null after target Fragment destruction")
+                .that(referrer.getTargetFragment())
+                .isNull();
+    }
+
+    /**
+     * Test the availability of getTargetFragment() when the target fragment is
+     * retained and the referrer fragment is also retained.
+     */
+    @Test
+    @UiThreadTest
+    public void targetFragmentRetainedRetained() {
+        final FragmentController fc = FragmentController.createController(
+                new HostCallbacks(mActivityRule.getActivity()));
+
+        final FragmentManager fm = fc.getSupportFragmentManager();
+
+        fc.attachHost(null);
+        fc.dispatchCreate();
+        fc.dispatchActivityCreated();
+        fc.noteStateNotSaved();
+        fc.execPendingActions();
+        fc.dispatchStart();
+        fc.dispatchResume();
+        fc.execPendingActions();
+
+        final Fragment target = new TargetFragment();
+        target.setRetainInstance(true);
+        final Fragment referrer = new ReferrerFragment();
+        referrer.setRetainInstance(true);
+        referrer.setTargetFragment(target, 0);
+
+        assertWithMessage("Target Fragment should be accessible before being added")
+                .that(referrer.getTargetFragment())
+                .isSameAs(target);
+
+        fm.beginTransaction().add(target, "target").add(referrer, "referrer").commitNow();
+
+        assertWithMessage("Target Fragment should be accessible after being added")
+                .that(referrer.getTargetFragment())
+                .isSameAs(target);
+
+        // Save the state
+        fc.dispatchPause();
+        fc.saveAllState();
+        fc.retainNestedNonConfig();
+        fc.dispatchStop();
+        fc.dispatchDestroy();
+
+        assertWithMessage("Target Fragment should be accessible after FragmentManager destruction")
+                .that(referrer.getTargetFragment())
+                .isSameAs(target);
+    }
+
     @Test
     public void targetFragmentNoCycles() throws Throwable {
         final Fragment one = new Fragment();
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/FragmentTest.java b/fragment/src/androidTest/java/androidx/fragment/app/FragmentTest.java
index 2e9c9bc..7864152 100644
--- a/fragment/src/androidTest/java/androidx/fragment/app/FragmentTest.java
+++ b/fragment/src/androidTest/java/androidx/fragment/app/FragmentTest.java
@@ -27,12 +27,14 @@
 import android.view.ViewTreeObserver;
 import android.view.animation.Animation;
 
+import androidx.annotation.RequiresApi;
 import androidx.fragment.app.test.FragmentTestActivity;
 import androidx.fragment.test.R;
 import androidx.test.InstrumentationRegistry;
 import androidx.test.annotation.UiThreadTest;
 import androidx.test.filters.LargeTest;
 import androidx.test.filters.MediumTest;
+import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
 import androidx.test.rule.ActivityTestRule;
 import androidx.test.runner.AndroidJUnit4;
@@ -81,6 +83,7 @@
 
     @LargeTest
     @Test
+    @SdkSuppress(minSdkVersion = 16) // waitForHalfFadeIn requires API 16
     public void testChildFragmentManagerGone() throws Throwable {
         final FragmentA fragmentA = new FragmentA();
         final FragmentB fragmentB = new FragmentB();
@@ -127,6 +130,7 @@
         FragmentTestUtil.popBackStackImmediate(mActivityRule);
     }
 
+    @RequiresApi(16) // ViewTreeObserver.OnDrawListener was added in API 16
     private void waitForHalfFadeIn(Fragment fragment) throws Throwable {
         if (fragment.getView() == null) {
             FragmentTestUtil.waitForExecution(mActivityRule);
diff --git a/fragment/src/main/java/androidx/fragment/app/FragmentManagerImpl.java b/fragment/src/main/java/androidx/fragment/app/FragmentManagerImpl.java
index 0ba9b8b..79697ec 100644
--- a/fragment/src/main/java/androidx/fragment/app/FragmentManagerImpl.java
+++ b/fragment/src/main/java/androidx/fragment/app/FragmentManagerImpl.java
@@ -1039,6 +1039,15 @@
                                     f.mHost = null;
                                     f.mParentFragment = null;
                                     f.mFragmentManager = null;
+                                    if (f.mTargetWho != null) {
+                                        Fragment target = mActive.get(f.mTargetWho);
+                                        if (target != null && target.getRetainInstance()) {
+                                            // Only keep references to other retained Fragments
+                                            // to avoid developers accessing Fragments that
+                                            // are never coming back
+                                            f.mTarget = target;
+                                        }
+                                    }
                                 }
                             }
                         }
@@ -1338,6 +1347,11 @@
         mActive.put(f.mWho, null);
         removeRetainedFragment(f);
 
+        if (f.mTargetWho != null) {
+            // Restore the target Fragment so that it can be accessed
+            // even after the Fragment is removed.
+            f.mTarget = mActive.get(f.mTargetWho);
+        }
         f.initState();
     }
 
diff --git a/fragment/testing/build.gradle b/fragment/testing/build.gradle
index d0556a1..4786100 100644
--- a/fragment/testing/build.gradle
+++ b/fragment/testing/build.gradle
@@ -42,6 +42,7 @@
     androidTestImplementation(TEST_RUNNER)
     androidTestImplementation(TEST_RULES)
     androidTestImplementation(TRUTH)
+    androidTestImplementation(GUAVA_LISTENABLE_FUTURE_AVOID_CONFLICT)
 }
 
 supportLibrary {
diff --git a/graphics/drawable/static/build.gradle b/graphics/drawable/static/build.gradle
index 30bcd11..503a1f6 100644
--- a/graphics/drawable/static/build.gradle
+++ b/graphics/drawable/static/build.gradle
@@ -28,7 +28,7 @@
 supportLibrary {
     name = "Android Support VectorDrawable"
     publish = true
-    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenVersion = LibraryVersions.VECTORDRAWABLE
     mavenGroup = LibraryGroups.VECTORDRAWABLE
     inceptionYear = "2015"
     description = "Android Support VectorDrawable"
diff --git a/heifwriter/build.gradle b/heifwriter/build.gradle
index 7728023..d7d1f58 100644
--- a/heifwriter/build.gradle
+++ b/heifwriter/build.gradle
@@ -23,7 +23,7 @@
 supportLibrary {
     name = "Android Support HeifWriter"
     publish = true
-    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenVersion = LibraryVersions.HEIFWRITER
     mavenGroup = LibraryGroups.HEIFWRITER
     inceptionYear = "2018"
     description = "Android Support HeifWriter for writing HEIF still images"
diff --git a/interpolator/build.gradle b/interpolator/build.gradle
index 480d1a3..8ec605a 100644
--- a/interpolator/build.gradle
+++ b/interpolator/build.gradle
@@ -12,7 +12,7 @@
 supportLibrary {
     name = "Android Support Library Interpolators"
     publish = true
-    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenVersion = LibraryVersions.INTERPOLATOR
     mavenGroup = LibraryGroups.INTERPOLATOR
     inceptionYear = "2018"
     description = "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren't a part of the framework APIs. Compatible on devices running API 14 or later."
diff --git a/legacy/core-ui/build.gradle b/legacy/core-ui/build.gradle
index 5d5140b..1298d4e 100644
--- a/legacy/core-ui/build.gradle
+++ b/legacy/core-ui/build.gradle
@@ -23,7 +23,7 @@
 supportLibrary {
     name = "Android Support Library core UI"
     publish = true
-    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenVersion = LibraryVersions.LEGACY
     mavenGroup = LibraryGroups.LEGACY
     inceptionYear = "2011"
     description = "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren't a part of the framework APIs. Compatible on devices running API 14 or later."
diff --git a/legacy/core-utils/build.gradle b/legacy/core-utils/build.gradle
index c9303ef..10eb003 100644
--- a/legacy/core-utils/build.gradle
+++ b/legacy/core-utils/build.gradle
@@ -17,7 +17,7 @@
 supportLibrary {
     name = "Android Support Library core utils"
     publish = true
-    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenVersion = LibraryVersions.LEGACY
     mavenGroup = LibraryGroups.LEGACY
     inceptionYear = "2011"
     description = "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren't a part of the framework APIs. Compatible on devices running API 14 or later."
diff --git a/lifecycle/savedstate-core/api/1.0.0-alpha01.txt b/lifecycle/savedstate-core/api/1.0.0-alpha01.txt
index 678b337f..51da4ec 100644
--- a/lifecycle/savedstate-core/api/1.0.0-alpha01.txt
+++ b/lifecycle/savedstate-core/api/1.0.0-alpha01.txt
@@ -1,29 +1,35 @@
 // Signature format: 2.0
 package androidx.lifecycle {
 
-  public final class BundlableSavedStateRegistry implements androidx.lifecycle.SavedStateRegistry {
-    ctor public BundlableSavedStateRegistry();
-    method @MainThread public android.os.Bundle? consumeRestoredStateForKey(String);
-    method @MainThread public boolean isRestored();
+  public abstract class AbstractSavedStateRegistry<S> implements androidx.lifecycle.SavedStateRegistry<S> {
+    ctor public AbstractSavedStateRegistry();
+    method @MainThread public final S? consumeRestoredStateForKey(String);
+    method @MainThread public final boolean isRestored();
+    method @MainThread public final void registerSavedStateProvider(String, androidx.lifecycle.SavedStateRegistry.SavedStateProvider<S>);
+    method @MainThread protected final void restoreSavedState(java.util.Map<java.lang.String,S>?);
+    method @MainThread protected final java.util.Map<java.lang.String,S> saveState();
+    method @MainThread public final void unregisterSavedStateProvider(String);
+  }
+
+  public final class BundleSavedStateRegistry extends androidx.lifecycle.AbstractSavedStateRegistry<android.os.Bundle> {
+    ctor public BundleSavedStateRegistry();
     method @MainThread public void performRestore(android.os.Bundle?);
     method @MainThread public void performSave(android.os.Bundle);
-    method @MainThread public void registerSavedStateProvider(String, androidx.lifecycle.SavedStateRegistry.SavedStateProvider);
-    method @MainThread public void unregisterSavedStateProvider(String);
   }
 
-  public interface SavedStateRegistry {
-    method @MainThread public android.os.Bundle? consumeRestoredStateForKey(String);
+  public interface BundleSavedStateRegistryOwner {
+    method public androidx.lifecycle.SavedStateRegistry<android.os.Bundle> getBundleSavedStateRegistry();
+  }
+
+  public interface SavedStateRegistry<S> {
+    method @MainThread public S? consumeRestoredStateForKey(String);
     method @MainThread public boolean isRestored();
-    method @MainThread public void registerSavedStateProvider(String, androidx.lifecycle.SavedStateRegistry.SavedStateProvider);
+    method @MainThread public void registerSavedStateProvider(String, androidx.lifecycle.SavedStateRegistry.SavedStateProvider<S>);
     method @MainThread public void unregisterSavedStateProvider(String);
   }
 
-  public static interface SavedStateRegistry.SavedStateProvider {
-    method public android.os.Bundle saveState();
-  }
-
-  public interface SavedStateRegistryOwner {
-    method public androidx.lifecycle.SavedStateRegistry getSavedState();
+  public static interface SavedStateRegistry.SavedStateProvider<S> {
+    method public S saveState();
   }
 
 }
diff --git a/lifecycle/savedstate-core/api/current.txt b/lifecycle/savedstate-core/api/current.txt
index 678b337f..51da4ec 100644
--- a/lifecycle/savedstate-core/api/current.txt
+++ b/lifecycle/savedstate-core/api/current.txt
@@ -1,29 +1,35 @@
 // Signature format: 2.0
 package androidx.lifecycle {
 
-  public final class BundlableSavedStateRegistry implements androidx.lifecycle.SavedStateRegistry {
-    ctor public BundlableSavedStateRegistry();
-    method @MainThread public android.os.Bundle? consumeRestoredStateForKey(String);
-    method @MainThread public boolean isRestored();
+  public abstract class AbstractSavedStateRegistry<S> implements androidx.lifecycle.SavedStateRegistry<S> {
+    ctor public AbstractSavedStateRegistry();
+    method @MainThread public final S? consumeRestoredStateForKey(String);
+    method @MainThread public final boolean isRestored();
+    method @MainThread public final void registerSavedStateProvider(String, androidx.lifecycle.SavedStateRegistry.SavedStateProvider<S>);
+    method @MainThread protected final void restoreSavedState(java.util.Map<java.lang.String,S>?);
+    method @MainThread protected final java.util.Map<java.lang.String,S> saveState();
+    method @MainThread public final void unregisterSavedStateProvider(String);
+  }
+
+  public final class BundleSavedStateRegistry extends androidx.lifecycle.AbstractSavedStateRegistry<android.os.Bundle> {
+    ctor public BundleSavedStateRegistry();
     method @MainThread public void performRestore(android.os.Bundle?);
     method @MainThread public void performSave(android.os.Bundle);
-    method @MainThread public void registerSavedStateProvider(String, androidx.lifecycle.SavedStateRegistry.SavedStateProvider);
-    method @MainThread public void unregisterSavedStateProvider(String);
   }
 
-  public interface SavedStateRegistry {
-    method @MainThread public android.os.Bundle? consumeRestoredStateForKey(String);
+  public interface BundleSavedStateRegistryOwner {
+    method public androidx.lifecycle.SavedStateRegistry<android.os.Bundle> getBundleSavedStateRegistry();
+  }
+
+  public interface SavedStateRegistry<S> {
+    method @MainThread public S? consumeRestoredStateForKey(String);
     method @MainThread public boolean isRestored();
-    method @MainThread public void registerSavedStateProvider(String, androidx.lifecycle.SavedStateRegistry.SavedStateProvider);
+    method @MainThread public void registerSavedStateProvider(String, androidx.lifecycle.SavedStateRegistry.SavedStateProvider<S>);
     method @MainThread public void unregisterSavedStateProvider(String);
   }
 
-  public static interface SavedStateRegistry.SavedStateProvider {
-    method public android.os.Bundle saveState();
-  }
-
-  public interface SavedStateRegistryOwner {
-    method public androidx.lifecycle.SavedStateRegistry getSavedState();
+  public static interface SavedStateRegistry.SavedStateProvider<S> {
+    method public S saveState();
   }
 
 }
diff --git a/lifecycle/savedstate-core/build.gradle b/lifecycle/savedstate-core/build.gradle
index 62e3460..798f658 100644
--- a/lifecycle/savedstate-core/build.gradle
+++ b/lifecycle/savedstate-core/build.gradle
@@ -28,6 +28,10 @@
     api(SUPPORT_ANNOTATIONS)
     implementation(ARCH_CORE_COMMON)
 
+    testImplementation(JUNIT)
+    testImplementation(TEST_RUNNER)
+    testImplementation(KOTLIN_STDLIB)
+
     androidTestImplementation(JUNIT)
     androidTestImplementation(TEST_RUNNER)
     androidTestImplementation(KOTLIN_STDLIB)
diff --git a/lifecycle/savedstate-core/src/androidTest/java/androidx/lifecycle/BundlableSavedStateRegistryTest.kt b/lifecycle/savedstate-core/src/androidTest/java/androidx/lifecycle/BundlableSavedStateRegistryTest.kt
index 31e537a..3f36f50 100644
--- a/lifecycle/savedstate-core/src/androidTest/java/androidx/lifecycle/BundlableSavedStateRegistryTest.kt
+++ b/lifecycle/savedstate-core/src/androidTest/java/androidx/lifecycle/BundlableSavedStateRegistryTest.kt
@@ -19,38 +19,24 @@
 import android.os.Bundle
 import androidx.test.filters.SmallTest
 import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.CoreMatchers.nullValue
 import org.hamcrest.MatcherAssert.assertThat
-import org.junit.Assert
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
 
 @SmallTest
 @RunWith(JUnit4::class)
-class SavedStateRegistryTest {
-
-    @Test
-    fun registerWithSameKey() {
-        val registry = BundlableSavedStateRegistry()
-        registry.registerSavedStateProvider("key") { Bundle.EMPTY }
-        try {
-            registry.registerSavedStateProvider("key") { Bundle.EMPTY }
-            Assert.fail("can't register with the same key")
-        } catch (e: IllegalArgumentException) {
-            // fail as expected
-        }
-    }
+class BundlableSavedStateRegistryTest {
 
     @Test
     fun saveRestoreFlow() {
-        val registry = BundlableSavedStateRegistry()
+        val registry = BundleSavedStateRegistry()
         registry.registerSavedStateProvider("a") { bundleOf("foo", 1) }
         registry.registerSavedStateProvider("b") { bundleOf("foo", 2) }
         val state = Bundle()
         registry.performSave(state)
 
-        val newRegistry = BundlableSavedStateRegistry()
+        val newRegistry = BundleSavedStateRegistry()
         newRegistry.performRestore(state)
 
         assertThat(newRegistry.isRestored, `is`(true))
@@ -60,69 +46,6 @@
         assertThat(bundleForA.isSame(bundleOf("foo", 1)), `is`(true))
         assertThat(bundleForB.isSame(bundleOf("foo", 2)), `is`(true))
     }
-
-    @Test
-    fun consumeSameTwice() {
-        val registry = BundlableSavedStateRegistry()
-        registry.registerSavedStateProvider("a") { bundleOf("foo", 1) }
-        val state = Bundle()
-        registry.performSave(state)
-
-        val newStore = BundlableSavedStateRegistry()
-        newStore.performRestore(state)
-
-        assertThat(newStore.isRestored, `is`(true))
-        val bundleForA = newStore.consumeRestoredStateForKey("a")
-        assertThat(bundleForA.isSame(bundleOf("foo", 1)), `is`(true))
-        assertThat(newStore.consumeRestoredStateForKey("a"), nullValue())
-    }
-
-    @Test
-    fun unregister() {
-        val registry = BundlableSavedStateRegistry()
-        registry.registerSavedStateProvider("a") { bundleOf("foo", 1) }
-        registry.unregisterSavedStateProvider("a")
-        // this call should succeed
-        registry.registerSavedStateProvider("a") { bundleOf("foo", 2) }
-        registry.unregisterSavedStateProvider("a")
-        val state = Bundle()
-        registry.performRestore(state)
-        assertThat(state.isEmpty, `is`(true))
-    }
-
-    @Test
-    fun unconsumedSavedState() {
-        val registry = BundlableSavedStateRegistry()
-        registry.registerSavedStateProvider("a") { bundleOf("foo", 1) }
-        val savedState1 = Bundle()
-        registry.performSave(savedState1)
-        val intermediateStore = BundlableSavedStateRegistry()
-        intermediateStore.performRestore(savedState1)
-        val savedState2 = Bundle()
-        intermediateStore.performSave(savedState2)
-        val newRegistry = BundlableSavedStateRegistry()
-        newRegistry.performRestore(savedState2)
-        val bundleForA = newRegistry.consumeRestoredStateForKey("a")
-        assertThat(bundleForA.isSame(bundleOf("foo", 1)), `is`(true))
-    }
-
-    @Test
-    fun unconsumedSavedStateClashWithCallback() {
-        val registry = BundlableSavedStateRegistry()
-        registry.registerSavedStateProvider("a") { bundleOf("foo", 1) }
-        val savedState1 = Bundle()
-        registry.performSave(savedState1)
-        val intermediateStore = BundlableSavedStateRegistry()
-        // there is unconsumed value for "a"
-        intermediateStore.performRestore(savedState1)
-        intermediateStore.registerSavedStateProvider("a") { bundleOf("foo", 2) }
-        val savedState2 = Bundle()
-        intermediateStore.performSave(savedState2)
-        val newStore = BundlableSavedStateRegistry()
-        newStore.performRestore(savedState2)
-        val bundleForA = newStore.consumeRestoredStateForKey("a")
-        assertThat(bundleForA.isSame(bundleOf("foo", 2)), `is`(true))
-    }
 }
 
 private fun bundleOf(key: String, value: Int): Bundle {
diff --git a/lifecycle/savedstate-core/src/main/java/androidx/lifecycle/AbstractSavedStateRegistry.java b/lifecycle/savedstate-core/src/main/java/androidx/lifecycle/AbstractSavedStateRegistry.java
new file mode 100644
index 0000000..edd7b6e
--- /dev/null
+++ b/lifecycle/savedstate-core/src/main/java/androidx/lifecycle/AbstractSavedStateRegistry.java
@@ -0,0 +1,134 @@
+/*
+ * 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.lifecycle;
+
+import android.annotation.SuppressLint;
+import android.os.Bundle;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.arch.core.internal.SafeIterableMap;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * This class provides a skeletal implementation of the {@link SavedStateRegistry}.
+ * <p>
+ * Implementations simply need to call {@link #restoreSavedState(Map)} to initialize restored state
+ * and call {@link #saveState()} once system requests saved state.
+ *
+ * @param <S> represents a class for saving a state, typically it is {@link Bundle}
+ *
+ * @see BundleSavedStateRegistry
+ */
+@SuppressLint("RestrictedApi")
+public abstract class AbstractSavedStateRegistry<S> implements SavedStateRegistry<S> {
+    private SafeIterableMap<String, SavedStateProvider<S>> mComponents =
+            new SafeIterableMap<>();
+    private Map<String, S> mSavedState;
+    private boolean mRestored;
+
+    @MainThread
+    @Nullable
+    @Override
+    public final S consumeRestoredStateForKey(@NonNull String key) {
+        if (!mRestored) {
+            throw new IllegalStateException("You can consumeRestoredStateForKey "
+                    + "only after super.onCreate of corresponding component");
+        }
+        S state = null;
+        if (mSavedState != null) {
+            state = mSavedState.remove(key);
+            if (mSavedState.isEmpty()) {
+                mSavedState = null;
+            }
+        }
+        return state;
+    }
+
+    @MainThread
+    @Override
+    public final void registerSavedStateProvider(@NonNull String key,
+            @NonNull SavedStateProvider<S> provider) {
+        SavedStateProvider<S> previous = mComponents.putIfAbsent(key, provider);
+        if (previous != null) {
+            throw new IllegalArgumentException("SavedStateProvider with the given key is"
+                    + " already registered");
+        }
+    }
+
+    /**
+     * Unregisters a component previously registered by the given {@code key}
+     *
+     * @param key a key with which a component was previously registered.
+     */
+    @MainThread
+    @Override
+    public final void unregisterSavedStateProvider(@NonNull String key) {
+        mComponents.remove(key);
+    }
+
+    /**
+     * Returns if state was restored after creation and can be safely consumed
+     * with {@link #consumeRestoredStateForKey(String)}
+     *
+     * @return true if state was restored.
+     */
+    @MainThread
+    @Override
+    public final boolean isRestored() {
+        return mRestored;
+    }
+
+    /**
+     * Subclasses of this {@code AbstractSavedStateRegistry} should call this
+     * method to initialize restored state.
+     */
+    @SuppressWarnings("WeakerAccess")
+    @MainThread
+    protected final void restoreSavedState(@Nullable Map<String, S> initialState) {
+        if (initialState != null) {
+            mSavedState = new HashMap<>(initialState);
+        }
+        mRestored = true;
+    }
+
+    /**
+     * Subclasses of this {@code AbstractSavedStateRegistry} should call this
+     * method to perform state saving, this method will call all registered providers and
+     * merge a state provided by them with all unconsumed values since previous restoration.
+     *
+     * @return state that should be saved.
+     */
+    @MainThread
+    @NonNull
+    protected final Map<String, S> saveState() {
+        Map<String, S> savedState = new HashMap<>();
+        if (mSavedState != null) {
+            savedState.putAll(mSavedState);
+        }
+        for (Iterator<Map.Entry<String, SavedStateProvider<S>>> it =
+                mComponents.iteratorWithAdditions(); it.hasNext(); ) {
+            Map.Entry<String, SavedStateProvider<S>> entry = it.next();
+            savedState.put(entry.getKey(), entry.getValue().saveState());
+        }
+        return savedState;
+    }
+}
diff --git a/lifecycle/savedstate-core/src/main/java/androidx/lifecycle/BundlableSavedStateRegistry.java b/lifecycle/savedstate-core/src/main/java/androidx/lifecycle/BundlableSavedStateRegistry.java
deleted file mode 100644
index e744236..0000000
--- a/lifecycle/savedstate-core/src/main/java/androidx/lifecycle/BundlableSavedStateRegistry.java
+++ /dev/null
@@ -1,128 +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.lifecycle;
-
-import android.annotation.SuppressLint;
-import android.os.Bundle;
-
-import androidx.annotation.MainThread;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.arch.core.internal.SafeIterableMap;
-import androidx.lifecycle.SavedStateRegistry.SavedStateProvider;
-
-import java.util.Iterator;
-import java.util.Map;
-
-/**
- * A default implementation of {@link SavedStateRegistry} backed by {@link Bundle}.
- * <p>
- * An owner of this {@link BundlableSavedStateRegistry} must call {@link #performRestore(Bundle)}
- * once previously saved state becomes available to it.
- * <p>
- * To collect saved state supplied by {@link SavedStateProvider}
- * an owner should call {@link #performSave(Bundle)}
- */
-@SuppressLint("RestrictedApi")
-public final class BundlableSavedStateRegistry implements SavedStateRegistry {
-    private static final String SAVED_COMPONENTS_KEY =
-            "androidx.lifecycle.BundlableSavedStateRegistry.key";
-    private SafeIterableMap<String, SavedStateProvider> mComponents = new SafeIterableMap<>();
-    private Bundle mSavedState;
-    private boolean mRestored;
-
-    @MainThread
-    @Nullable
-    @Override
-    public Bundle consumeRestoredStateForKey(@NonNull String key) {
-        if (!mRestored) {
-            throw new IllegalStateException("You can consumeRestoredStateForKey "
-                    + "only after super.onCreate of corresponding component");
-        }
-        Bundle state = null;
-        if (mSavedState != null)  {
-            state = mSavedState.getBundle(key);
-            mSavedState.remove(key);
-            if (mSavedState.isEmpty()) {
-                mSavedState = null;
-            }
-        }
-        return state;
-    }
-
-    @MainThread
-    @Override
-    public void registerSavedStateProvider(@NonNull String key,
-            @NonNull SavedStateProvider provider) {
-        SavedStateProvider previous = mComponents.putIfAbsent(key, provider);
-        if (previous != null) {
-            throw new IllegalArgumentException("SavedStateProvider with the given key is"
-                    + " already registered");
-        }
-    }
-
-    /**
-     * Unregisters a component previously registered by the given {@code key}
-     *
-     * @param key a key with which a component was previously registered.
-     */
-    @MainThread
-    @Override
-    public void unregisterSavedStateProvider(@NonNull String key) {
-        mComponents.remove(key);
-    }
-
-    /**
-     * Returns if state was restored after creation and can be safely consumed
-     * with {@link #consumeRestoredStateForKey(String)}
-     * @return true if state was restored.
-     */
-    @MainThread
-    @Override
-    public boolean isRestored() {
-        return mRestored;
-    }
-
-    /**
-     * An interface for an owner of this @{code {@link SavedStateRegistry} to restore saved state.
-     * @param savedState restored state
-     */
-    @SuppressWarnings("WeakerAccess")
-    @MainThread
-    public void performRestore(@Nullable Bundle savedState) {
-        mSavedState = savedState != null ? savedState.getBundle(SAVED_COMPONENTS_KEY) : null;
-        mRestored = true;
-    }
-
-    /**
-     * An interface for an owner of this @{code {@link SavedStateRegistry}
-     * to perform state saving, it will call all registered providers and
-     * merge with unconsumed state.
-     *
-     * @param outBundle Bundle in which to place a saved state
-     */
-    @MainThread
-    public void performSave(@NonNull Bundle outBundle) {
-        Bundle res = mSavedState == null ? new Bundle() : new Bundle(mSavedState);
-        for (Iterator<Map.Entry<String, SavedStateProvider>> it =
-                mComponents.iteratorWithAdditions(); it.hasNext(); ) {
-            Map.Entry<String, SavedStateProvider> entry = it.next();
-            res.putBundle(entry.getKey(), entry.getValue().saveState());
-        }
-        outBundle.putBundle(SAVED_COMPONENTS_KEY, res);
-    }
-}
diff --git a/lifecycle/savedstate-core/src/main/java/androidx/lifecycle/BundleSavedStateRegistry.java b/lifecycle/savedstate-core/src/main/java/androidx/lifecycle/BundleSavedStateRegistry.java
new file mode 100644
index 0000000..da640dc
--- /dev/null
+++ b/lifecycle/savedstate-core/src/main/java/androidx/lifecycle/BundleSavedStateRegistry.java
@@ -0,0 +1,78 @@
+/*
+ * 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.lifecycle;
+
+import android.os.Bundle;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A default implementation of {@link SavedStateRegistry} backed by {@link Bundle}.
+ * <p>
+ * An owner of this {@link BundleSavedStateRegistry} must call {@link #performRestore(Bundle)}
+ * once previously saved state becomes available to it.
+ * <p>
+ * To collect saved state supplied by {@link SavedStateProvider}
+ * an owner should call {@link #performSave(Bundle)}
+ */
+public final class BundleSavedStateRegistry extends AbstractSavedStateRegistry<Bundle> {
+    private static final String SAVED_COMPONENTS_KEY =
+            "androidx.lifecycle.BundlableSavedStateRegistry.key";
+
+    /**
+     * An interface for an owner of this @{code {@link SavedStateRegistry} to restore saved state.
+     *
+     * @param savedState restored state
+     */
+    @SuppressWarnings("WeakerAccess")
+    @MainThread
+    public void performRestore(@Nullable Bundle savedState) {
+        Bundle componentsState = savedState != null ? savedState.getBundle(SAVED_COMPONENTS_KEY)
+                : null;
+        if (componentsState == null || componentsState.isEmpty()) {
+            restoreSavedState(null);
+            return;
+        }
+        Map<String, Bundle> initialState = new HashMap<>();
+        for (String key : componentsState.keySet()) {
+            initialState.put(key, componentsState.getBundle(key));
+        }
+        restoreSavedState(initialState);
+    }
+
+    /**
+     * An interface for an owner of this @{code {@link SavedStateRegistry}
+     * to perform state saving, it will call all registered providers and
+     * merge with unconsumed state.
+     *
+     * @param outBundle Bundle in which to place a saved state
+     */
+    @MainThread
+    public void performSave(@NonNull Bundle outBundle) {
+        Map<String, Bundle> bundleMap = saveState();
+        Bundle components = new Bundle();
+        for (Map.Entry<String, Bundle> entry : bundleMap.entrySet()) {
+            components.putBundle(entry.getKey(), entry.getValue());
+        }
+        outBundle.putBundle(SAVED_COMPONENTS_KEY, components);
+    }
+}
diff --git a/lifecycle/savedstate-core/src/main/java/androidx/lifecycle/SavedStateRegistryOwner.java b/lifecycle/savedstate-core/src/main/java/androidx/lifecycle/BundleSavedStateRegistryOwner.java
similarity index 86%
rename from lifecycle/savedstate-core/src/main/java/androidx/lifecycle/SavedStateRegistryOwner.java
rename to lifecycle/savedstate-core/src/main/java/androidx/lifecycle/BundleSavedStateRegistryOwner.java
index 9ac507b..d34382a 100644
--- a/lifecycle/savedstate-core/src/main/java/androidx/lifecycle/SavedStateRegistryOwner.java
+++ b/lifecycle/savedstate-core/src/main/java/androidx/lifecycle/BundleSavedStateRegistryOwner.java
@@ -17,17 +17,19 @@
 package androidx.lifecycle;
 
 
+import android.os.Bundle;
+
 import androidx.annotation.NonNull;
 
 /**
  * A scope that owns {@link SavedStateRegistry}
  */
-public interface SavedStateRegistryOwner {
+public interface BundleSavedStateRegistryOwner {
     /**
      * Returns owned {@link SavedStateRegistry}
      *
      * @return a {@link SavedStateRegistry}
      */
     @NonNull
-    SavedStateRegistry getSavedState();
+    SavedStateRegistry<Bundle> getBundleSavedStateRegistry();
 }
diff --git a/lifecycle/savedstate-core/src/main/java/androidx/lifecycle/SavedStateRegistry.java b/lifecycle/savedstate-core/src/main/java/androidx/lifecycle/SavedStateRegistry.java
index ade256e..af77b34 100644
--- a/lifecycle/savedstate-core/src/main/java/androidx/lifecycle/SavedStateRegistry.java
+++ b/lifecycle/savedstate-core/src/main/java/androidx/lifecycle/SavedStateRegistry.java
@@ -23,9 +23,11 @@
 import androidx.annotation.Nullable;
 
 /**
- * A class for managing saved state.
+ * An interface for plugging components that consumes and contributes to the saved state.
+ *
+ * @param <S> represents a class for saving a state, typically it is {@link Bundle}
  */
-public interface SavedStateRegistry {
+public interface SavedStateRegistry<S> {
     /**
      * Consumes saved state previously supplied by {@link SavedStateProvider} registered
      * via {@link #registerSavedStateProvider(String, SavedStateProvider)}
@@ -42,11 +44,11 @@
      * that a saved state can be safely consumed.
      *
      * @param key a key with which {@link SavedStateProvider} was previously registered.
-     * @return {@code Bundle} with the previously saved state or {@code null}
+     * @return {@code S} with the previously saved state or {@code null}
      */
     @MainThread
     @Nullable
-    Bundle consumeRestoredStateForKey(@NonNull String key);
+    S consumeRestoredStateForKey(@NonNull String key);
 
     /**
      * Returns if a state was restored and can be safely consumed
@@ -59,8 +61,10 @@
 
     /**
      * This interface marks a component that contributes to saved state.
+     *
+     * @param <S> represents a class for saving a state, typically it is {@link Bundle}
      */
-    interface SavedStateProvider {
+    interface SavedStateProvider<S> {
         /**
          * Called to retrieve a state from a component before being killed
          * so later the state can be received from {@link #consumeRestoredStateForKey(String)}
@@ -68,7 +72,7 @@
          * @return Bundle with your saved state.
          */
         @NonNull
-        Bundle saveState();
+        S saveState();
     }
 
     /**
@@ -87,7 +91,7 @@
      */
     @MainThread
     void registerSavedStateProvider(@NonNull String key,
-            @NonNull SavedStateProvider savedStateProvider);
+            @NonNull SavedStateProvider<S> savedStateProvider);
 
     /**
      * Unregisters a component previously registered by the given {@code key}
diff --git a/lifecycle/savedstate-core/src/test/java/androidx/lifecycle/AbstractSavedStateRegistryTest.kt b/lifecycle/savedstate-core/src/test/java/androidx/lifecycle/AbstractSavedStateRegistryTest.kt
new file mode 100644
index 0000000..61d8195
--- /dev/null
+++ b/lifecycle/savedstate-core/src/test/java/androidx/lifecycle/AbstractSavedStateRegistryTest.kt
@@ -0,0 +1,119 @@
+/*
+ * 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.lifecycle
+
+import androidx.test.filters.SmallTest
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.CoreMatchers.nullValue
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class AbstractSavedStateRegistryTest {
+
+    @Test
+    fun registerWithSameKey() {
+        val registry = TestSavedStateRegistry()
+        registry.registerSavedStateProvider("key") { "a" }
+        try {
+            registry.registerSavedStateProvider("key") { "b" }
+            Assert.fail("can't register with the same key")
+        } catch (e: IllegalArgumentException) {
+            // fail as expected
+        }
+    }
+
+    @Test
+    fun saveRestoreFlow() {
+        val registry = TestSavedStateRegistry()
+        registry.registerSavedStateProvider("a") { "bla" }
+        registry.registerSavedStateProvider("b") { "boo" }
+        val savedState = registry.savedState()
+
+        val newRegistry = TestSavedStateRegistry()
+        newRegistry.restoreState(savedState)
+
+        assertThat(newRegistry.isRestored, `is`(true))
+        assertThat(newRegistry.consumeRestoredStateForKey("a"), `is`("bla"))
+        assertThat(newRegistry.consumeRestoredStateForKey("b"), `is`("boo"))
+    }
+
+    @Test
+    fun consumeSameTwice() {
+        val registry = TestSavedStateRegistry()
+        registry.registerSavedStateProvider("a") { "foo" }
+        val state = registry.savedState()
+
+        val newStore = TestSavedStateRegistry()
+        newStore.restoreState(state)
+
+        assertThat(newStore.isRestored, `is`(true))
+        assertThat(newStore.consumeRestoredStateForKey("a"), `is`("foo"))
+        assertThat(newStore.consumeRestoredStateForKey("a"), nullValue())
+    }
+
+    @Test
+    fun unregister() {
+        val registry = TestSavedStateRegistry()
+        registry.registerSavedStateProvider("a") { "foo" }
+        registry.unregisterSavedStateProvider("a")
+        // this call should succeed
+        registry.registerSavedStateProvider("a") { "foo" }
+        registry.unregisterSavedStateProvider("a")
+
+        assertThat(registry.savedState().isEmpty(), `is`(true))
+    }
+
+    @Test
+    fun unconsumedSavedState() {
+        val registry = TestSavedStateRegistry()
+        registry.registerSavedStateProvider("a") { "foo" }
+        val savedState1 = registry.savedState()
+        val intermediateStore = TestSavedStateRegistry()
+        intermediateStore.restoreState(savedState1)
+        val savedState2 = intermediateStore.savedState()
+        val newRegistry = TestSavedStateRegistry()
+        newRegistry.restoreState(savedState2)
+        assertThat(newRegistry.consumeRestoredStateForKey("a"), `is`("foo"))
+    }
+
+    @Test
+    fun unconsumedSavedStateClashWithCallback() {
+        val registry = TestSavedStateRegistry()
+        registry.registerSavedStateProvider("a") { "foo" }
+        val savedState1 = registry.savedState()
+        val intermediateStore = TestSavedStateRegistry()
+        intermediateStore.restoreState(savedState1)
+        // there is unconsumed value for "a"
+        intermediateStore.registerSavedStateProvider("a") { "bar" }
+        val savedState2 = intermediateStore.savedState()
+        val newStore = TestSavedStateRegistry()
+        newStore.restoreState(savedState2)
+        assertThat(newStore.consumeRestoredStateForKey("a"), `is`("bar"))
+    }
+
+    class TestSavedStateRegistry : AbstractSavedStateRegistry<String>() {
+        fun restoreState(state: Map<String, String>) {
+            restoreSavedState(state)
+        }
+        fun savedState() = saveState()
+    }
+}
diff --git a/lifecycle/savedstate-fragment/src/androidTest/java/androidx/lifecycle/savedstate/SavedStateRegistriesTest.java b/lifecycle/savedstate-fragment/src/androidTest/java/androidx/lifecycle/savedstate/SavedStateRegistriesTest.java
index ab6003e..57801ac 100644
--- a/lifecycle/savedstate-fragment/src/androidTest/java/androidx/lifecycle/savedstate/SavedStateRegistriesTest.java
+++ b/lifecycle/savedstate-fragment/src/androidTest/java/androidx/lifecycle/savedstate/SavedStateRegistriesTest.java
@@ -27,7 +27,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.arch.core.util.Function;
-import androidx.lifecycle.BundlableSavedStateRegistry;
+import androidx.lifecycle.BundleSavedStateRegistry;
 import androidx.lifecycle.Lifecycle;
 import androidx.lifecycle.LifecycleObserver;
 import androidx.lifecycle.LifecycleOwner;
@@ -62,7 +62,7 @@
         return Arrays.asList(FRAGMENT_MODE, ACTIVITY_MODE);
     }
 
-    private BundlableSavedStateRegistry testedSavedStateRegistry(
+    private BundleSavedStateRegistry testedSavedStateRegistry(
             SavedStateActivity currentActivity) {
         if (FRAGMENT_MODE.equals(mode)) {
             return SavedStateRegistries.of(currentActivity.getFragment());
@@ -100,7 +100,7 @@
                 Lifecycle.State currentState = testedLifecycleOwner(activity)
                         .getLifecycle().getCurrentState();
                 assertThat(currentState.isAtLeast(Lifecycle.State.CREATED), is(true));
-                BundlableSavedStateRegistry store = testedSavedStateRegistry(activity);
+                BundleSavedStateRegistry store = testedSavedStateRegistry(activity);
                 assertThat(store.consumeRestoredStateForKey(CALLBACK_KEY), nullValue());
                 testedSavedStateRegistry(activity)
                         .registerSavedStateProvider(CALLBACK_KEY, new DefaultProvider());
@@ -146,9 +146,9 @@
         SavedStateActivity activity = initializeSavedState();
 
         SavedStateActivity.duringOnCreate(FRAGMENT_MODE.equals(mode),
-                new Function<BundlableSavedStateRegistry, Void>() {
+                new Function<BundleSavedStateRegistry, Void>() {
                     @Override
-                    public Void apply(BundlableSavedStateRegistry store) {
+                    public Void apply(BundleSavedStateRegistry store) {
                         checkDefaultSavedState(store);
                         return null;
                     }
@@ -156,7 +156,7 @@
         recreateActivity(activity, mActivityRule);
     }
 
-    private static class DefaultProvider implements SavedStateRegistry.SavedStateProvider {
+    private static class DefaultProvider implements SavedStateRegistry.SavedStateProvider<Bundle> {
         @NonNull
         @Override
         public Bundle saveState() {
@@ -166,7 +166,7 @@
         }
     }
 
-    private static void checkDefaultSavedState(BundlableSavedStateRegistry store) {
+    private static void checkDefaultSavedState(BundleSavedStateRegistry store) {
         Bundle savedState = store.consumeRestoredStateForKey(CALLBACK_KEY);
         assertThat(savedState, notNullValue());
         //noinspection ConstantConditions
diff --git a/lifecycle/savedstate-fragment/src/androidTest/java/androidx/lifecycle/savedstate/activity/SavedStateActivity.java b/lifecycle/savedstate-fragment/src/androidTest/java/androidx/lifecycle/savedstate/activity/SavedStateActivity.java
index b9dc95d..26e6c63 100644
--- a/lifecycle/savedstate-fragment/src/androidTest/java/androidx/lifecycle/savedstate/activity/SavedStateActivity.java
+++ b/lifecycle/savedstate-fragment/src/androidTest/java/androidx/lifecycle/savedstate/activity/SavedStateActivity.java
@@ -23,29 +23,29 @@
 import androidx.arch.core.util.Function;
 import androidx.fragment.app.Fragment;
 import androidx.fragment.app.FragmentActivity;
-import androidx.lifecycle.BundlableSavedStateRegistry;
+import androidx.lifecycle.BundleSavedStateRegistry;
 import androidx.lifecycle.SavedStateRegistries;
 
 public class SavedStateActivity extends FragmentActivity {
     private static final String TAG = "fragment";
 
-    private static Function<BundlableSavedStateRegistry, Void> sOnCreateRunnable;
+    private static Function<BundleSavedStateRegistry, Void> sOnCreateRunnable;
     private static boolean sInFragment;
 
     public static void duringOnCreate(boolean inFragment,
-            Function<BundlableSavedStateRegistry, Void> f) {
+            Function<BundleSavedStateRegistry, Void> f) {
         sInFragment = inFragment;
         sOnCreateRunnable = f;
     }
 
-    private static void shotOnCreateRunnable(BundlableSavedStateRegistry store) {
+    private static void shotOnCreateRunnable(BundleSavedStateRegistry store) {
         if (sOnCreateRunnable != null) {
             sOnCreateRunnable.apply(store);
             sOnCreateRunnable = null;
         }
     }
 
-    private BundlableSavedStateRegistry mSavedStateStore;
+    private BundleSavedStateRegistry mSavedStateStore;
 
     public SavedStateActivity() {
         if (!sInFragment && sOnCreateRunnable != null) {
@@ -70,7 +70,7 @@
     }
 
     public static class FragmentWithRunnable extends Fragment {
-        private BundlableSavedStateRegistry mSavedStateStore;
+        private BundleSavedStateRegistry mSavedStateStore;
         @SuppressWarnings("WeakerAccess")
         public FragmentWithRunnable() {
             if (sInFragment && sOnCreateRunnable != null) {
diff --git a/lifecycle/savedstate-fragment/src/main/java/androidx/lifecycle/SavedStateHolderFragmentController.java b/lifecycle/savedstate-fragment/src/main/java/androidx/lifecycle/SavedStateHolderFragmentController.java
index 40a8653..05a74d8 100644
--- a/lifecycle/savedstate-fragment/src/main/java/androidx/lifecycle/SavedStateHolderFragmentController.java
+++ b/lifecycle/savedstate-fragment/src/main/java/androidx/lifecycle/SavedStateHolderFragmentController.java
@@ -42,7 +42,7 @@
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     public static class SavedStateHolderFragment extends Fragment {
 
-        private BundlableSavedStateRegistry mRegistry;
+        private BundleSavedStateRegistry mRegistry;
 
         @SuppressWarnings({"WeakerAccess", "unused"})
         public SavedStateHolderFragment() {
@@ -50,7 +50,7 @@
 
         @SuppressLint("ValidFragment")
         @SuppressWarnings("WeakerAccess")
-        SavedStateHolderFragment(BundlableSavedStateRegistry registry) {
+        SavedStateHolderFragment(BundleSavedStateRegistry registry) {
             mRegistry = registry;
         }
 
@@ -62,14 +62,14 @@
                         getParentFragment() != null ? getParentFragment() : getActivity());
             }
             if (mRegistry == null) {
-                mRegistry = new BundlableSavedStateRegistry();
+                mRegistry = new BundleSavedStateRegistry();
             }
             if (!mRegistry.isRestored()) {
                 mRegistry.performRestore(savedInstanceState);
             }
         }
 
-        BundlableSavedStateRegistry getSavedStateStore() {
+        BundleSavedStateRegistry getSavedStateStore() {
             return mRegistry;
         }
 
@@ -90,7 +90,7 @@
     static final String HOLDER_TAG = "androidx.lifecycle.SavedStateHolderFragment";
 
     @SuppressWarnings("WeakerAccess")
-    Map<LifecycleOwner, BundlableSavedStateRegistry> mNotCommittedStores = new HashMap<>();
+    Map<LifecycleOwner, BundleSavedStateRegistry> mNotCommittedStores = new HashMap<>();
 
     @SuppressWarnings("WeakerAccess")
     static SavedStateHolderFragment findHolderFragment(FragmentManager manager) {
@@ -106,10 +106,10 @@
         return (SavedStateHolderFragment) fragmentByTag;
     }
 
-    private BundlableSavedStateRegistry createHolderFragment(
+    private BundleSavedStateRegistry createHolderFragment(
             final FragmentManagerCall fragmentManager, LifecycleOwner owner) {
 
-        final BundlableSavedStateRegistry registry = new BundlableSavedStateRegistry();
+        final BundleSavedStateRegistry registry = new BundleSavedStateRegistry();
 
         if (fragmentManager.call() != null) {
             registry.performRestore(null);
@@ -138,7 +138,7 @@
                 }
                 if (event == Lifecycle.Event.ON_DESTROY) {
                     source.getLifecycle().removeObserver(this);
-                    BundlableSavedStateRegistry notCommitted = mNotCommittedStores.remove(source);
+                    BundleSavedStateRegistry notCommitted = mNotCommittedStores.remove(source);
                     if (notCommitted != null) {
                         Log.e(LOG_TAG, "Failed to save a SavedStateComponents for " + source);
                     }
@@ -148,7 +148,7 @@
         return registry;
     }
 
-    private BundlableSavedStateRegistry savedStateRegistry(FragmentManagerCall fragmentManager,
+    private BundleSavedStateRegistry savedStateRegistry(FragmentManagerCall fragmentManager,
             LifecycleOwner owner) {
         FragmentManager fm = fragmentManager.call();
         if (fm != null) {
@@ -157,7 +157,7 @@
                 return holder.getSavedStateStore();
             }
         }
-        BundlableSavedStateRegistry store = mNotCommittedStores.get(owner);
+        BundleSavedStateRegistry store = mNotCommittedStores.get(owner);
         if (store != null) {
             return store;
         }
@@ -165,11 +165,11 @@
         return createHolderFragment(fragmentManager, owner);
     }
 
-    static BundlableSavedStateRegistry savedStateRegistry(FragmentActivity activity) {
+    static BundleSavedStateRegistry savedStateRegistry(FragmentActivity activity) {
         return sHolderFragmentManager.savedStateRegistry(fragmentManager(activity), activity);
     }
 
-    static BundlableSavedStateRegistry savedStateRegistry(Fragment parentFragment) {
+    static BundleSavedStateRegistry savedStateRegistry(Fragment parentFragment) {
         return sHolderFragmentManager.savedStateRegistry(fragmentManager(parentFragment),
                 parentFragment);
     }
diff --git a/lifecycle/savedstate-fragment/src/main/java/androidx/lifecycle/SavedStateRegistries.java b/lifecycle/savedstate-fragment/src/main/java/androidx/lifecycle/SavedStateRegistries.java
index 9578108..44f7f9d 100644
--- a/lifecycle/savedstate-fragment/src/main/java/androidx/lifecycle/SavedStateRegistries.java
+++ b/lifecycle/savedstate-fragment/src/main/java/androidx/lifecycle/SavedStateRegistries.java
@@ -21,7 +21,7 @@
 import androidx.fragment.app.FragmentActivity;
 
 /**
- * Provides simple accessor for {@link BundlableSavedStateRegistry} of Activity and Fragments
+ * Provides simple accessor for {@link BundleSavedStateRegistry} of Activity and Fragments
  */
 public class SavedStateRegistries {
 
@@ -29,20 +29,20 @@
     }
 
     /**
-     * Returns {@link BundlableSavedStateRegistry} for the given fragment.
-     * @param fragment a fragment whose {@link BundlableSavedStateRegistry} is requested
+     * Returns {@link BundleSavedStateRegistry} for the given fragment.
+     * @param fragment a fragment whose {@link BundleSavedStateRegistry} is requested
      * @return a {@code SavedStateStore}
      */
-    public static BundlableSavedStateRegistry of(Fragment fragment) {
+    public static BundleSavedStateRegistry of(Fragment fragment) {
         return SavedStateHolderFragmentController.savedStateRegistry(fragment);
     }
 
     /**
-     * Returns {@link BundlableSavedStateRegistry} for the given activity.
-     * @param activity a activity whose {@link BundlableSavedStateRegistry} is requested
+     * Returns {@link BundleSavedStateRegistry} for the given activity.
+     * @param activity a activity whose {@link BundleSavedStateRegistry} is requested
      * @return a {@code SavedStateStore}
      */
-    public static BundlableSavedStateRegistry of(FragmentActivity activity) {
+    public static BundleSavedStateRegistry of(FragmentActivity activity) {
         return SavedStateHolderFragmentController.savedStateRegistry(activity);
     }
 }
diff --git a/lifecycle/viewmodel-fragment/src/main/java/androidx/lifecycle/VMSavedStateInitializer.java b/lifecycle/viewmodel-fragment/src/main/java/androidx/lifecycle/VMSavedStateInitializer.java
index 8cf0d1b..4bae785 100644
--- a/lifecycle/viewmodel-fragment/src/main/java/androidx/lifecycle/VMSavedStateInitializer.java
+++ b/lifecycle/viewmodel-fragment/src/main/java/androidx/lifecycle/VMSavedStateInitializer.java
@@ -89,7 +89,7 @@
     }
 
     @SuppressWarnings("WeakerAccess")
-    static void attach(BundlableSavedStateRegistry savedStateStore, ViewModelStoreOwner store) {
+    static void attach(BundleSavedStateRegistry savedStateStore, ViewModelStoreOwner store) {
         ViewModelStore viewModelStore = store.getViewModelStore();
         for (String key : viewModelStore.keys()) {
             ViewModel viewModel = viewModelStore.get(key);
diff --git a/lifecycle/viewmodel-fragment/src/main/java/androidx/lifecycle/ViewModelsWithStateFactories.java b/lifecycle/viewmodel-fragment/src/main/java/androidx/lifecycle/ViewModelsWithStateFactories.java
index dc56975..cc4a339 100644
--- a/lifecycle/viewmodel-fragment/src/main/java/androidx/lifecycle/ViewModelsWithStateFactories.java
+++ b/lifecycle/viewmodel-fragment/src/main/java/androidx/lifecycle/ViewModelsWithStateFactories.java
@@ -136,7 +136,7 @@
 
 
     static class FragmentVmFactory extends SavedStateVMFactory {
-        FragmentVmFactory(Application app, BundlableSavedStateRegistry savedStateStore,
+        FragmentVmFactory(Application app, BundleSavedStateRegistry savedStateStore,
                 ViewModelWithStateFactory factory, Bundle initialArgs) {
             super(savedStateStore, initialArgs, factory);
             VMSavedStateInitializer.initializeIfNeeded(app);
diff --git a/lifecycle/viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateAccessorHolder.java b/lifecycle/viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateAccessorHolder.java
index 2c3e3d6..c4ff4d1 100644
--- a/lifecycle/viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateAccessorHolder.java
+++ b/lifecycle/viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateAccessorHolder.java
@@ -27,7 +27,7 @@
 import java.util.Map;
 import java.util.Set;
 
-class SavedStateAccessorHolder implements SavedStateRegistry.SavedStateProvider {
+class SavedStateAccessorHolder implements SavedStateRegistry.SavedStateProvider<Bundle> {
     private static final String VALUES = "values";
     private static final String KEYS = "keys";
 
diff --git a/lifecycle/viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandle.java b/lifecycle/viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandle.java
index 2f4591e..85e90fe 100644
--- a/lifecycle/viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandle.java
+++ b/lifecycle/viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandle.java
@@ -45,7 +45,7 @@
         return mHolder.savedStateAccessor();
     }
 
-    SavedStateRegistry.SavedStateProvider savedStateComponent() {
+    SavedStateRegistry.SavedStateProvider<Bundle> savedStateComponent() {
         return mHolder;
     }
 
diff --git a/lifecycle/viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateVMFactory.java b/lifecycle/viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateVMFactory.java
index 1dbb539..5fbba45 100644
--- a/lifecycle/viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateVMFactory.java
+++ b/lifecycle/viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateVMFactory.java
@@ -24,10 +24,10 @@
     static final String TAG_SAVED_STATE_HANDLE = "androidx.lifecycle.savedstate.vm.tag";
 
     private final ViewModelWithStateFactory mWrappedFactory;
-    private final BundlableSavedStateRegistry mSavedStateStore;
+    private final SavedStateRegistry<Bundle> mSavedStateStore;
     private final Bundle mInitialArgs;
 
-    SavedStateVMFactory(BundlableSavedStateRegistry savedStateStore, Bundle initialArgs,
+    SavedStateVMFactory(SavedStateRegistry<Bundle> savedStateStore, Bundle initialArgs,
             ViewModelWithStateFactory factory) {
         mWrappedFactory = factory;
         mSavedStateStore = savedStateStore;
diff --git a/loader/api/1.1.0-alpha01.txt b/loader/api/1.1.0-alpha01.txt
new file mode 100644
index 0000000..1bc56b87
--- /dev/null
+++ b/loader/api/1.1.0-alpha01.txt
@@ -0,0 +1,102 @@
+// Signature format: 2.0
+package androidx.loader.app {
+
+  public abstract class LoaderManager {
+    ctor public LoaderManager();
+    method @MainThread public abstract void destroyLoader(int);
+    method @Deprecated public abstract void dump(String!, java.io.FileDescriptor!, java.io.PrintWriter!, String[]!);
+    method public static void enableDebugLogging(boolean);
+    method public static <T extends androidx.lifecycle.LifecycleOwner & androidx.lifecycle.ViewModelStoreOwner> androidx.loader.app.LoaderManager getInstance(T);
+    method public abstract <D> androidx.loader.content.Loader<D>? getLoader(int);
+    method public boolean hasRunningLoaders();
+    method @MainThread public abstract <D> androidx.loader.content.Loader<D> initLoader(int, android.os.Bundle?, androidx.loader.app.LoaderManager.LoaderCallbacks<D>);
+    method public abstract void markForRedelivery();
+    method @MainThread public abstract <D> androidx.loader.content.Loader<D> restartLoader(int, android.os.Bundle?, androidx.loader.app.LoaderManager.LoaderCallbacks<D>);
+  }
+
+  public static interface LoaderManager.LoaderCallbacks<D> {
+    method @MainThread public androidx.loader.content.Loader<D> onCreateLoader(int, android.os.Bundle?);
+    method @MainThread public void onLoadFinished(androidx.loader.content.Loader<D>, D!);
+    method @MainThread public void onLoaderReset(androidx.loader.content.Loader<D>);
+  }
+
+}
+
+package androidx.loader.content {
+
+  public abstract class AsyncTaskLoader<D> extends androidx.loader.content.Loader<D> {
+    ctor public AsyncTaskLoader(android.content.Context);
+    method public void cancelLoadInBackground();
+    method protected java.util.concurrent.Executor getExecutor();
+    method public boolean isLoadInBackgroundCanceled();
+    method public abstract D? loadInBackground();
+    method public void onCanceled(D?);
+    method protected D? onLoadInBackground();
+    method public void setUpdateThrottle(long);
+  }
+
+  public class CursorLoader extends androidx.loader.content.AsyncTaskLoader<android.database.Cursor> {
+    ctor public CursorLoader(android.content.Context);
+    ctor public CursorLoader(android.content.Context, android.net.Uri, String[]?, String?, String[]?, String?);
+    method public void deliverResult(android.database.Cursor!);
+    method public String[]? getProjection();
+    method public String? getSelection();
+    method public String[]? getSelectionArgs();
+    method public String? getSortOrder();
+    method public android.net.Uri getUri();
+    method public android.database.Cursor! loadInBackground();
+    method public void onCanceled(android.database.Cursor!);
+    method public void setProjection(String[]?);
+    method public void setSelection(String?);
+    method public void setSelectionArgs(String[]?);
+    method public void setSortOrder(String?);
+    method public void setUri(android.net.Uri);
+  }
+
+  public class Loader<D> {
+    ctor public Loader(android.content.Context);
+    method @MainThread public void abandon();
+    method @MainThread public boolean cancelLoad();
+    method public void commitContentChanged();
+    method public String dataToString(D?);
+    method @MainThread public void deliverCancellation();
+    method @MainThread public void deliverResult(D?);
+    method @Deprecated public void dump(String!, java.io.FileDescriptor!, java.io.PrintWriter!, String[]!);
+    method @MainThread public void forceLoad();
+    method public android.content.Context getContext();
+    method public int getId();
+    method public boolean isAbandoned();
+    method public boolean isReset();
+    method public boolean isStarted();
+    method @MainThread protected void onAbandon();
+    method @MainThread protected boolean onCancelLoad();
+    method @MainThread public void onContentChanged();
+    method @MainThread protected void onForceLoad();
+    method @MainThread protected void onReset();
+    method @MainThread protected void onStartLoading();
+    method @MainThread protected void onStopLoading();
+    method @MainThread public void registerListener(int, androidx.loader.content.Loader.OnLoadCompleteListener<D>);
+    method @MainThread public void registerOnLoadCanceledListener(androidx.loader.content.Loader.OnLoadCanceledListener<D>);
+    method @MainThread public void reset();
+    method public void rollbackContentChanged();
+    method @MainThread public final void startLoading();
+    method @MainThread public void stopLoading();
+    method public boolean takeContentChanged();
+    method @MainThread public void unregisterListener(androidx.loader.content.Loader.OnLoadCompleteListener<D>);
+    method @MainThread public void unregisterOnLoadCanceledListener(androidx.loader.content.Loader.OnLoadCanceledListener<D>);
+  }
+
+  public final class Loader.ForceLoadContentObserver extends android.database.ContentObserver {
+    ctor public Loader.ForceLoadContentObserver();
+  }
+
+  public static interface Loader.OnLoadCanceledListener<D> {
+    method public void onLoadCanceled(androidx.loader.content.Loader<D>);
+  }
+
+  public static interface Loader.OnLoadCompleteListener<D> {
+    method public void onLoadComplete(androidx.loader.content.Loader<D>, D?);
+  }
+
+}
+
diff --git a/loader/api/current.txt b/loader/api/current.txt
index ba1f38c..1bc56b87 100644
--- a/loader/api/current.txt
+++ b/loader/api/current.txt
@@ -27,6 +27,7 @@
   public abstract class AsyncTaskLoader<D> extends androidx.loader.content.Loader<D> {
     ctor public AsyncTaskLoader(android.content.Context);
     method public void cancelLoadInBackground();
+    method protected java.util.concurrent.Executor getExecutor();
     method public boolean isLoadInBackgroundCanceled();
     method public abstract D? loadInBackground();
     method public void onCanceled(D?);
diff --git a/loader/build.gradle b/loader/build.gradle
index 73d8803..c927d80 100644
--- a/loader/build.gradle
+++ b/loader/build.gradle
@@ -22,7 +22,7 @@
 supportLibrary {
     name = "Android Support Library loader"
     publish = true
-    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenVersion = LibraryVersions.LOADER
     mavenGroup = LibraryGroups.LOADER
     inceptionYear = "2011"
     description = "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren\'t a part of the framework APIs. Compatible on devices running API 14 or later."
diff --git a/loader/src/androidTest/java/androidx/loader/content/AsyncTaskLoaderTest.java b/loader/src/androidTest/java/androidx/loader/content/AsyncTaskLoaderTest.java
new file mode 100644
index 0000000..4881c83
--- /dev/null
+++ b/loader/src/androidTest/java/androidx/loader/content/AsyncTaskLoaderTest.java
@@ -0,0 +1,113 @@
+/*
+ * 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.loader.content;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class AsyncTaskLoaderTest {
+
+    @Test
+    public void testForceLoad_runsAsyncTask() throws InterruptedException {
+        TestAsyncTaskLoader loader = new TestAsyncTaskLoader(1);
+        loader.forceLoad();
+
+        assertTrue(loader.mLoadInBackgoundLatch.await(1, TimeUnit.SECONDS));
+    }
+
+    @Test
+    public void testForceLoad_runsOnCustomExecutor() throws InterruptedException {
+        final CountDownLatch executorLatch = new CountDownLatch(1);
+        TestAsyncTaskLoader loader = new TestAsyncTaskLoader(1);
+        loader.mExecutor = new Executor() {
+            @Override
+            public void execute(Runnable command) {
+                executorLatch.countDown();
+                command.run();
+            }
+        };
+        loader.forceLoad();
+
+        assertTrue(executorLatch.await(1, TimeUnit.SECONDS));
+    }
+
+    @Test
+    public void testGetExecutor_notCalledOnConstruction() {
+        TestAsyncTaskLoader loader = new TestAsyncTaskLoader(1);
+
+        assertEquals(0, loader.mGetExecutorCallCount);
+    }
+
+    @Test
+    public void testGetExecutor_forceLoadMultipleTimes_getExecutorCalledOnce()
+            throws InterruptedException {
+        TestAsyncTaskLoader loader = new TestAsyncTaskLoader(3);
+        final AtomicInteger loadCount = new AtomicInteger(3);
+        loader.registerListener(0, new Loader.OnLoadCompleteListener<Void>() {
+            @Override
+            public void onLoadComplete(Loader<Void> loader, Void data) {
+                if (loadCount.getAndDecrement() > 0) {
+                    loader.forceLoad();
+                }
+            }
+        });
+        loader.forceLoad();
+
+        assertTrue(loader.mLoadInBackgoundLatch.await(1, TimeUnit.SECONDS));
+        assertEquals(1, loader.mGetExecutorCallCount);
+    }
+
+    private static class TestAsyncTaskLoader extends AsyncTaskLoader<Void> {
+        final CountDownLatch mLoadInBackgoundLatch;
+        Executor mExecutor = null;
+        int mGetExecutorCallCount = 0;
+
+        TestAsyncTaskLoader(int latchCount) {
+            super(InstrumentationRegistry.getContext());
+            mLoadInBackgoundLatch = new CountDownLatch(latchCount);
+        }
+
+        @Override
+        public Void loadInBackground() {
+            mLoadInBackgoundLatch.countDown();
+            return null;
+        }
+
+        @Override
+        protected Executor getExecutor() {
+            mGetExecutorCallCount += 1;
+            if (mExecutor != null) {
+                return mExecutor;
+            }
+            return super.getExecutor();
+        }
+    }
+}
diff --git a/loader/src/main/java/androidx/loader/content/AsyncTaskLoader.java b/loader/src/main/java/androidx/loader/content/AsyncTaskLoader.java
index bbc14ea..fae3fba 100644
--- a/loader/src/main/java/androidx/loader/content/AsyncTaskLoader.java
+++ b/loader/src/main/java/androidx/loader/content/AsyncTaskLoader.java
@@ -117,7 +117,7 @@
         }
     }
 
-    private final Executor mExecutor;
+    private Executor mExecutor;
 
     volatile LoadTask mTask;
     volatile LoadTask mCancellingTask;
@@ -127,12 +127,7 @@
     Handler mHandler;
 
     public AsyncTaskLoader(@NonNull Context context) {
-        this(context, AsyncTask.THREAD_POOL_EXECUTOR);
-    }
-
-    private AsyncTaskLoader(@NonNull Context context, @NonNull Executor executor) {
         super(context);
-        mExecutor = executor;
     }
 
     /**
@@ -227,6 +222,9 @@
                 }
             }
             if (DEBUG) Log.v(TAG, "Executing: " + mTask);
+            if (mExecutor == null) {
+                mExecutor = getExecutor();
+            }
             mTask.executeOnExecutor(mExecutor);
         }
     }
@@ -335,6 +333,22 @@
     }
 
     /**
+     * Returns the {@link Executor} to use for this {@link Loader}'s {@link AsyncTask}s.
+     * By default {@link AsyncTask#THREAD_POOL_EXECUTOR} will be used.
+     *
+     * Override this method to return a custom executor. Note that this method will only be called
+     * once before this {@link Loader}'s first {@link AsyncTask} is run. It is up to the
+     * {@link Loader} to shut down the {@link Executor} at the appropriate place
+     * (e.g. in {@link #onAbandon()}) if necessary.
+     *
+     * @return the {@link Executor} to use for this {@link Loader}'s {@link AsyncTask}s.
+     */
+    @NonNull
+    protected Executor getExecutor() {
+        return AsyncTask.THREAD_POOL_EXECUTOR;
+    }
+
+    /**
      * Locks the current thread until the loader completes the current load
      * operation. Returns immediately if there is no load operation running.
      * Should not be called from the UI thread: calling it from the UI
diff --git a/localbroadcastmanager/build.gradle b/localbroadcastmanager/build.gradle
index 451fada..c97f62f 100644
--- a/localbroadcastmanager/build.gradle
+++ b/localbroadcastmanager/build.gradle
@@ -12,7 +12,7 @@
 supportLibrary {
     name = "Android Support Library Local Broadcast Manager"
     publish = true
-    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenVersion = LibraryVersions.LOCALBROADCASTMANAGER
     mavenGroup = LibraryGroups.LOCALBROADCASTMANAGER
     inceptionYear = "2018"
     description = "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren't a part of the framework APIs. Compatible on devices running API 14 or later."
diff --git a/media-widget/api/1.0.0-alpha5.txt b/media-widget/api/1.0.0-alpha5.txt
index bc36437..b803bef 100644
--- a/media-widget/api/1.0.0-alpha5.txt
+++ b/media-widget/api/1.0.0-alpha5.txt
@@ -5,35 +5,29 @@
     ctor public MediaControlView2(android.content.Context);
     ctor public MediaControlView2(android.content.Context, android.util.AttributeSet?);
     ctor public MediaControlView2(android.content.Context, android.util.AttributeSet?, int);
-    method public boolean checkLayoutParams(android.view.ViewGroup.LayoutParams!);
-    method public android.view.ViewGroup.LayoutParams! generateDefaultLayoutParams();
-    method public android.view.ViewGroup.LayoutParams! generateLayoutParams(android.view.ViewGroup.LayoutParams!);
     method public void onMeasure(int, int);
     method public void requestPlayButtonFocus();
-    method public void setMediaSessionToken2(androidx.media2.SessionToken2!);
-    method public void setOnFullScreenListener(androidx.media.widget.MediaControlView2.OnFullScreenListener!);
+    method public void setMediaSessionToken2(androidx.media2.SessionToken2);
+    method public void setOnFullScreenListener(androidx.media.widget.MediaControlView2.OnFullScreenListener);
   }
 
   public static interface MediaControlView2.OnFullScreenListener {
-    method public void onFullScreen(android.view.View!, boolean);
+    method public void onFullScreen(android.view.View, boolean);
   }
 
   public class VideoView2 extends android.view.ViewGroup {
     ctor public VideoView2(android.content.Context);
     ctor public VideoView2(android.content.Context, android.util.AttributeSet?);
     ctor public VideoView2(android.content.Context, android.util.AttributeSet?, int);
-    method public boolean checkLayoutParams(android.view.ViewGroup.LayoutParams!);
-    method public android.view.ViewGroup.LayoutParams! generateDefaultLayoutParams();
-    method public android.view.ViewGroup.LayoutParams! generateLayoutParams(android.view.ViewGroup.LayoutParams!);
-    method public androidx.media.widget.MediaControlView2! getMediaControlView2();
-    method public androidx.media2.SessionToken2! getMediaSessionToken2();
+    method public androidx.media.widget.MediaControlView2? getMediaControlView2();
+    method public androidx.media2.SessionToken2 getMediaSessionToken2();
     method public int getViewType();
     method public void onAttachedToWindow();
     method public void onDetachedFromWindow();
     method public void onMeasure(int, int);
     method public void setAudioAttributes(androidx.media.AudioAttributesCompat);
-    method public void setMediaControlView2(androidx.media.widget.MediaControlView2!, long);
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public void setMediaItem2(androidx.media2.MediaItem2);
+    method public void setMediaControlView2(androidx.media.widget.MediaControlView2, long);
+    method public void setMediaItem2(androidx.media2.MediaItem2);
     method public void setViewType(int);
     field public static final int VIEW_TYPE_SURFACEVIEW = 0; // 0x0
     field public static final int VIEW_TYPE_TEXTUREVIEW = 1; // 0x1
diff --git a/media-widget/api/current.txt b/media-widget/api/current.txt
index bc36437..b803bef 100644
--- a/media-widget/api/current.txt
+++ b/media-widget/api/current.txt
@@ -5,35 +5,29 @@
     ctor public MediaControlView2(android.content.Context);
     ctor public MediaControlView2(android.content.Context, android.util.AttributeSet?);
     ctor public MediaControlView2(android.content.Context, android.util.AttributeSet?, int);
-    method public boolean checkLayoutParams(android.view.ViewGroup.LayoutParams!);
-    method public android.view.ViewGroup.LayoutParams! generateDefaultLayoutParams();
-    method public android.view.ViewGroup.LayoutParams! generateLayoutParams(android.view.ViewGroup.LayoutParams!);
     method public void onMeasure(int, int);
     method public void requestPlayButtonFocus();
-    method public void setMediaSessionToken2(androidx.media2.SessionToken2!);
-    method public void setOnFullScreenListener(androidx.media.widget.MediaControlView2.OnFullScreenListener!);
+    method public void setMediaSessionToken2(androidx.media2.SessionToken2);
+    method public void setOnFullScreenListener(androidx.media.widget.MediaControlView2.OnFullScreenListener);
   }
 
   public static interface MediaControlView2.OnFullScreenListener {
-    method public void onFullScreen(android.view.View!, boolean);
+    method public void onFullScreen(android.view.View, boolean);
   }
 
   public class VideoView2 extends android.view.ViewGroup {
     ctor public VideoView2(android.content.Context);
     ctor public VideoView2(android.content.Context, android.util.AttributeSet?);
     ctor public VideoView2(android.content.Context, android.util.AttributeSet?, int);
-    method public boolean checkLayoutParams(android.view.ViewGroup.LayoutParams!);
-    method public android.view.ViewGroup.LayoutParams! generateDefaultLayoutParams();
-    method public android.view.ViewGroup.LayoutParams! generateLayoutParams(android.view.ViewGroup.LayoutParams!);
-    method public androidx.media.widget.MediaControlView2! getMediaControlView2();
-    method public androidx.media2.SessionToken2! getMediaSessionToken2();
+    method public androidx.media.widget.MediaControlView2? getMediaControlView2();
+    method public androidx.media2.SessionToken2 getMediaSessionToken2();
     method public int getViewType();
     method public void onAttachedToWindow();
     method public void onDetachedFromWindow();
     method public void onMeasure(int, int);
     method public void setAudioAttributes(androidx.media.AudioAttributesCompat);
-    method public void setMediaControlView2(androidx.media.widget.MediaControlView2!, long);
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public void setMediaItem2(androidx.media2.MediaItem2);
+    method public void setMediaControlView2(androidx.media.widget.MediaControlView2, long);
+    method public void setMediaItem2(androidx.media2.MediaItem2);
     method public void setViewType(int);
     field public static final int VIEW_TYPE_SURFACEVIEW = 0; // 0x0
     field public static final int VIEW_TYPE_TEXTUREVIEW = 1; // 0x1
diff --git a/media-widget/src/androidTest/java/androidx/media/widget/MediaControlView2Test.java b/media-widget/src/androidTest/java/androidx/media/widget/MediaControlView2Test.java
index 9c5b3ac..2e29e89 100644
--- a/media-widget/src/androidTest/java/androidx/media/widget/MediaControlView2Test.java
+++ b/media-widget/src/androidTest/java/androidx/media/widget/MediaControlView2Test.java
@@ -216,33 +216,35 @@
             return;
         }
 
-        final CountDownLatch latchForPausedState = new CountDownLatch(1);
-        final CountDownLatch latchForRew = new CountDownLatch(2);
+        final CountDownLatch latchForFfwd = new CountDownLatch(1);
+        final CountDownLatch latchForRew = new CountDownLatch(1);
         final MediaController2 controller =
                 createController(new MediaController2.ControllerCallback() {
+                    long mExpectedPosition;
+                    final long mDelta = 1000L;
                     @Override
                     public void onPlayerStateChanged(@NonNull MediaController2 controller,
                             int state) {
                         if (state == SessionPlayer2.PLAYER_STATE_PAUSED) {
-                            controller.seekTo(FFWD_MS);
-                            latchForPausedState.countDown();
+                            mExpectedPosition = FFWD_MS;
+                            controller.seekTo(mExpectedPosition);
                         }
                     }
                     @Override
                     public void onSeekCompleted(@NonNull MediaController2 controller,
                             long position) {
-                        switch ((int) latchForRew.getCount()) {
-                            case 2:
-                                if (position == FFWD_MS) {
-                                    latchForRew.countDown();
-                                }
-                                break;
-                            case 1:
-                                if (position == FFWD_MS - REW_MS) {
-                                    latchForRew.countDown();
-                                }
+                        assertTrue(equalsSeekPosition(mExpectedPosition, position, mDelta));
+                        if (mExpectedPosition == FFWD_MS) {
+                            mExpectedPosition = position - REW_MS;
+                            latchForFfwd.countDown();
+                        } else {
+                            latchForRew.countDown();
                         }
                     }
+
+                    private boolean equalsSeekPosition(long expected, long actual, long delta) {
+                        return (actual < expected + delta) && (actual > expected - delta);
+                    }
                 });
         mActivityRule.runOnUiThread(new Runnable() {
             @Override
@@ -250,7 +252,7 @@
                 mVideoView.setMediaItem2(mFileSchemeMediaItem);
             }
         });
-        assertTrue(latchForPausedState.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        assertTrue(latchForFfwd.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
         onView(withId(R.id.rew)).perform(click());
         assertTrue(latchForRew.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
     }
@@ -298,24 +300,27 @@
 
         final long duration = 49056L;
         final String title = "BigBuckBunny";
-        final CountDownLatch latch = new CountDownLatch(1);
+        final CountDownLatch latch = new CountDownLatch(2);
         final MediaController2 controller =
                 createController(new MediaController2.ControllerCallback() {
                     @Override
-                    public void onPlaylistChanged(@NonNull MediaController2 controller,
-                            @NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) {
-                        MediaMetadata2 itemMetadata = list.get(0).getMetadata();
-                        if (itemMetadata != null) {
-                            if (itemMetadata.containsKey(MediaMetadata2.METADATA_KEY_TITLE)) {
-                                assertEquals(title, itemMetadata.getString(
-                                        MediaMetadata2.METADATA_KEY_TITLE));
-                            }
-                            if (itemMetadata.containsKey(MediaMetadata2.METADATA_KEY_DURATION)) {
-                                assertEquals(duration, itemMetadata.getLong(
-                                        MediaMetadata2.METADATA_KEY_DURATION));
+                    public void onCurrentMediaItemChanged(@NonNull MediaController2 controller,
+                            @Nullable MediaItem2 item) {
+                        if (item != null) {
+                            MediaMetadata2 metadata = item.getMetadata();
+                            if (metadata != null) {
+                                if (metadata.containsKey(MediaMetadata2.METADATA_KEY_TITLE)) {
+                                    assertEquals(title, metadata.getString(
+                                            MediaMetadata2.METADATA_KEY_TITLE));
+                                    latch.countDown();
+                                }
+                                if (metadata.containsKey(MediaMetadata2.METADATA_KEY_DURATION)) {
+                                    assertEquals(duration, metadata.getLong(
+                                            MediaMetadata2.METADATA_KEY_DURATION));
+                                    latch.countDown();
+                                }
                             }
                         }
-                        latch.countDown();
                     }
                 });
         mActivityRule.runOnUiThread(new Runnable() {
@@ -345,28 +350,35 @@
         final MediaItem2 uriMediaItem = createTestMediaItem2(uri);
         final MediaItem2 fileMediaItem = new FileMediaItem2.Builder(afd.getFileDescriptor(),
                 afd.getStartOffset(), afd.getLength()).build();
-        final CountDownLatch latchForUri = new CountDownLatch(1);
-        final CountDownLatch latchForFile = new CountDownLatch(1);
+        final CountDownLatch latchForUri = new CountDownLatch(3);
+        final CountDownLatch latchForFile = new CountDownLatch(3);
         final MediaController2 controller =
                 createController(new MediaController2.ControllerCallback() {
                     @Override
-                    public void onPlaylistChanged(@NonNull MediaController2 controller,
-                            @NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) {
-                        MediaMetadata2 itemMetadata = list.get(0).getMetadata();
-                        if (itemMetadata != null) {
-                            if (itemMetadata.containsKey(MediaMetadata2.METADATA_KEY_TITLE)) {
-                                assertEquals(title, itemMetadata.getString(
-                                        MediaMetadata2.METADATA_KEY_TITLE));
-                            }
-                            if (itemMetadata.containsKey(MediaMetadata2.METADATA_KEY_ARTIST)) {
-                                assertEquals(artist, itemMetadata.getString(
-                                        MediaMetadata2.METADATA_KEY_ARTIST));
-                            }
-                            if (itemMetadata.containsKey(MediaMetadata2.METADATA_KEY_DURATION)) {
-                                assertEquals(duration, itemMetadata.getLong(
-                                        MediaMetadata2.METADATA_KEY_DURATION));
+                    public void onCurrentMediaItemChanged(@NonNull MediaController2 controller,
+                            @Nullable MediaItem2 item) {
+                        if (item != null) {
+                            MediaMetadata2 metadata = item.getMetadata();
+                            if (metadata != null) {
+                                if (metadata.containsKey(MediaMetadata2.METADATA_KEY_TITLE)) {
+                                    assertEquals(title, metadata.getString(
+                                            MediaMetadata2.METADATA_KEY_TITLE));
+                                    countDown();
+                                }
+                                if (metadata.containsKey(MediaMetadata2.METADATA_KEY_ARTIST)) {
+                                    assertEquals(artist, metadata.getString(
+                                            MediaMetadata2.METADATA_KEY_ARTIST));
+                                    countDown();
+                                }
+                                if (metadata.containsKey(MediaMetadata2.METADATA_KEY_DURATION)) {
+                                    assertEquals(duration, metadata.getLong(
+                                            MediaMetadata2.METADATA_KEY_DURATION));
+                                    countDown();
+                                }
                             }
                         }
+                    }
+                    private void countDown() {
                         if (latchForUri.getCount() != 0) {
                             latchForUri.countDown();
                         } else {
diff --git a/media-widget/src/androidTest/java/androidx/media/widget/VideoView2Test.java b/media-widget/src/androidTest/java/androidx/media/widget/VideoView2Test.java
index 4939ecd..a4d14bb 100644
--- a/media-widget/src/androidTest/java/androidx/media/widget/VideoView2Test.java
+++ b/media-widget/src/androidTest/java/androidx/media/widget/VideoView2Test.java
@@ -220,9 +220,7 @@
                 any(MediaController2.class), eq(SessionPlayer2.PLAYER_STATE_PLAYING));
     }
 
-    // TODO: Shortly prevented to be run, since it crashed.
-    //       Revive @Test annotation after investigating b/118412745.
-    // @Test
+    @Test
     public void testPlayVideoOnTextureView() throws Throwable {
         // Don't run the test if the codec isn't supported.
         if (!hasCodec()) {
@@ -248,12 +246,46 @@
                 .onViewTypeChanged(mVideoView, VideoView2.VIEW_TYPE_TEXTUREVIEW);
         verify(mControllerCallback, timeout(TIME_OUT).atLeastOnce()).onConnected(
                 any(MediaController2.class), any(SessionCommandGroup2.class));
+        verify(mControllerCallback, timeout(TIME_OUT).atLeast(1)).onPlayerStateChanged(
+                any(MediaController2.class), eq(SessionPlayer2.PLAYER_STATE_PAUSED));
 
         mController.play();
         verify(mControllerCallback, timeout(TIME_OUT).atLeast(1)).onPlayerStateChanged(
                 any(MediaController2.class), eq(SessionPlayer2.PLAYER_STATE_PLAYING));
+    }
+
+    @Test
+    public void testSetViewType() throws Throwable {
+        // Don't run the test if the codec isn't supported.
+        if (!hasCodec()) {
+            Log.i(TAG, "SKIPPING testPlayVideoOnTextureView(): codec is not supported");
+            return;
+        }
+
+        final VideoView2.OnViewTypeChangedListener mockViewTypeListener =
+                mock(VideoView2.OnViewTypeChangedListener.class);
+
+        // The default view type is surface view.
+        assertEquals(mVideoView.getViewType(), mVideoView.VIEW_TYPE_SURFACEVIEW);
+
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mVideoView.setOnViewTypeChangedListener(mockViewTypeListener);
+                mVideoView.setViewType(mVideoView.VIEW_TYPE_TEXTUREVIEW);
+                mVideoView.setViewType(mVideoView.VIEW_TYPE_SURFACEVIEW);
+                mVideoView.setViewType(mVideoView.VIEW_TYPE_TEXTUREVIEW);
+                mVideoView.setViewType(mVideoView.VIEW_TYPE_SURFACEVIEW);
+                mVideoView.setMediaItem2(mMediaItem);
+            }
+        });
+
+        verify(mControllerCallback, timeout(TIME_OUT).atLeastOnce()).onConnected(
+                any(MediaController2.class), any(SessionCommandGroup2.class));
         verify(mControllerCallback, timeout(TIME_OUT).atLeast(1)).onPlayerStateChanged(
                 any(MediaController2.class), eq(SessionPlayer2.PLAYER_STATE_PAUSED));
+
+        assertEquals(mVideoView.getViewType(), mVideoView.VIEW_TYPE_SURFACEVIEW);
     }
 
     @Test
diff --git a/media-widget/src/main/java/androidx/media/widget/BaseLayout.java b/media-widget/src/main/java/androidx/media/widget/BaseLayout.java
index 982513a..231f259 100644
--- a/media-widget/src/main/java/androidx/media/widget/BaseLayout.java
+++ b/media-widget/src/main/java/androidx/media/widget/BaseLayout.java
@@ -54,12 +54,12 @@
     }
 
     @Override
-    public boolean checkLayoutParams(LayoutParams p) {
+    protected boolean checkLayoutParams(LayoutParams p) {
         return p instanceof MarginLayoutParams;
     }
 
     @Override
-    public LayoutParams generateDefaultLayoutParams() {
+    protected LayoutParams generateDefaultLayoutParams() {
         return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
     }
 
@@ -69,7 +69,7 @@
     }
 
     @Override
-    public LayoutParams generateLayoutParams(LayoutParams lp) {
+    protected LayoutParams generateLayoutParams(LayoutParams lp) {
         if (lp instanceof MarginLayoutParams) {
             return lp;
         }
diff --git a/media-widget/src/main/java/androidx/media/widget/MediaControlView2.java b/media-widget/src/main/java/androidx/media/widget/MediaControlView2.java
index f473e96..f8db518 100644
--- a/media-widget/src/main/java/androidx/media/widget/MediaControlView2.java
+++ b/media-widget/src/main/java/androidx/media/widget/MediaControlView2.java
@@ -67,13 +67,13 @@
 import androidx.media2.MediaController2;
 import androidx.media2.MediaItem2;
 import androidx.media2.MediaMetadata2;
+import androidx.media2.MediaPlayer;
 import androidx.media2.MediaSession2;
 import androidx.media2.SessionCommand2;
 import androidx.media2.SessionCommandGroup2;
 import androidx.media2.SessionPlayer2;
 import androidx.media2.SessionToken2;
 import androidx.media2.UriMediaItem2;
-import androidx.media2.XMediaPlayer;
 import androidx.mediarouter.app.MediaRouteButton;
 import androidx.mediarouter.media.MediaRouteSelector;
 
@@ -85,7 +85,7 @@
 import java.util.concurrent.Executor;
 
 /**
- * A View that contains the controls for {@link XMediaPlayer}.
+ * A View that contains the controls for {@link MediaPlayer}.
  * It provides a wide range of buttons that serve the following functions: play/pause,
  * rewind/fast-forward, skip to next/previous, select subtitle track, enter/exit full screen mode,
  * adjust video quality, select audio track, and adjust playback speed.
@@ -234,6 +234,8 @@
     ImageButton mPlayPauseButton;
     ImageButton mFfwdButton;
     ImageButton mRewButton;
+    // TODO: Disable Next/Previous buttons when the current item does not have a next/previous
+    // item in the playlist. (b/119159436)
     private ImageButton mNextButton;
     private ImageButton mPrevButton;
 
@@ -319,11 +321,10 @@
      * Sets MediaSession2 token to control corresponding MediaSession2. It makes it possible to
      * send and receive data between MediaControlView2 and VideoView2.
      */
-    public void setMediaSessionToken2(SessionToken2 token) {
+    public void setMediaSessionToken2(@NonNull SessionToken2 token) {
         mController.setMediaSessionToken2(token);
         if (mController.hasMetadata()) {
-            updateDuration();
-            updateTitle();
+            updateMetadata();
         }
     }
 
@@ -332,7 +333,7 @@
      * This needs to be implemented in order to display the fullscreen button.
      * @param l The callback that will be run
      */
-    public void setOnFullScreenListener(OnFullScreenListener l) {
+    public void setOnFullScreenListener(@NonNull OnFullScreenListener l) {
         mOnFullScreenListener = l;
         mFullScreenButton.setVisibility(View.VISIBLE);
     }
@@ -354,7 +355,7 @@
         /**
          * Called to indicate a fullscreen mode change.
          */
-        void onFullScreen(View view, boolean fullScreen);
+        void onFullScreen(@NonNull View view, boolean fullScreen);
     }
 
     @Override
@@ -1390,36 +1391,34 @@
                 }
             };
 
-    void updateDuration() {
-        if (mController.hasMetadata()) {
-            mTimeView.setVisibility(View.VISIBLE);
-            mDuration = mController.getDurationMs();
-            setProgress();
-        }
-    }
-
-    void updateTitle() {
-        if (mController.hasMetadata()) {
-            mTitleView.setText(mController.getTitle());
-        }
-    }
-
-    void updateAudioMetadata() {
-        if (mMediaType != MEDIA_TYPE_MUSIC) {
+    void updateMetadata() {
+        if (!mController.hasMetadata()) {
             return;
         }
-        if (mController.hasMetadata()) {
-            String titleText = mController.getTitle();
-            String artistText = mController.getArtistText();
-            if (titleText == null) {
-                titleText = mResources.getString(R.string.mcv2_music_title_unknown_text);
-            }
-            if (artistText == null) {
-                artistText = mResources.getString(R.string.mcv2_music_artist_unknown_text);
-            }
 
+        long duration = mController.getDurationMs();
+        if (duration != 0) {
+            mDuration = duration;
+            mTimeView.setVisibility(View.VISIBLE);
+            setProgress();
+        }
+
+        if (mMediaType != MEDIA_TYPE_MUSIC) {
+            String title = mController.getTitle();
+            if (title != null) {
+                mTitleView.setText(title);
+            }
+        } else {
+            String title = mController.getTitle();
+            if (title == null) {
+                title = mResources.getString(R.string.mcv2_music_title_unknown_text);
+            }
+            String artist = mController.getArtistText();
+            if (artist == null) {
+                artist = mResources.getString(R.string.mcv2_music_artist_unknown_text);
+            }
             // Update title for Embedded size type
-            mTitleView.setText(titleText + " - " + artistText);
+            mTitleView.setText(title + " - " + artist);
 
             // Remove unnecessary buttons
             mVideoQualityButton.setVisibility(View.GONE);
@@ -1660,12 +1659,10 @@
         mNextButton = v.findViewById(R.id.next);
         if (mNextButton != null) {
             mNextButton.setOnClickListener(mNextListener);
-            mNextButton.setVisibility(View.GONE);
         }
         mPrevButton = v.findViewById(R.id.prev);
         if (mPrevButton != null) {
             mPrevButton.setOnClickListener(mPrevListener);
-            mPrevButton.setVisibility(View.GONE);
         }
         return v;
     }
@@ -1831,6 +1828,28 @@
                 mFfwdButton.setVisibility(View.GONE);
             }
         }
+        if (commands.hasCommand(
+                SessionCommand2.COMMAND_CODE_PLAYER_SKIP_TO_PREVIOUS_PLAYLIST_ITEM)) {
+            if (mPrevButton != null) {
+                mPrevButton.setVisibility(VISIBLE);
+                mPrevButton.setEnabled(true);
+            }
+        } else {
+            if (mPrevButton != null) {
+                mPrevButton.setVisibility(View.GONE);
+            }
+        }
+        if (commands.hasCommand(
+                SessionCommand2.COMMAND_CODE_PLAYER_SKIP_TO_NEXT_PLAYLIST_ITEM)) {
+            if (mNextButton != null) {
+                mNextButton.setVisibility(VISIBLE);
+                mNextButton.setEnabled(true);
+            }
+        } else {
+            if (mNextButton != null) {
+                mNextButton.setVisibility(View.GONE);
+            }
+        }
         if (commands.hasCommand(SessionCommand2.COMMAND_CODE_PLAYER_SEEK_TO)) {
             mSeekAvailable = true;
             mProgress.setEnabled(true);
@@ -2344,6 +2363,8 @@
                 if (DEBUG) {
                     Log.d(TAG, "onCurrentMediaItemChanged(): " + mediaItem);
                 }
+                mMediaMetadata2 = mediaItem.getMetadata();
+                updateMetadata();
             }
 
             @Override
@@ -2360,7 +2381,16 @@
             @Override
             public void onConnected(@NonNull MediaController2 controller,
                     @NonNull SessionCommandGroup2 allowedCommands) {
+                if (DEBUG) {
+                    Log.d(TAG, "onConnected(): " + allowedCommands);
+                }
                 updateAllowedCommands(allowedCommands);
+
+                MediaItem2 mediaItem = controller.getCurrentMediaItem();
+                if (mediaItem != null) {
+                    mMediaMetadata2 = mediaItem.getMetadata();
+                    updateMetadata();
+                }
             }
 
             @Override
@@ -2376,12 +2406,6 @@
                 if (DEBUG) {
                     Log.d(TAG, "onPlaylistChanged(): list: " + list);
                 }
-                // Note that currently MediaControlView2 assumes single media item to play.
-                MediaItem2 mediaItem = list.isEmpty() ? null : list.get(0);
-                mMediaMetadata2 = (mediaItem != null) ? mediaItem.getMetadata() : null;
-                updateDuration();
-                updateTitle();
-                updateAudioMetadata();
             }
 
             @Override
diff --git a/media-widget/src/main/java/androidx/media/widget/VideoSurfaceView.java b/media-widget/src/main/java/androidx/media/widget/VideoSurfaceView.java
index 52b2c63..559d6a8 100644
--- a/media-widget/src/main/java/androidx/media/widget/VideoSurfaceView.java
+++ b/media-widget/src/main/java/androidx/media/widget/VideoSurfaceView.java
@@ -28,7 +28,8 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
-import androidx.media2.XMediaPlayer;
+import androidx.core.content.ContextCompat;
+import androidx.media2.MediaPlayer;
 
 @RequiresApi(21)
 class VideoSurfaceView extends SurfaceView
@@ -36,11 +37,11 @@
     private static final String TAG = "VideoSurfaceView";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
     private Surface mSurface = null;
-    private SurfaceListener mSurfaceListener = null;
-    private XMediaPlayer mMediaPlayer;
+    SurfaceListener mSurfaceListener = null;
+    private MediaPlayer mMediaPlayer;
     // A flag to indicate taking over other view should be proceed.
     private boolean mIsTakingOverOldView;
-    private VideoViewInterface mOldView;
+    VideoViewInterface mOldView;
 
     VideoSurfaceView(Context context) {
         super(context, null);
@@ -48,16 +49,29 @@
     }
 
     ////////////////////////////////////////////////////
-    // implements VideoViewInterfaceWithMp1
+    // implements VideoViewInterface
     ////////////////////////////////////////////////////
 
     @Override
-    public boolean assignSurfaceToMediaPlayer(XMediaPlayer mp) {
+    public boolean assignSurfaceToMediaPlayer(MediaPlayer mp) {
         Log.d(TAG, "assignSurfaceToMediaPlayer(): mSurface: " + mSurface);
         if (mp == null || !hasAvailableSurface()) {
             return false;
         }
-        mp.setSurface(mSurface);
+        mp.setSurface(mSurface).addListener(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        if (mOldView != null) {
+                            ((View) mOldView).setVisibility(GONE);
+                            mOldView = null;
+                        }
+                        if (mSurfaceListener != null) {
+                            mSurfaceListener.onSurfaceTakeOverDone(VideoSurfaceView.this);
+                        }
+                    }
+                }, ContextCompat.getMainExecutor(getContext())
+        );
         return true;
     }
 
@@ -72,7 +86,7 @@
     }
 
     @Override
-    public void setMediaPlayer(XMediaPlayer mp) {
+    public void setMediaPlayer(MediaPlayer mp) {
         mMediaPlayer = mp;
         if (mIsTakingOverOldView) {
             takeOver(mOldView);
@@ -82,12 +96,7 @@
     @Override
     public void takeOver(@NonNull VideoViewInterface oldView) {
         if (assignSurfaceToMediaPlayer(mMediaPlayer)) {
-            ((View) oldView).setVisibility(GONE);
             mIsTakingOverOldView = false;
-            mOldView = null;
-            if (mSurfaceListener != null) {
-                mSurfaceListener.onSurfaceTakeOverDone(this);
-            }
         } else {
             mIsTakingOverOldView = true;
             mOldView = oldView;
diff --git a/media-widget/src/main/java/androidx/media/widget/VideoTextureView.java b/media-widget/src/main/java/androidx/media/widget/VideoTextureView.java
index 44fdb1d3..f45081c 100644
--- a/media-widget/src/main/java/androidx/media/widget/VideoTextureView.java
+++ b/media-widget/src/main/java/androidx/media/widget/VideoTextureView.java
@@ -27,20 +27,21 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
-import androidx.media2.XMediaPlayer;
+import androidx.core.content.ContextCompat;
+import androidx.media2.MediaPlayer;
 
 @RequiresApi(21)
 class VideoTextureView extends TextureView
         implements VideoViewInterface, TextureView.SurfaceTextureListener {
-    private static final String TAG = "VideoTextureViewWithMp1";
+    private static final String TAG = "VideoTextureView";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
     private Surface mSurface;
-    private SurfaceListener mSurfaceListener;
-    private XMediaPlayer mMediaPlayer;
+    SurfaceListener mSurfaceListener;
+    private MediaPlayer mMediaPlayer;
     // A flag to indicate taking over other view should be proceed.
     private boolean mIsTakingOverOldView;
-    private VideoViewInterface mOldView;
+    VideoViewInterface mOldView;
 
     VideoTextureView(Context context) {
         super(context, null);
@@ -48,16 +49,29 @@
     }
 
     ////////////////////////////////////////////////////
-    // implements VideoViewInterfaceWithMp1
+    // implements VideoViewInterface
     ////////////////////////////////////////////////////
 
     @Override
-    public boolean assignSurfaceToMediaPlayer(XMediaPlayer mp) {
+    public boolean assignSurfaceToMediaPlayer(MediaPlayer mp) {
         if (mp == null || !hasAvailableSurface()) {
             // Surface is not ready.
             return false;
         }
-        mp.setSurface(mSurface);
+        mp.setSurface(mSurface).addListener(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        if (mOldView != null) {
+                            ((View) mOldView).setVisibility(GONE);
+                            mOldView = null;
+                        }
+                        if (mSurfaceListener != null) {
+                            mSurfaceListener.onSurfaceTakeOverDone(VideoTextureView.this);
+                        }
+                    }
+                }, ContextCompat.getMainExecutor(getContext())
+        );
         return true;
     }
 
@@ -72,7 +86,7 @@
     }
 
     @Override
-    public void setMediaPlayer(XMediaPlayer mp) {
+    public void setMediaPlayer(MediaPlayer mp) {
         mMediaPlayer = mp;
         if (mIsTakingOverOldView) {
             takeOver(mOldView);
@@ -82,12 +96,7 @@
     @Override
     public void takeOver(@NonNull VideoViewInterface oldView) {
         if (assignSurfaceToMediaPlayer(mMediaPlayer)) {
-            ((View) oldView).setVisibility(GONE);
             mIsTakingOverOldView = false;
-            mOldView = null;
-            if (mSurfaceListener != null) {
-                mSurfaceListener.onSurfaceTakeOverDone(this);
-            }
         } else {
             mIsTakingOverOldView = true;
             mOldView = oldView;
diff --git a/media-widget/src/main/java/androidx/media/widget/VideoView2.java b/media-widget/src/main/java/androidx/media/widget/VideoView2.java
index 0c74793..732e7ef 100644
--- a/media-widget/src/main/java/androidx/media/widget/VideoView2.java
+++ b/media-widget/src/main/java/androidx/media/widget/VideoView2.java
@@ -22,7 +22,6 @@
 import android.content.Context;
 import android.media.AudioManager;
 import android.media.MediaPlayer;
-import android.net.Uri;
 import android.os.Build;
 import android.util.AttributeSet;
 import android.util.Log;
@@ -37,12 +36,10 @@
 import androidx.annotation.VisibleForTesting;
 import androidx.media.AudioAttributesCompat;
 import androidx.media2.MediaItem2;
-import androidx.media2.MediaMetadata2;
 import androidx.media2.SessionToken2;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.util.Map;
 
 /**
  * Displays a video file.  VideoView2 class is a ViewGroup class which is wrapping
@@ -150,7 +147,7 @@
      * @param mediaControlView a media control view2 instance.
      * @param intervalMs a time interval in milliseconds until VideoView2 hides MediaControlView2.
      */
-    public void setMediaControlView2(MediaControlView2 mediaControlView, long intervalMs) {
+    public void setMediaControlView2(@NonNull MediaControlView2 mediaControlView, long intervalMs) {
         mImpl.setMediaControlView2(mediaControlView, intervalMs);
     }
 
@@ -158,39 +155,19 @@
      * Returns MediaControlView2 instance which is currently attached to VideoView2 by default or by
      * {@link #setMediaControlView2} method.
      */
+    @Nullable
     public MediaControlView2 getMediaControlView2() {
         return mImpl.getMediaControlView2();
     }
 
     /**
-     * Sets MediaMetadata2 instance. It will replace the previously assigned MediaMetadata2 instance
-     * if any.
-     *
-     * @param metadata a MediaMetadata2 instance.
-     * @hide
-     */
-    @RestrictTo(LIBRARY_GROUP)
-    public void setMediaMetadata(MediaMetadata2 metadata) {
-        mImpl.setMediaMetadata(metadata);
-    }
-
-    /**
-     * Returns MediaMetadata2 instance which is retrieved from MediaPlayer inside VideoView2 by
-     * default or by {@link #setMediaMetadata} method.
-     * @hide
-     */
-    @RestrictTo(LIBRARY_GROUP)
-    public MediaMetadata2 getMediaMetadata() {
-        return mImpl.getMediaMetadata();
-    }
-
-    /**
      * Returns {@link SessionToken2} so that developers create their own
      * {@link androidx.media2.MediaController2} instance. This method should be called when
      * VideoView2 is attached to window, or it throws IllegalStateException.
      *
      * @throws IllegalStateException if internal MediaSession is not created yet.
      */
+    @NonNull
     public SessionToken2 getMediaSessionToken2() {
         return mImpl.getMediaSessionToken2();
     }
@@ -205,50 +182,9 @@
     }
 
     /**
-     * Sets video path.
-     *
-     * @param path the path of the video.
-     *
-     * @hide
-     */
-    @RestrictTo(LIBRARY_GROUP)
-    public void setVideoPath(String path) {
-        mImpl.setVideoUri(Uri.parse(path));
-    }
-
-    /**
-     * Sets video URI.
-     *
-     * @param uri the URI of the video.
-     *
-     * @hide
-     */
-    @RestrictTo(LIBRARY_GROUP)
-    public void setVideoUri(Uri uri) {
-        mImpl.setVideoUri(uri, null);
-    }
-
-    /**
-     * Sets video URI using specific headers.
-     *
-     * @param uri     the URI of the video.
-     * @param headers the headers for the URI request.
-     *                Note that the cross domain redirection is allowed by default, but that can be
-     *                changed with key/value pairs through the headers parameter with
-     *                "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value
-     *                to disallow or allow cross domain redirection.
-     *
-     * @hide
-     */
-    public void setVideoUri(Uri uri, @Nullable Map<String, String> headers) {
-        mImpl.setVideoUri(uri, headers);
-    }
-
-    /**
      * Sets {@link MediaItem2} object to render using VideoView2.
      * @param mediaItem the MediaItem2 to play
      */
-    @RestrictTo(LIBRARY_GROUP)
     public void setMediaItem2(@NonNull MediaItem2 mediaItem) {
         mImpl.setMediaItem2(mediaItem);
     }
diff --git a/media-widget/src/main/java/androidx/media/widget/VideoView2Impl.java b/media-widget/src/main/java/androidx/media/widget/VideoView2Impl.java
index aa91162..10db526 100644
--- a/media-widget/src/main/java/androidx/media/widget/VideoView2Impl.java
+++ b/media-widget/src/main/java/androidx/media/widget/VideoView2Impl.java
@@ -17,21 +17,15 @@
 package androidx.media.widget;
 
 import android.content.Context;
-import android.net.Uri;
-import android.support.v4.media.session.MediaControllerCompat;
 import android.util.AttributeSet;
 import android.view.MotionEvent;
-import android.view.View;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.media.AudioAttributesCompat;
 import androidx.media2.MediaItem2;
-import androidx.media2.MediaMetadata2;
 import androidx.media2.SessionToken2;
 
-import java.util.Map;
-
 /**
  * Interface for impl classes.
  */
@@ -47,7 +41,7 @@
      * @param mediaControlView a media control view2 instance.
      * @param intervalMs a time interval in milliseconds until VideoView2 hides MediaControlView2.
      */
-    void setMediaControlView2(MediaControlView2 mediaControlView, long intervalMs);
+    void setMediaControlView2(@NonNull MediaControlView2 mediaControlView, long intervalMs);
 
     /**
      * Returns MediaControlView2 instance which is currently attached to VideoView2 by default or by
@@ -56,37 +50,13 @@
     MediaControlView2 getMediaControlView2();
 
     /**
-     * Sets MediaMetadata2 instance. It will replace the previously assigned MediaMetadata2 instance
-     * if any.
-     *
-     * @param metadata a MediaMetadata2 instance.
-     */
-    void setMediaMetadata(MediaMetadata2 metadata);
-
-    /**
-     * Returns MediaMetadata2 instance which is retrieved from MediaPlayer inside VideoView2 by
-     * default or by {@link #setMediaMetadata} method.
-     */
-    MediaMetadata2 getMediaMetadata();
-
-    /**
-     * Returns MediaController instance which is connected with MediaSession that VideoView2 is
-     * using. This method should be called when VideoView2 is attached to window, or it throws
-     * IllegalStateException, since internal MediaSession instance is not available until
-     * this view is attached to window. Please check {@link View#isAttachedToWindow}
-     * before calling this method.
-     *
-     * @throws IllegalStateException if internal MediaSession is not created yet.
-     */
-    MediaControllerCompat getMediaController();
-
-    /**
      * Returns {@link SessionToken2} so that developers create their own
      * {@link androidx.media2.MediaController2} instance. This method should be called when
      * VideoView2 is attached to window, or it throws IllegalStateException.
      *
      * @throws IllegalStateException if internal MediaSession is not created yet.
      */
+    @NonNull
     SessionToken2 getMediaSessionToken2();
 
     /**
@@ -97,32 +67,6 @@
     void setAudioAttributes(@NonNull AudioAttributesCompat attributes);
 
     /**
-     * Sets video path.
-     *
-     * @param path the path of the video.
-     */
-    void setVideoPath(String path);
-
-    /**
-     * Sets video URI.
-     *
-     * @param uri the URI of the video.
-     */
-    void setVideoUri(Uri uri);
-
-    /**
-     * Sets video URI using specific headers.
-     *
-     * @param uri     the URI of the video.
-     * @param headers the headers for the URI request.
-     *                Note that the cross domain redirection is allowed by default, but that can be
-     *                changed with key/value pairs through the headers parameter with
-     *                "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value
-     *                to disallow or allow cross domain redirection.
-     */
-    void setVideoUri(Uri uri, @Nullable Map<String, String> headers);
-
-    /**
      * Sets {@link MediaItem2} object to render using VideoView2.
      *
      * @param mediaItem the MediaItem2 to play
diff --git a/media-widget/src/main/java/androidx/media/widget/VideoView2ImplBase.java b/media-widget/src/main/java/androidx/media/widget/VideoView2ImplBase.java
index c211e3c..a0dedb3 100644
--- a/media-widget/src/main/java/androidx/media/widget/VideoView2ImplBase.java
+++ b/media-widget/src/main/java/androidx/media/widget/VideoView2ImplBase.java
@@ -30,7 +30,6 @@
 import android.media.MediaMetadataRetriever;
 import android.net.Uri;
 import android.os.Bundle;
-import android.support.v4.media.session.MediaControllerCompat;
 import android.util.AttributeSet;
 import android.util.DisplayMetrics;
 import android.util.Log;
@@ -51,6 +50,7 @@
 import androidx.media2.FileMediaItem2;
 import androidx.media2.MediaItem2;
 import androidx.media2.MediaMetadata2;
+import androidx.media2.MediaPlayer;
 import androidx.media2.MediaPlayer2;
 import androidx.media2.MediaSession2;
 import androidx.media2.RemoteSessionPlayer2;
@@ -60,7 +60,6 @@
 import androidx.media2.SessionToken2;
 import androidx.media2.SubtitleData2;
 import androidx.media2.UriMediaItem2;
-import androidx.media2.XMediaPlayer;
 import androidx.media2.subtitle.Cea708CaptionRenderer;
 import androidx.media2.subtitle.ClosedCaptionRenderer;
 import androidx.media2.subtitle.SubtitleController;
@@ -72,7 +71,6 @@
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
 import java.util.concurrent.Executor;
 
 /**
@@ -102,6 +100,7 @@
     private VideoView2.OnViewTypeChangedListener mViewTypeChangedListener;
 
     VideoViewInterface mCurrentView;
+    VideoViewInterface mTargetView;
     private VideoTextureView mTextureView;
     private VideoSurfaceView mSurfaceView;
 
@@ -121,7 +120,6 @@
     private String mMusicArtistText;
     boolean mIsMusicMediaType;
     private int mPrevWidth;
-    private int mPrevHeight;
     int mDominantColor;
     private int mSizeType;
 
@@ -142,7 +140,6 @@
     int mSelectedSubtitleTrackIndex;
 
     private SubtitleAnchorView mSubtitleAnchorView;
-    boolean mSubtitleEnabled;
 
     VideoView2 mInstance;
 
@@ -268,6 +265,7 @@
             mSurfaceView.setVisibility(View.GONE);
             mCurrentView = mTextureView;
         }
+        mTargetView = mCurrentView;
 
         MediaRouteSelector.Builder builder = new MediaRouteSelector.Builder();
         builder.addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
@@ -284,7 +282,7 @@
      * @param intervalMs a time interval in milliseconds until VideoView2 hides MediaControlView2.
      */
     @Override
-    public void setMediaControlView2(MediaControlView2 mediaControlView, long intervalMs) {
+    public void setMediaControlView2(@NonNull MediaControlView2 mediaControlView, long intervalMs) {
         mMediaControlView = mediaControlView;
         mMediaControlView.setShowControllerInterval(intervalMs);
 
@@ -303,42 +301,6 @@
     }
 
     /**
-     * Sets MediaMetadata2 instance. It will replace the previously assigned MediaMetadata2 instance
-     * if any.
-     *
-     * @param metadata a MediaMetadata2 instance.
-     */
-    @Override
-    public void setMediaMetadata(MediaMetadata2 metadata) {
-        if (mMediaItem != null) {
-            mMediaItem.setMetadata(metadata);
-        }
-    }
-
-    /**
-     * Returns MediaMetadata2 instance which is retrieved from MediaPlayer inside VideoView2 by
-     * default or by {@link #setMediaMetadata} method.
-     */
-    @Override
-    public MediaMetadata2 getMediaMetadata() {
-        return (mMediaItem != null) ? mMediaItem.getMetadata() : null;
-    }
-
-    /**
-     * Returns MediaController instance which is connected with MediaSession that VideoView2 is
-     * using. This method should be called when VideoView2 is attached to window, or it throws
-     * IllegalStateException, since internal MediaSession instance is not available until
-     * this view is attached to window. Please check {@link View#isAttachedToWindow}
-     * before calling this method.
-     *
-     * @throws IllegalStateException if internal MediaSession is not created yet.
-     */
-    @Override
-    public MediaControllerCompat getMediaController() {
-        return null;
-    }
-
-    /**
      * Returns {@link SessionToken2} so that developers create their own
      * {@link androidx.media2.MediaController2} instance. This method should be called when
      * VideoView2 is attached to window or after {@link #setMediaItem2} is called.
@@ -346,6 +308,7 @@
      * @throws IllegalStateException if internal MediaSession is not created yet.
      */
     @Override
+    @NonNull
     public SessionToken2 getMediaSessionToken2() {
         if (mMediaSession == null) {
             throw new IllegalStateException("MediaSession2 instance is not available.");
@@ -367,43 +330,6 @@
     }
 
     /**
-     * Sets video path.
-     *
-     * @param path the path of the video.
-     */
-    @Override
-    public void setVideoPath(String path) {
-        setVideoUri(Uri.parse(path));
-    }
-
-    /**
-     * Sets video URI.
-     *
-     * @param uri the URI of the video.
-     */
-    @Override
-    public void setVideoUri(Uri uri) {
-        setVideoUri(uri, null);
-    }
-
-    /**
-     * Sets video URI using specific headers.
-     *
-     * @param uri     the URI of the video.
-     * @param headers the headers for the URI request.
-     *                Note that the cross domain redirection is allowed by default, but that can be
-     *                changed with key/value pairs through the headers parameter with
-     *                "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value
-     *                to disallow or allow cross domain redirection.
-     */
-    @Override
-    public void setVideoUri(Uri uri, @Nullable Map<String, String> headers) {
-        UriMediaItem2.Builder builder = new UriMediaItem2.Builder(
-                mInstance.getContext(), uri, headers, null);
-        setMediaItem2(builder.build());
-    }
-
-    /**
      * Sets {@link MediaItem2} object to render using VideoView2.
      * @param mediaItem the MediaItem2 to play
      */
@@ -425,7 +351,8 @@
      */
     @Override
     public void setViewType(@VideoView2.ViewType int viewType) {
-        if (viewType == mCurrentView.getViewType()) {
+        if (viewType == mTargetView.getViewType()) {
+            Log.d(TAG, "setViewType with the same type (" + viewType + ") is ignored.");
             return;
         }
         VideoViewInterface targetView;
@@ -438,6 +365,8 @@
         } else {
             throw new IllegalArgumentException("Unknown view type: " + viewType);
         }
+
+        mTargetView = targetView;
         ((View) targetView).setVisibility(View.VISIBLE);
         targetView.takeOver(mCurrentView);
         mInstance.requestLayout();
@@ -559,7 +488,6 @@
                     }
                 }
                 mPrevWidth = currWidth;
-                mPrevHeight = currHeight;
             }
         }
     }
@@ -598,15 +526,23 @@
 
     @Override
     public void onSurfaceTakeOverDone(VideoViewInterface view) {
+        if (view != mTargetView) {
+            if (DEBUG) {
+                Log.d(TAG, "onSurfaceTakeOverDone(). view is not targetView. ignore.: " + view);
+            }
+            return;
+        }
         if (DEBUG) {
             Log.d(TAG, "onSurfaceTakeOverDone(). Now current view is: " + view);
         }
         if (mCurrentState != STATE_PLAYING) {
             mMediaSession.getPlayer().seekTo(mMediaSession.getPlayer().getCurrentPosition());
         }
-        mCurrentView = view;
-        if (mViewTypeChangedListener != null) {
-            mViewTypeChangedListener.onViewTypeChanged(mInstance, view.getViewType());
+        if (view != mCurrentView) {
+            mCurrentView = view;
+            if (mViewTypeChangedListener != null) {
+                mViewTypeChangedListener.onViewTypeChanged(mInstance, view.getViewType());
+            }
         }
 
         if (needToStart()) {
@@ -689,7 +625,7 @@
             mSubtitleController = new SubtitleController(context);
             mSubtitleController.registerRenderer(new ClosedCaptionRenderer(context));
             mSubtitleController.registerRenderer(new Cea708CaptionRenderer(context));
-            mSubtitleController.setAnchor((SubtitleController.Anchor) mSubtitleAnchorView);
+            mSubtitleController.setAnchor(mSubtitleAnchorView);
 
             // we don't set the target state here either, but preserve the
             // target state that was there before.
@@ -760,7 +696,7 @@
 
     // TODO: move this method inside callback to make sure it runs inside the callback thread.
     Bundle extractTrackInfoData() {
-        List<XMediaPlayer.TrackInfo> trackInfos = mMediaPlayer.getTrackInfo();
+        List<MediaPlayer.TrackInfo> trackInfos = mMediaPlayer.getTrackInfo();
         mVideoTrackIndices = new ArrayList<>();
         mAudioTrackIndices = new ArrayList<>();
         mSubtitleTracks = new SparseArray<>();
@@ -874,6 +810,8 @@
                 MediaMetadata2.METADATA_KEY_DURATION, mMediaSession.getPlayer().getDuration());
         builder.putString(
                 MediaMetadata2.METADATA_KEY_MEDIA_ID, mMediaItem.getMediaId());
+        builder.putLong(MediaMetadata2.METADATA_KEY_BROWSABLE, MediaMetadata2.BROWSABLE_TYPE_NONE);
+        builder.putLong(MediaMetadata2.METADATA_KEY_PLAYABLE, 1);
         return builder.build();
     }
 
@@ -956,11 +894,11 @@
         mCurrentMusicView = newMusicView;
     }
 
-    XMediaPlayer.PlayerCallback mMediaPlayerCallback =
-            new XMediaPlayer.PlayerCallback() {
+    MediaPlayer.PlayerCallback mMediaPlayerCallback =
+            new MediaPlayer.PlayerCallback() {
                 @Override
                 public void onVideoSizeChanged(
-                        XMediaPlayer mp, MediaItem2 dsd, int width, int height) {
+                        MediaPlayer mp, MediaItem2 dsd, int width, int height) {
                     if (DEBUG) {
                         Log.d(TAG, "onVideoSizeChanged(): size: " + width + "/" + height);
                     }
@@ -983,7 +921,7 @@
 
                 @Override
                 public void onInfo(
-                        XMediaPlayer mp, MediaItem2 dsd, int what, int extra) {
+                        MediaPlayer mp, MediaItem2 dsd, int what, int extra) {
                     if (DEBUG) {
                         Log.d(TAG, "onInfo()");
                     }
@@ -1005,7 +943,7 @@
 
                 @Override
                 public void onError(
-                        XMediaPlayer mp, MediaItem2 dsd, int frameworkErr, int implErr) {
+                        MediaPlayer mp, MediaItem2 dsd, int frameworkErr, int implErr) {
                     if (DEBUG) {
                         Log.d(TAG, "Error: " + frameworkErr + "," + implErr);
                     }
@@ -1023,7 +961,7 @@
 
                 @Override
                 public void onSubtitleData(
-                        XMediaPlayer mp, MediaItem2 dsd, SubtitleData2 data) {
+                        MediaPlayer mp, MediaItem2 dsd, SubtitleData2 data) {
                     if (DEBUG) {
                         Log.d(TAG, "onSubtitleData(): getTrackIndex: " + data.getTrackIndex()
                                 + ", getCurrentPosition: " + mp.getCurrentPosition()
@@ -1089,14 +1027,13 @@
                         MediaMetadata2 metadata = extractMetadata();
                         if (metadata != null) {
                             mMediaItem.setMetadata(metadata);
-                            mMediaSession.getPlayer().replacePlaylistItem(0, mMediaItem);
                         }
                     }
 
                     if (mMediaControlView != null) {
                         mMediaControlView.setEnabled(true);
 
-                        Uri uri = (mMediaItem != null && mMediaItem instanceof UriMediaItem2)
+                        Uri uri = (mMediaItem instanceof UriMediaItem2)
                                 ? ((UriMediaItem2) mMediaItem).getUri() : null;
                         if (uri != null) {
                             String scheme = uri.getScheme();
@@ -1133,7 +1070,7 @@
                     }
                 }
 
-                private void onCompletion(XMediaPlayer mp, MediaItem2 dsd) {
+                private void onCompletion(MediaPlayer mp, MediaItem2 dsd) {
                     mCurrentState = STATE_PLAYBACK_COMPLETED;
                     mTargetState = STATE_PLAYBACK_COMPLETED;
                 }
@@ -1152,15 +1089,17 @@
             SessionCommandGroup2.Builder commandsBuilder = new SessionCommandGroup2.Builder()
                     .addCommand(SessionCommand2.COMMAND_CODE_PLAYER_PAUSE)
                     .addCommand(SessionCommand2.COMMAND_CODE_PLAYER_PLAY)
-                    .addCommand(SessionCommand2.COMMAND_CODE_PLAYER_PREFETCH)
+                    .addCommand(SessionCommand2.COMMAND_CODE_PLAYER_PREPARE)
                     .addCommand(SessionCommand2.COMMAND_CODE_PLAYER_SET_SPEED)
                     .addCommand(SessionCommand2.COMMAND_CODE_SESSION_FAST_FORWARD)
                     .addCommand(SessionCommand2.COMMAND_CODE_SESSION_REWIND)
+                    .addCommand(SessionCommand2.COMMAND_CODE_PLAYER_SKIP_TO_NEXT_PLAYLIST_ITEM)
+                    .addCommand(SessionCommand2.COMMAND_CODE_PLAYER_SKIP_TO_PREVIOUS_PLAYLIST_ITEM)
                     .addCommand(SessionCommand2.COMMAND_CODE_PLAYER_SEEK_TO)
                     .addCommand(SessionCommand2.COMMAND_CODE_VOLUME_SET_VOLUME)
                     .addCommand(SessionCommand2.COMMAND_CODE_VOLUME_ADJUST_VOLUME)
                     .addCommand(SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_URI)
-                    .addCommand(SessionCommand2.COMMAND_CODE_SESSION_PREFETCH_FROM_URI)
+                    .addCommand(SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_URI)
                     .addCommand(SessionCommand2.COMMAND_CODE_SESSION_SELECT_ROUTE)
                     .addCommand(SessionCommand2.COMMAND_CODE_PLAYER_GET_PLAYLIST)
                     .addCommand(SessionCommand2.COMMAND_CODE_PLAYER_GET_PLAYLIST_METADATA)
diff --git a/media-widget/src/main/java/androidx/media/widget/VideoView2Player.java b/media-widget/src/main/java/androidx/media/widget/VideoView2Player.java
index 35c2f6e..aaa5458 100644
--- a/media-widget/src/main/java/androidx/media/widget/VideoView2Player.java
+++ b/media-widget/src/main/java/androidx/media/widget/VideoView2Player.java
@@ -20,14 +20,14 @@
 
 import androidx.media2.MediaItem2;
 import androidx.media2.MediaMetadata2;
-import androidx.media2.XMediaPlayer;
+import androidx.media2.MediaPlayer;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
 import java.util.ArrayList;
 import java.util.List;
 
-class VideoView2Player extends XMediaPlayer {
+class VideoView2Player extends MediaPlayer {
     VideoView2Player(Context context) {
         super(context);
     }
diff --git a/media-widget/src/main/java/androidx/media/widget/VideoViewInterface.java b/media-widget/src/main/java/androidx/media/widget/VideoViewInterface.java
index 2c7879c..d7eb7c2 100644
--- a/media-widget/src/main/java/androidx/media/widget/VideoViewInterface.java
+++ b/media-widget/src/main/java/androidx/media/widget/VideoViewInterface.java
@@ -19,7 +19,7 @@
 import android.view.View;
 
 import androidx.annotation.NonNull;
-import androidx.media2.XMediaPlayer;
+import androidx.media2.MediaPlayer;
 
 interface VideoViewInterface {
     /**
@@ -29,10 +29,10 @@
      * @return true if the surface is successfully assigned, false if not. It will fail to assign
      *         if any of MediaPlayer or surface is unavailable.
      */
-    boolean assignSurfaceToMediaPlayer(XMediaPlayer mp);
+    boolean assignSurfaceToMediaPlayer(MediaPlayer mp);
     void setSurfaceListener(SurfaceListener l);
     int getViewType();
-    void setMediaPlayer(XMediaPlayer mp);
+    void setMediaPlayer(MediaPlayer mp);
 
     /**
      * Takes over oldView. It means that the MediaPlayer will start rendering on this view.
diff --git a/media/api21/android/support/v4/media/MediaBrowserCompatApi21.java b/media/api21/android/support/v4/media/MediaBrowserCompatApi21.java
deleted file mode 100644
index acd8bf8..0000000
--- a/media/api21/android/support/v4/media/MediaBrowserCompatApi21.java
+++ /dev/null
@@ -1,158 +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 android.support.v4.media;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.media.browse.MediaBrowser;
-import android.os.Bundle;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-
-import java.util.List;
-
-@RequiresApi(21)
-class MediaBrowserCompatApi21 {
-    static final String NULL_MEDIA_ITEM_ID =
-            "android.support.v4.media.MediaBrowserCompat.NULL_MEDIA_ITEM";
-
-    public static Object createConnectionCallback(ConnectionCallback callback) {
-        return new ConnectionCallbackProxy<>(callback);
-    }
-
-    public static Object createBrowser(Context context, ComponentName serviceComponent,
-            Object callback, Bundle rootHints) {
-        return new MediaBrowser(context, serviceComponent,
-                (MediaBrowser.ConnectionCallback) callback, rootHints);
-    }
-
-    public static void connect(Object browserObj) {
-        ((MediaBrowser)browserObj).connect();
-    }
-
-    public static void disconnect(Object browserObj) {
-        ((MediaBrowser)browserObj).disconnect();
-
-    }
-
-    public static boolean isConnected(Object browserObj) {
-        return ((MediaBrowser)browserObj).isConnected();
-    }
-
-    public static ComponentName getServiceComponent(Object browserObj) {
-        return ((MediaBrowser)browserObj).getServiceComponent();
-    }
-
-    public static String getRoot(Object browserObj) {
-        return ((MediaBrowser)browserObj).getRoot();
-    }
-
-    public static Bundle getExtras(Object browserObj) {
-        return ((MediaBrowser)browserObj).getExtras();
-    }
-
-    public static Object getSessionToken(Object browserObj) {
-        return ((MediaBrowser)browserObj).getSessionToken();
-    }
-
-    public static Object createSubscriptionCallback(SubscriptionCallback callback) {
-        return new SubscriptionCallbackProxy<>(callback);
-    }
-
-    public static void subscribe(
-            Object browserObj, String parentId, Object subscriptionCallbackObj) {
-        ((MediaBrowser)browserObj).subscribe(parentId,
-                (MediaBrowser.SubscriptionCallback) subscriptionCallbackObj);
-    }
-
-    public static void unsubscribe(Object browserObj, String parentId) {
-        ((MediaBrowser)browserObj).unsubscribe(parentId);
-    }
-
-    interface ConnectionCallback {
-        void onConnected();
-        void onConnectionSuspended();
-        void onConnectionFailed();
-    }
-
-    static class ConnectionCallbackProxy<T extends ConnectionCallback>
-            extends MediaBrowser.ConnectionCallback {
-        protected final T mConnectionCallback;
-
-        public ConnectionCallbackProxy(T connectionCallback) {
-            mConnectionCallback = connectionCallback;
-        }
-
-        @Override
-        public void onConnected() {
-            mConnectionCallback.onConnected();
-        }
-
-        @Override
-        public void onConnectionSuspended() {
-            mConnectionCallback.onConnectionSuspended();
-        }
-
-        @Override
-        public void onConnectionFailed() {
-            mConnectionCallback.onConnectionFailed();
-        }
-    }
-
-    interface SubscriptionCallback {
-        void onChildrenLoaded(@NonNull String parentId, List<?> children);
-        void onError(@NonNull String parentId);
-    }
-
-    static class SubscriptionCallbackProxy<T extends SubscriptionCallback>
-            extends MediaBrowser.SubscriptionCallback {
-        protected final T mSubscriptionCallback;
-
-        public SubscriptionCallbackProxy(T callback) {
-            mSubscriptionCallback = callback;
-        }
-
-        @Override
-        public void onChildrenLoaded(@NonNull String parentId,
-                List<MediaBrowser.MediaItem> children) {
-            mSubscriptionCallback.onChildrenLoaded(parentId, children);
-        }
-
-        @Override
-        public void onError(@NonNull String parentId) {
-            mSubscriptionCallback.onError(parentId);
-        }
-    }
-
-    static class MediaItem {
-
-        public static int getFlags(Object itemObj) {
-            return ((MediaBrowser.MediaItem) itemObj).getFlags();
-        }
-
-        public static Object getDescription(Object itemObj) {
-            return ((MediaBrowser.MediaItem) itemObj).getDescription();
-        }
-
-        private MediaItem() {
-        }
-    }
-
-    private MediaBrowserCompatApi21() {
-    }
-}
diff --git a/media/api21/android/support/v4/media/MediaDescriptionCompatApi21.java b/media/api21/android/support/v4/media/MediaDescriptionCompatApi21.java
deleted file mode 100644
index bed7f01..0000000
--- a/media/api21/android/support/v4/media/MediaDescriptionCompatApi21.java
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v4.media;
-
-import android.graphics.Bitmap;
-import android.media.MediaDescription;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.Parcel;
-
-import androidx.annotation.RequiresApi;
-
-@RequiresApi(21)
-class MediaDescriptionCompatApi21 {
-
-    public static String getMediaId(Object descriptionObj) {
-        return ((MediaDescription) descriptionObj).getMediaId();
-    }
-
-    public static CharSequence getTitle(Object descriptionObj) {
-        return ((MediaDescription) descriptionObj).getTitle();
-    }
-
-    public static CharSequence getSubtitle(Object descriptionObj) {
-        return ((MediaDescription) descriptionObj).getSubtitle();
-    }
-
-    public static CharSequence getDescription(Object descriptionObj) {
-        return ((MediaDescription) descriptionObj).getDescription();
-    }
-
-    public static Bitmap getIconBitmap(Object descriptionObj) {
-        return ((MediaDescription) descriptionObj).getIconBitmap();
-    }
-
-    public static Uri getIconUri(Object descriptionObj) {
-        return ((MediaDescription) descriptionObj).getIconUri();
-    }
-
-    public static Bundle getExtras(Object descriptionObj) {
-        return ((MediaDescription) descriptionObj).getExtras();
-    }
-
-    public static void writeToParcel(Object descriptionObj, Parcel dest, int flags) {
-        ((MediaDescription) descriptionObj).writeToParcel(dest, flags);
-    }
-
-    public static Object fromParcel(Parcel in) {
-        return MediaDescription.CREATOR.createFromParcel(in);
-    }
-
-    static class Builder {
-        public static Object newInstance() {
-            return new MediaDescription.Builder();
-        }
-
-
-        public static void setMediaId(Object builderObj, String mediaId) {
-            ((MediaDescription.Builder)builderObj).setMediaId(mediaId);
-        }
-
-        public static void setTitle(Object builderObj, CharSequence title) {
-            ((MediaDescription.Builder)builderObj).setTitle(title);
-        }
-
-        public static void setSubtitle(Object builderObj, CharSequence subtitle) {
-            ((MediaDescription.Builder)builderObj).setSubtitle(subtitle);
-        }
-
-        public static void setDescription(Object builderObj, CharSequence description) {
-            ((MediaDescription.Builder)builderObj).setDescription(description);
-        }
-
-        public static void setIconBitmap(Object builderObj, Bitmap iconBitmap) {
-            ((MediaDescription.Builder)builderObj).setIconBitmap(iconBitmap);
-        }
-
-        public static void setIconUri(Object builderObj, Uri iconUri) {
-            ((MediaDescription.Builder)builderObj).setIconUri(iconUri);
-        }
-
-        public static void setExtras(Object builderObj, Bundle extras) {
-            ((MediaDescription.Builder)builderObj).setExtras(extras);
-        }
-
-        public static Object build(Object builderObj) {
-            return ((MediaDescription.Builder) builderObj).build();
-        }
-
-        private Builder() {
-        }
-    }
-
-    private MediaDescriptionCompatApi21() {
-    }
-}
diff --git a/media/api21/android/support/v4/media/MediaMetadataCompatApi21.java b/media/api21/android/support/v4/media/MediaMetadataCompatApi21.java
deleted file mode 100644
index 5ce873a..0000000
--- a/media/api21/android/support/v4/media/MediaMetadataCompatApi21.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.v4.media;
-
-import android.graphics.Bitmap;
-import android.media.MediaMetadata;
-import android.media.Rating;
-import android.os.Parcel;
-
-import androidx.annotation.RequiresApi;
-
-import java.util.Set;
-
-@RequiresApi(21)
-class MediaMetadataCompatApi21 {
-    public static Set<String> keySet(Object metadataObj) {
-        return ((MediaMetadata)metadataObj).keySet();
-    }
-
-    public static Bitmap getBitmap(Object metadataObj, String key) {
-        return ((MediaMetadata)metadataObj).getBitmap(key);
-    }
-
-    public static long getLong(Object metadataObj, String key) {
-        return ((MediaMetadata)metadataObj).getLong(key);
-    }
-
-    public static Object getRating(Object metadataObj, String key) {
-        return ((MediaMetadata)metadataObj).getRating(key);
-    }
-
-    public static CharSequence getText(Object metadataObj, String key) {
-        return ((MediaMetadata) metadataObj).getText(key);
-    }
-
-    public static void writeToParcel(Object metadataObj, Parcel dest, int flags) {
-        ((MediaMetadata) metadataObj).writeToParcel(dest, flags);
-    }
-
-    public static Object createFromParcel(Parcel in) {
-        return MediaMetadata.CREATOR.createFromParcel(in);
-    }
-
-    public static class Builder {
-        public static Object newInstance() {
-            return new MediaMetadata.Builder();
-        }
-
-        public static void putBitmap(Object builderObj, String key, Bitmap value) {
-            ((MediaMetadata.Builder)builderObj).putBitmap(key, value);
-        }
-
-        public static void putLong(Object builderObj, String key, long value) {
-            ((MediaMetadata.Builder)builderObj).putLong(key, value);
-        }
-
-        public static void putRating(Object builderObj, String key, Object ratingObj) {
-            ((MediaMetadata.Builder)builderObj).putRating(key, (Rating)ratingObj);
-        }
-
-        public static void putText(Object builderObj, String key, CharSequence value) {
-            ((MediaMetadata.Builder) builderObj).putText(key, value);
-        }
-
-        public static void putString(Object builderObj, String key, String value) {
-            ((MediaMetadata.Builder) builderObj).putString(key, value);
-        }
-
-        public static Object build(Object builderObj) {
-            return ((MediaMetadata.Builder)builderObj).build();
-        }
-
-        private Builder() {
-        }
-    }
-
-    private MediaMetadataCompatApi21() {
-    }
-}
diff --git a/media/api21/android/support/v4/media/session/MediaControllerCompatApi21.java b/media/api21/android/support/v4/media/session/MediaControllerCompatApi21.java
deleted file mode 100644
index a7fb3fe..0000000
--- a/media/api21/android/support/v4/media/session/MediaControllerCompatApi21.java
+++ /dev/null
@@ -1,334 +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 android.support.v4.media.session;
-
-import android.app.Activity;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.media.AudioAttributes;
-import android.media.AudioManager;
-import android.media.MediaMetadata;
-import android.media.Rating;
-import android.media.session.MediaController;
-import android.media.session.MediaSession;
-import android.media.session.PlaybackState;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.ResultReceiver;
-import android.view.KeyEvent;
-
-import androidx.annotation.RequiresApi;
-
-import java.util.ArrayList;
-import java.util.List;
-
-@RequiresApi(21)
-class MediaControllerCompatApi21 {
-    public static Object fromToken(Context context, Object sessionToken) {
-        return new MediaController(context, (MediaSession.Token) sessionToken);
-    }
-
-    public static Object createCallback(Callback callback) {
-        return new CallbackProxy<Callback>(callback);
-    }
-
-    public static void registerCallback(Object controllerObj, Object callbackObj, Handler handler) {
-        ((MediaController) controllerObj).registerCallback(
-                (MediaController.Callback)callbackObj, handler);
-    }
-
-    public static void unregisterCallback(Object controllerObj, Object callbackObj) {
-        ((MediaController) controllerObj)
-                .unregisterCallback((MediaController.Callback) callbackObj);
-    }
-
-    public static void setMediaController(Activity activity, Object controllerObj) {
-        activity.setMediaController((MediaController) controllerObj);
-    }
-
-    public static Object getMediaController(Activity activity) {
-        return activity.getMediaController();
-    }
-
-    public static Object getSessionToken(Object controllerObj) {
-        return ((MediaController) controllerObj).getSessionToken();
-    }
-
-    public static Object getTransportControls(Object controllerObj) {
-        return ((MediaController)controllerObj).getTransportControls();
-    }
-
-    public static Object getPlaybackState(Object controllerObj) {
-        return ((MediaController)controllerObj).getPlaybackState();
-    }
-
-    public static Object getMetadata(Object controllerObj) {
-        return ((MediaController)controllerObj).getMetadata();
-    }
-
-    public static List<Object> getQueue(Object controllerObj) {
-        List<MediaSession.QueueItem> queue = ((MediaController) controllerObj).getQueue();
-        if (queue == null) {
-            return null;
-        }
-        List<Object> queueObjs = new ArrayList<Object>(queue);
-        return queueObjs;
-    }
-
-    public static CharSequence getQueueTitle(Object controllerObj) {
-        return ((MediaController) controllerObj).getQueueTitle();
-    }
-
-    public static Bundle getExtras(Object controllerObj) {
-        return ((MediaController) controllerObj).getExtras();
-    }
-
-    public static int getRatingType(Object controllerObj) {
-        return ((MediaController) controllerObj).getRatingType();
-    }
-
-    public static long getFlags(Object controllerObj) {
-        return ((MediaController) controllerObj).getFlags();
-    }
-
-    public static Object getPlaybackInfo(Object controllerObj) {
-        return ((MediaController) controllerObj).getPlaybackInfo();
-    }
-
-    public static PendingIntent getSessionActivity(Object controllerObj) {
-        return ((MediaController) controllerObj).getSessionActivity();
-    }
-
-    public static boolean dispatchMediaButtonEvent(Object controllerObj, KeyEvent event) {
-        return ((MediaController) controllerObj).dispatchMediaButtonEvent(event);
-    }
-
-    public static void setVolumeTo(Object controllerObj, int value, int flags) {
-        ((MediaController) controllerObj).setVolumeTo(value, flags);
-    }
-
-    public static void adjustVolume(Object controllerObj, int direction, int flags) {
-        ((MediaController) controllerObj).adjustVolume(direction, flags);
-    }
-
-    public static void sendCommand(Object controllerObj,
-            String command, Bundle params, ResultReceiver cb) {
-        ((MediaController) controllerObj).sendCommand(command, params, cb);
-    }
-
-    public static String getPackageName(Object controllerObj) {
-        return ((MediaController) controllerObj).getPackageName();
-    }
-
-    public static class TransportControls {
-        public static void play(Object controlsObj) {
-            ((MediaController.TransportControls)controlsObj).play();
-        }
-
-        public static void pause(Object controlsObj) {
-            ((MediaController.TransportControls)controlsObj).pause();
-        }
-
-        public static void stop(Object controlsObj) {
-            ((MediaController.TransportControls)controlsObj).stop();
-        }
-
-        public static void seekTo(Object controlsObj, long pos) {
-            ((MediaController.TransportControls)controlsObj).seekTo(pos);
-        }
-
-        public static void fastForward(Object controlsObj) {
-            ((MediaController.TransportControls)controlsObj).fastForward();
-        }
-
-        public static void rewind(Object controlsObj) {
-            ((MediaController.TransportControls)controlsObj).rewind();
-        }
-
-        public static void skipToNext(Object controlsObj) {
-            ((MediaController.TransportControls)controlsObj).skipToNext();
-        }
-
-        public static void skipToPrevious(Object controlsObj) {
-            ((MediaController.TransportControls)controlsObj).skipToPrevious();
-        }
-
-        public static void setRating(Object controlsObj, Object ratingObj) {
-            ((MediaController.TransportControls)controlsObj).setRating((Rating)ratingObj);
-        }
-
-        public static void playFromMediaId(Object controlsObj, String mediaId, Bundle extras) {
-            ((MediaController.TransportControls) controlsObj).playFromMediaId(mediaId, extras);
-        }
-
-        public static void playFromSearch(Object controlsObj, String query, Bundle extras) {
-            ((MediaController.TransportControls) controlsObj).playFromSearch(query, extras);
-        }
-
-        public static void skipToQueueItem(Object controlsObj, long id) {
-            ((MediaController.TransportControls) controlsObj).skipToQueueItem(id);
-        }
-
-        public static void sendCustomAction(Object controlsObj, String action, Bundle args) {
-            ((MediaController.TransportControls) controlsObj).sendCustomAction(action, args);
-        }
-
-        private TransportControls() {
-        }
-    }
-
-    public static class PlaybackInfo {
-        public static int getPlaybackType(Object volumeInfoObj) {
-            return ((MediaController.PlaybackInfo)volumeInfoObj).getPlaybackType();
-        }
-
-        public static AudioAttributes getAudioAttributes(Object volumeInfoObj) {
-            return ((MediaController.PlaybackInfo) volumeInfoObj).getAudioAttributes();
-        }
-
-        public static int getLegacyAudioStream(Object volumeInfoObj) {
-            AudioAttributes attrs = getAudioAttributes(volumeInfoObj);
-            return toLegacyStreamType(attrs);
-        }
-
-        public static int getVolumeControl(Object volumeInfoObj) {
-            return ((MediaController.PlaybackInfo)volumeInfoObj).getVolumeControl();
-        }
-
-        public static int getMaxVolume(Object volumeInfoObj) {
-            return ((MediaController.PlaybackInfo)volumeInfoObj).getMaxVolume();
-        }
-
-        public static int getCurrentVolume(Object volumeInfoObj) {
-            return ((MediaController.PlaybackInfo)volumeInfoObj).getCurrentVolume();
-        }
-
-        // This is copied from AudioAttributes.toLegacyStreamType. TODO This
-        // either needs to be kept in sync with that one or toLegacyStreamType
-        // needs to be made public so it can be used by the support lib.
-        private static final int FLAG_SCO = 0x1 << 2;
-        private static final int STREAM_BLUETOOTH_SCO = 6;
-        private static final int STREAM_SYSTEM_ENFORCED = 7;
-        private static int toLegacyStreamType(AudioAttributes aa) {
-            // flags to stream type mapping
-            if ((aa.getFlags() & AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
-                    == AudioAttributes.FLAG_AUDIBILITY_ENFORCED) {
-                return STREAM_SYSTEM_ENFORCED;
-            }
-            if ((aa.getFlags() & FLAG_SCO) == FLAG_SCO) {
-                return STREAM_BLUETOOTH_SCO;
-            }
-
-            // usage to stream type mapping
-            switch (aa.getUsage()) {
-                case AudioAttributes.USAGE_MEDIA:
-                case AudioAttributes.USAGE_GAME:
-                case AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY:
-                case AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE:
-                    return AudioManager.STREAM_MUSIC;
-                case AudioAttributes.USAGE_ASSISTANCE_SONIFICATION:
-                    return AudioManager.STREAM_SYSTEM;
-                case AudioAttributes.USAGE_VOICE_COMMUNICATION:
-                    return AudioManager.STREAM_VOICE_CALL;
-                case AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING:
-                    return AudioManager.STREAM_DTMF;
-                case AudioAttributes.USAGE_ALARM:
-                    return AudioManager.STREAM_ALARM;
-                case AudioAttributes.USAGE_NOTIFICATION_RINGTONE:
-                    return AudioManager.STREAM_RING;
-                case AudioAttributes.USAGE_NOTIFICATION:
-                case AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST:
-                case AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT:
-                case AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED:
-                case AudioAttributes.USAGE_NOTIFICATION_EVENT:
-                    return AudioManager.STREAM_NOTIFICATION;
-                case AudioAttributes.USAGE_UNKNOWN:
-                default:
-                    return AudioManager.STREAM_MUSIC;
-            }
-        }
-
-        private PlaybackInfo() {
-        }
-    }
-
-    public static interface Callback {
-        public void onSessionDestroyed();
-        public void onSessionEvent(String event, Bundle extras);
-        public void onPlaybackStateChanged(Object stateObj);
-        public void onMetadataChanged(Object metadataObj);
-        public void onQueueChanged(List<?> queue);
-        public void onQueueTitleChanged(CharSequence title);
-        public void onExtrasChanged(Bundle extras);
-        public void onAudioInfoChanged(int type, int stream, int control, int max, int current);
-    }
-
-    static class CallbackProxy<T extends Callback> extends MediaController.Callback {
-        protected final T mCallback;
-
-        public CallbackProxy(T callback) {
-            mCallback = callback;
-        }
-
-        @Override
-        public void onSessionDestroyed() {
-            mCallback.onSessionDestroyed();
-        }
-
-        @Override
-        public void onSessionEvent(String event, Bundle extras) {
-            MediaSessionCompat.ensureClassLoader(extras);
-            mCallback.onSessionEvent(event, extras);
-        }
-
-        @Override
-        public void onPlaybackStateChanged(PlaybackState state) {
-            mCallback.onPlaybackStateChanged(state);
-        }
-
-        @Override
-        public void onMetadataChanged(MediaMetadata metadata) {
-            mCallback.onMetadataChanged(metadata);
-        }
-
-        @Override
-        public void onQueueChanged(List<MediaSession.QueueItem> queue) {
-            mCallback.onQueueChanged(queue);
-        }
-
-        @Override
-        public void onQueueTitleChanged(CharSequence title) {
-            mCallback.onQueueTitleChanged(title);
-        }
-
-        @Override
-        public void onExtrasChanged(Bundle extras) {
-            MediaSessionCompat.ensureClassLoader(extras);
-            mCallback.onExtrasChanged(extras);
-        }
-
-        @Override
-        public void onAudioInfoChanged(MediaController.PlaybackInfo info){
-            mCallback.onAudioInfoChanged(info.getPlaybackType(),
-                    PlaybackInfo.getLegacyAudioStream(info), info.getVolumeControl(),
-                    info.getMaxVolume(), info.getCurrentVolume());
-        }
-    }
-
-    private MediaControllerCompatApi21() {
-    }
-}
diff --git a/media/api21/android/support/v4/media/session/MediaSessionCompatApi21.java b/media/api21/android/support/v4/media/session/MediaSessionCompatApi21.java
deleted file mode 100644
index 7d36c4a..0000000
--- a/media/api21/android/support/v4/media/session/MediaSessionCompatApi21.java
+++ /dev/null
@@ -1,283 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.v4.media.session;
-
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
-import android.media.AudioAttributes;
-import android.media.MediaDescription;
-import android.media.MediaMetadata;
-import android.media.Rating;
-import android.media.VolumeProvider;
-import android.media.session.MediaSession;
-import android.media.session.PlaybackState;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Parcelable;
-import android.os.ResultReceiver;
-import android.util.Log;
-
-import androidx.annotation.RequiresApi;
-
-import java.lang.reflect.Field;
-import java.util.ArrayList;
-import java.util.List;
-
-@RequiresApi(21)
-class MediaSessionCompatApi21 {
-    static final String TAG = "MediaSessionCompatApi21";
-
-    public static Object createSession(Context context, String tag) {
-        return new MediaSession(context, tag);
-    }
-
-    public static Object verifySession(Object mediaSession) {
-        if (mediaSession instanceof MediaSession) {
-            return mediaSession;
-        }
-        throw new IllegalArgumentException("mediaSession is not a valid MediaSession object");
-    }
-
-    public static Object verifyToken(Object token) {
-        if (token instanceof MediaSession.Token) {
-            return token;
-        }
-        throw new IllegalArgumentException("token is not a valid MediaSession.Token object");
-    }
-
-    public static Object createCallback(Callback callback) {
-        return new CallbackProxy<Callback>(callback);
-    }
-
-    public static void setCallback(Object sessionObj, Object callbackObj, Handler handler) {
-        ((MediaSession) sessionObj).setCallback((MediaSession.Callback) callbackObj, handler);
-    }
-
-    public static void setFlags(Object sessionObj, int flags) {
-        ((MediaSession)sessionObj).setFlags(flags);
-    }
-
-    public static void setPlaybackToLocal(Object sessionObj, int stream) {
-        // TODO update APIs to use support version of AudioAttributes
-        AudioAttributes.Builder bob = new AudioAttributes.Builder();
-        bob.setLegacyStreamType(stream);
-        ((MediaSession) sessionObj).setPlaybackToLocal(bob.build());
-    }
-
-    public static void setPlaybackToRemote(Object sessionObj, Object volumeProviderObj) {
-        ((MediaSession)sessionObj).setPlaybackToRemote((VolumeProvider)volumeProviderObj);
-    }
-
-    public static void setActive(Object sessionObj, boolean active) {
-        ((MediaSession)sessionObj).setActive(active);
-    }
-
-    public static boolean isActive(Object sessionObj) {
-        return ((MediaSession)sessionObj).isActive();
-    }
-
-    public static void sendSessionEvent(Object sessionObj, String event, Bundle extras) {
-        ((MediaSession)sessionObj).sendSessionEvent(event, extras);
-    }
-
-    public static void release(Object sessionObj) {
-        ((MediaSession)sessionObj).release();
-    }
-
-    public static Parcelable getSessionToken(Object sessionObj) {
-        return ((MediaSession)sessionObj).getSessionToken();
-    }
-
-    public static void setPlaybackState(Object sessionObj, Object stateObj) {
-        ((MediaSession)sessionObj).setPlaybackState((PlaybackState)stateObj);
-    }
-
-    public static void setMetadata(Object sessionObj, Object metadataObj) {
-        ((MediaSession)sessionObj).setMetadata((MediaMetadata)metadataObj);
-    }
-
-    public static void setSessionActivity(Object sessionObj, PendingIntent pi) {
-        ((MediaSession) sessionObj).setSessionActivity(pi);
-    }
-
-    public static void setMediaButtonReceiver(Object sessionObj, PendingIntent pi) {
-        ((MediaSession) sessionObj).setMediaButtonReceiver(pi);
-    }
-
-    public static void setQueue(Object sessionObj, List<Object> queueObjs) {
-        if (queueObjs == null) {
-            ((MediaSession) sessionObj).setQueue(null);
-            return;
-        }
-        ArrayList<MediaSession.QueueItem> queue = new ArrayList<MediaSession.QueueItem>();
-        for (Object itemObj : queueObjs) {
-            queue.add((MediaSession.QueueItem) itemObj);
-        }
-        ((MediaSession) sessionObj).setQueue(queue);
-    }
-
-    public static void setQueueTitle(Object sessionObj, CharSequence title) {
-        ((MediaSession) sessionObj).setQueueTitle(title);
-    }
-
-    public static void setExtras(Object sessionObj, Bundle extras) {
-        ((MediaSession) sessionObj).setExtras(extras);
-    }
-
-    public static boolean hasCallback(Object sessionObj) {
-        Field callbackField = null;
-        try {
-            callbackField = sessionObj.getClass().getDeclaredField("mCallback");
-            if (callbackField != null) {
-                callbackField.setAccessible(true);
-                return callbackField.get(sessionObj) != null;
-            }
-        } catch (NoSuchFieldException | IllegalAccessException e) {
-            Log.w(TAG, "Failed to get mCallback object.");
-        }
-        return false;
-    }
-
-    interface Callback {
-        void onCommand(String command, Bundle extras, ResultReceiver cb);
-        boolean onMediaButtonEvent(Intent mediaButtonIntent);
-        void onPlay();
-        void onPlayFromMediaId(String mediaId, Bundle extras);
-        void onPlayFromSearch(String search, Bundle extras);
-        void onSkipToQueueItem(long id);
-        void onPause();
-        void onSkipToNext();
-        void onSkipToPrevious();
-        void onFastForward();
-        void onRewind();
-        void onStop();
-        void onSeekTo(long position);
-        void onSetRating(Object ratingObject);
-        void onSetRating(Object ratingObject, Bundle extras);
-        void onCustomAction(String action, Bundle extras);
-    }
-
-    static class CallbackProxy<T extends Callback> extends MediaSession.Callback {
-        protected final T mCallback;
-
-        public CallbackProxy(T callback) {
-            mCallback = callback;
-        }
-
-        @Override
-        public void onCommand(String command, Bundle args, ResultReceiver cb) {
-            MediaSessionCompat.ensureClassLoader(args);
-            mCallback.onCommand(command, args, cb);
-        }
-
-        @Override
-        public boolean onMediaButtonEvent(Intent mediaButtonIntent) {
-            return mCallback.onMediaButtonEvent(mediaButtonIntent)
-                    || super.onMediaButtonEvent(mediaButtonIntent);
-        }
-
-        @Override
-        public void onPlay() {
-            mCallback.onPlay();
-        }
-
-        @Override
-        public void onPlayFromMediaId(String mediaId, Bundle extras) {
-            MediaSessionCompat.ensureClassLoader(extras);
-            mCallback.onPlayFromMediaId(mediaId, extras);
-        }
-
-        @Override
-        public void onPlayFromSearch(String search, Bundle extras) {
-            MediaSessionCompat.ensureClassLoader(extras);
-            mCallback.onPlayFromSearch(search, extras);
-        }
-
-        @Override
-        public void onSkipToQueueItem(long id) {
-            mCallback.onSkipToQueueItem(id);
-        }
-
-        @Override
-        public void onPause() {
-            mCallback.onPause();
-        }
-
-        @Override
-        public void onSkipToNext() {
-            mCallback.onSkipToNext();
-        }
-
-        @Override
-        public void onSkipToPrevious() {
-            mCallback.onSkipToPrevious();
-        }
-
-        @Override
-        public void onFastForward() {
-            mCallback.onFastForward();
-        }
-
-        @Override
-        public void onRewind() {
-            mCallback.onRewind();
-        }
-
-        @Override
-        public void onStop() {
-            mCallback.onStop();
-        }
-
-        @Override
-        public void onSeekTo(long pos) {
-            mCallback.onSeekTo(pos);
-        }
-
-        @Override
-        public void onSetRating(Rating rating) {
-            mCallback.onSetRating(rating);
-        }
-
-        @Override
-        public void onCustomAction(String action, Bundle extras) {
-            MediaSessionCompat.ensureClassLoader(extras);
-            mCallback.onCustomAction(action, extras);
-        }
-    }
-
-    static class QueueItem {
-
-        public static Object createItem(Object mediaDescription, long id) {
-            return new MediaSession.QueueItem((MediaDescription) mediaDescription, id);
-        }
-
-        public static Object getDescription(Object queueItem) {
-            return ((MediaSession.QueueItem) queueItem).getDescription();
-        }
-
-        public static long getQueueId(Object queueItem) {
-            return ((MediaSession.QueueItem) queueItem).getQueueId();
-        }
-
-        private QueueItem() {
-        }
-    }
-
-    private MediaSessionCompatApi21() {
-    }
-}
diff --git a/media/api21/android/support/v4/media/session/PlaybackStateCompatApi21.java b/media/api21/android/support/v4/media/session/PlaybackStateCompatApi21.java
deleted file mode 100644
index f4aa9fc..0000000
--- a/media/api21/android/support/v4/media/session/PlaybackStateCompatApi21.java
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.v4.media.session;
-
-import android.media.session.PlaybackState;
-import android.os.Bundle;
-
-import androidx.annotation.RequiresApi;
-
-import java.util.List;
-
-@RequiresApi(21)
-class PlaybackStateCompatApi21 {
-    public static int getState(Object stateObj) {
-        return ((PlaybackState)stateObj).getState();
-    }
-
-    public static long getPosition(Object stateObj) {
-        return ((PlaybackState)stateObj).getPosition();
-    }
-
-    public static long getBufferedPosition(Object stateObj) {
-        return ((PlaybackState)stateObj).getBufferedPosition();
-    }
-
-    public static float getPlaybackSpeed(Object stateObj) {
-        return ((PlaybackState)stateObj).getPlaybackSpeed();
-    }
-
-    public static long getActions(Object stateObj) {
-        return ((PlaybackState)stateObj).getActions();
-    }
-
-    public static CharSequence getErrorMessage(Object stateObj) {
-        return ((PlaybackState)stateObj).getErrorMessage();
-    }
-
-    public static long getLastPositionUpdateTime(Object stateObj) {
-        return ((PlaybackState)stateObj).getLastPositionUpdateTime();
-    }
-
-    public static List<Object> getCustomActions(Object stateObj) {
-        return (List)((PlaybackState)stateObj).getCustomActions();
-    }
-
-    public static long getActiveQueueItemId(Object stateObj) {
-        return ((PlaybackState)stateObj).getActiveQueueItemId();
-    }
-
-    public static Object newInstance(int state, long position, long bufferedPosition,
-            float speed, long actions, CharSequence errorMessage, long updateTime,
-            List<Object> customActions,
-            long activeItemId) {
-        PlaybackState.Builder stateObj = new PlaybackState.Builder();
-        stateObj.setState(state, position, speed, updateTime);
-        stateObj.setBufferedPosition(bufferedPosition);
-        stateObj.setActions(actions);
-        stateObj.setErrorMessage(errorMessage);
-        for (Object customAction : customActions) {
-            stateObj.addCustomAction((PlaybackState.CustomAction) customAction);
-        }
-        stateObj.setActiveQueueItemId(activeItemId);
-        return stateObj.build();
-    }
-
-    static final class CustomAction {
-        public static String getAction(Object customActionObj) {
-            return ((PlaybackState.CustomAction)customActionObj).getAction();
-        }
-
-        public static CharSequence getName(Object customActionObj) {
-            return ((PlaybackState.CustomAction)customActionObj).getName();
-        }
-
-        public static int getIcon(Object customActionObj) {
-            return ((PlaybackState.CustomAction)customActionObj).getIcon();
-        }
-        public static Bundle getExtras(Object customActionObj) {
-            return ((PlaybackState.CustomAction)customActionObj).getExtras();
-        }
-
-        public static Object newInstance(String action, CharSequence name,
-                int icon, Bundle extras) {
-            PlaybackState.CustomAction.Builder customActionObj =
-                    new PlaybackState.CustomAction.Builder(action, name, icon);
-            customActionObj.setExtras(extras);
-            return customActionObj.build();
-        }
-
-        private CustomAction() {
-        }
-    }
-
-    private PlaybackStateCompatApi21() {
-    }
-}
diff --git a/media/api21/androidx/media/MediaBrowserServiceCompatApi21.java b/media/api21/androidx/media/MediaBrowserServiceCompatApi21.java
deleted file mode 100644
index c9c1a28..0000000
--- a/media/api21/androidx/media/MediaBrowserServiceCompatApi21.java
+++ /dev/null
@@ -1,137 +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.media;
-
-import android.content.Context;
-import android.content.Intent;
-import android.media.browse.MediaBrowser;
-import android.media.session.MediaSession;
-import android.os.Bundle;
-import android.os.IBinder;
-import android.os.Parcel;
-import android.service.media.MediaBrowserService;
-import android.support.v4.media.session.MediaSessionCompat;
-
-import androidx.annotation.RequiresApi;
-
-import java.util.ArrayList;
-import java.util.List;
-
-@RequiresApi(21)
-class MediaBrowserServiceCompatApi21 {
-
-    public static Object createService(Context context, ServiceCompatProxy serviceProxy) {
-        return new MediaBrowserServiceAdaptor(context, serviceProxy);
-    }
-
-    public static void onCreate(Object serviceObj) {
-        ((MediaBrowserService) serviceObj).onCreate();
-    }
-
-    public static IBinder onBind(Object serviceObj, Intent intent) {
-        return ((MediaBrowserService) serviceObj).onBind(intent);
-    }
-
-    public static void setSessionToken(Object serviceObj, Object token) {
-        ((MediaBrowserService) serviceObj).setSessionToken((MediaSession.Token) token);
-    }
-
-    public static void notifyChildrenChanged(Object serviceObj, String parentId) {
-        ((MediaBrowserService) serviceObj).notifyChildrenChanged(parentId);
-    }
-
-    public interface ServiceCompatProxy {
-        BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints);
-        void onLoadChildren(String parentId, ResultWrapper<List<Parcel>> result);
-    }
-
-    static class ResultWrapper<T> {
-        MediaBrowserService.Result mResultObj;
-
-        ResultWrapper(MediaBrowserService.Result result) {
-            mResultObj = result;
-        }
-
-        public void sendResult(T result) {
-            if (result instanceof List) {
-                mResultObj.sendResult(parcelListToItemList((List<Parcel>)result));
-            } else if (result instanceof Parcel) {
-                Parcel parcel = (Parcel) result;
-                parcel.setDataPosition(0);
-                mResultObj.sendResult(MediaBrowser.MediaItem.CREATOR.createFromParcel(parcel));
-                parcel.recycle();
-            } else {
-                // The result is null or an invalid instance.
-                mResultObj.sendResult(null);
-            }
-        }
-
-        public void detach() {
-            mResultObj.detach();
-        }
-
-        List<MediaBrowser.MediaItem> parcelListToItemList(List<Parcel> parcelList) {
-            if (parcelList == null) {
-                return null;
-            }
-            List<MediaBrowser.MediaItem> items = new ArrayList<>();
-            for (Parcel parcel : parcelList) {
-                parcel.setDataPosition(0);
-                items.add(MediaBrowser.MediaItem.CREATOR.createFromParcel(parcel));
-                parcel.recycle();
-            }
-            return items;
-        }
-    }
-
-    static class BrowserRoot {
-        final String mRootId;
-        final Bundle mExtras;
-
-        BrowserRoot(String rootId, Bundle extras) {
-            mRootId = rootId;
-            mExtras = extras;
-        }
-    }
-
-    static class MediaBrowserServiceAdaptor extends MediaBrowserService {
-        final ServiceCompatProxy mServiceProxy;
-
-        MediaBrowserServiceAdaptor(Context context, ServiceCompatProxy serviceWrapper) {
-            attachBaseContext(context);
-            mServiceProxy = serviceWrapper;
-        }
-
-        @Override
-        public MediaBrowserService.BrowserRoot onGetRoot(String clientPackageName, int clientUid,
-                Bundle rootHints) {
-            MediaSessionCompat.ensureClassLoader(rootHints);
-            MediaBrowserServiceCompatApi21.BrowserRoot browserRoot = mServiceProxy.onGetRoot(
-                    clientPackageName, clientUid, rootHints == null ? null : new Bundle(rootHints));
-            return browserRoot == null ? null : new MediaBrowserService.BrowserRoot(
-                    browserRoot.mRootId, browserRoot.mExtras);
-        }
-
-        @Override
-        public void onLoadChildren(String parentId, Result<List<MediaBrowser.MediaItem>> result) {
-            mServiceProxy.onLoadChildren(parentId, new ResultWrapper<List<Parcel>>(result));
-        }
-    }
-
-    private MediaBrowserServiceCompatApi21() {
-    }
-}
diff --git a/media/api21/androidx/media/VolumeProviderCompatApi21.java b/media/api21/androidx/media/VolumeProviderCompatApi21.java
deleted file mode 100644
index 6d540b3..0000000
--- a/media/api21/androidx/media/VolumeProviderCompatApi21.java
+++ /dev/null
@@ -1,51 +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.media;
-
-import android.media.VolumeProvider;
-
-import androidx.annotation.RequiresApi;
-
-@RequiresApi(21)
-class VolumeProviderCompatApi21 {
-    public static Object createVolumeProvider(int volumeControl, int maxVolume, int currentVolume,
-            final Delegate delegate) {
-        return new VolumeProvider(volumeControl, maxVolume, currentVolume) {
-            @Override
-            public void onSetVolumeTo(int volume) {
-                delegate.onSetVolumeTo(volume);
-            }
-
-            @Override
-            public void onAdjustVolume(int direction) {
-                delegate.onAdjustVolume(direction);
-            }
-        };
-    }
-
-    public static void setCurrentVolume(Object volumeProviderObj, int currentVolume) {
-        ((VolumeProvider) volumeProviderObj).setCurrentVolume(currentVolume);
-    }
-
-    public interface Delegate {
-        void onSetVolumeTo(int volume);
-        void onAdjustVolume(int delta);
-    }
-
-    private VolumeProviderCompatApi21() {
-    }
-}
diff --git a/media/api22/android/support/v4/media/session/MediaSessionCompatApi22.java b/media/api22/android/support/v4/media/session/MediaSessionCompatApi22.java
deleted file mode 100644
index 9a44d8c..0000000
--- a/media/api22/android/support/v4/media/session/MediaSessionCompatApi22.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v4.media.session;
-
-import android.media.session.MediaSession;
-
-import androidx.annotation.RequiresApi;
-
-@RequiresApi(22)
-class MediaSessionCompatApi22 {
-
-    public static void setRatingType(Object sessionObj, int type) {
-        ((MediaSession) sessionObj).setRatingType(type);
-    }
-
-    private MediaSessionCompatApi22() {
-    }
-}
diff --git a/media/api22/android/support/v4/media/session/PlaybackStateCompatApi22.java b/media/api22/android/support/v4/media/session/PlaybackStateCompatApi22.java
deleted file mode 100644
index 1be825fb..0000000
--- a/media/api22/android/support/v4/media/session/PlaybackStateCompatApi22.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.v4.media.session;
-
-import android.media.session.PlaybackState;
-import android.os.Bundle;
-
-import androidx.annotation.RequiresApi;
-
-import java.util.List;
-
-@RequiresApi(22)
-class PlaybackStateCompatApi22 {
-    public static Bundle getExtras(Object stateObj) {
-        return ((PlaybackState)stateObj).getExtras();
-    }
-
-    public static Object newInstance(int state, long position, long bufferedPosition,
-            float speed, long actions, CharSequence errorMessage, long updateTime,
-            List<Object> customActions,
-            long activeItemId, Bundle extras) {
-        PlaybackState.Builder stateObj = new PlaybackState.Builder();
-        stateObj.setState(state, position, speed, updateTime);
-        stateObj.setBufferedPosition(bufferedPosition);
-        stateObj.setActions(actions);
-        stateObj.setErrorMessage(errorMessage);
-        for (Object customAction : customActions) {
-            stateObj.addCustomAction((PlaybackState.CustomAction) customAction);
-        }
-        stateObj.setActiveQueueItemId(activeItemId);
-        stateObj.setExtras(extras);
-        return stateObj.build();
-    }
-
-    private PlaybackStateCompatApi22() {
-    }
-}
diff --git a/media/api23/android/support/v4/media/MediaBrowserCompatApi23.java b/media/api23/android/support/v4/media/MediaBrowserCompatApi23.java
deleted file mode 100644
index 514b1e9..0000000
--- a/media/api23/android/support/v4/media/MediaBrowserCompatApi23.java
+++ /dev/null
@@ -1,67 +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 android.support.v4.media;
-
-import android.media.browse.MediaBrowser;
-import android.os.Parcel;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-
-@RequiresApi(23)
-class MediaBrowserCompatApi23 {
-
-    public static Object createItemCallback(ItemCallback callback) {
-        return new ItemCallbackProxy<>(callback);
-    }
-
-    public static void getItem(Object browserObj, String mediaId, Object itemCallbackObj) {
-        ((MediaBrowser) browserObj).getItem(mediaId, ((MediaBrowser.ItemCallback) itemCallbackObj));
-    }
-
-    interface ItemCallback {
-        void onItemLoaded(Parcel itemParcel);
-        void onError(@NonNull String itemId);
-    }
-
-    static class ItemCallbackProxy<T extends ItemCallback> extends MediaBrowser.ItemCallback {
-        protected final T mItemCallback;
-
-        public ItemCallbackProxy(T callback) {
-            mItemCallback = callback;
-        }
-
-        @Override
-        public void onItemLoaded(MediaBrowser.MediaItem item) {
-            if (item == null) {
-                mItemCallback.onItemLoaded(null);
-            } else {
-                Parcel parcel = Parcel.obtain();
-                item.writeToParcel(parcel, 0);
-                mItemCallback.onItemLoaded(parcel);
-            }
-        }
-
-        @Override
-        public void onError(@NonNull String itemId) {
-            mItemCallback.onError(itemId);
-        }
-    }
-
-    private MediaBrowserCompatApi23() {
-    }
-}
diff --git a/media/api23/android/support/v4/media/MediaDescriptionCompatApi23.java b/media/api23/android/support/v4/media/MediaDescriptionCompatApi23.java
deleted file mode 100644
index ee1b1a1..0000000
--- a/media/api23/android/support/v4/media/MediaDescriptionCompatApi23.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v4.media;
-
-import android.media.MediaDescription;
-import android.net.Uri;
-
-import androidx.annotation.RequiresApi;
-
-@RequiresApi(23)
-class MediaDescriptionCompatApi23 {
-    public static Uri getMediaUri(Object descriptionObj) {
-        return ((MediaDescription) descriptionObj).getMediaUri();
-    }
-
-    static class Builder {
-        public static void setMediaUri(Object builderObj, Uri mediaUri) {
-            ((MediaDescription.Builder)builderObj).setMediaUri(mediaUri);
-        }
-
-        private Builder() {
-        }
-    }
-
-    private MediaDescriptionCompatApi23() {
-    }
-}
diff --git a/media/api23/android/support/v4/media/session/MediaControllerCompatApi23.java b/media/api23/android/support/v4/media/session/MediaControllerCompatApi23.java
deleted file mode 100644
index c5137b5..0000000
--- a/media/api23/android/support/v4/media/session/MediaControllerCompatApi23.java
+++ /dev/null
@@ -1,39 +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 android.support.v4.media.session;
-
-import android.media.session.MediaController;
-import android.net.Uri;
-import android.os.Bundle;
-
-import androidx.annotation.RequiresApi;
-
-@RequiresApi(23)
-class MediaControllerCompatApi23 {
-
-    public static class TransportControls {
-        public static void playFromUri(Object controlsObj, Uri uri, Bundle extras) {
-            ((MediaController.TransportControls) controlsObj).playFromUri(uri, extras);
-        }
-
-        private TransportControls() {
-        }
-    }
-
-    private MediaControllerCompatApi23() {
-    }
-}
diff --git a/media/api23/android/support/v4/media/session/MediaSessionCompatApi23.java b/media/api23/android/support/v4/media/session/MediaSessionCompatApi23.java
deleted file mode 100644
index e95e13e..0000000
--- a/media/api23/android/support/v4/media/session/MediaSessionCompatApi23.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.v4.media.session;
-
-import android.net.Uri;
-import android.os.Bundle;
-
-import androidx.annotation.RequiresApi;
-
-@RequiresApi(23)
-class MediaSessionCompatApi23 {
-
-    public static Object createCallback(Callback callback) {
-        return new CallbackProxy<Callback>(callback);
-    }
-
-    public interface Callback extends MediaSessionCompatApi21.Callback {
-        public void onPlayFromUri(Uri uri, Bundle extras);
-    }
-
-    static class CallbackProxy<T extends Callback>
-            extends MediaSessionCompatApi21.CallbackProxy<T> {
-        public CallbackProxy(T callback) {
-            super(callback);
-        }
-
-        @Override
-        public void onPlayFromUri(Uri uri, Bundle extras) {
-            MediaSessionCompat.ensureClassLoader(extras);
-            mCallback.onPlayFromUri(uri, extras);
-        }
-    }
-
-    private MediaSessionCompatApi23() {
-    }
-}
diff --git a/media/api23/androidx/media/MediaBrowserServiceCompatApi23.java b/media/api23/androidx/media/MediaBrowserServiceCompatApi23.java
deleted file mode 100644
index 0292fb9..0000000
--- a/media/api23/androidx/media/MediaBrowserServiceCompatApi23.java
+++ /dev/null
@@ -1,51 +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.media;
-
-import android.content.Context;
-import android.media.browse.MediaBrowser;
-import android.os.Parcel;
-
-import androidx.annotation.RequiresApi;
-
-@RequiresApi(23)
-class MediaBrowserServiceCompatApi23 {
-
-    public static Object createService(Context context, ServiceCompatProxy serviceProxy) {
-        return new MediaBrowserServiceAdaptor(context, serviceProxy);
-    }
-
-    public interface ServiceCompatProxy extends MediaBrowserServiceCompatApi21.ServiceCompatProxy {
-        void onLoadItem(String itemId, MediaBrowserServiceCompatApi21.ResultWrapper<Parcel> result);
-    }
-
-    static class MediaBrowserServiceAdaptor extends
-            MediaBrowserServiceCompatApi21.MediaBrowserServiceAdaptor {
-        MediaBrowserServiceAdaptor(Context context, ServiceCompatProxy serviceWrapper) {
-            super(context, serviceWrapper);
-        }
-
-        @Override
-        public void onLoadItem(String itemId, Result<MediaBrowser.MediaItem> result) {
-            ((ServiceCompatProxy) mServiceProxy).onLoadItem(itemId,
-                    new MediaBrowserServiceCompatApi21.ResultWrapper<Parcel>(result));
-        }
-    }
-
-    private MediaBrowserServiceCompatApi23() {
-    }
-}
diff --git a/media/api24/android/support/v4/media/session/MediaControllerCompatApi24.java b/media/api24/android/support/v4/media/session/MediaControllerCompatApi24.java
deleted file mode 100644
index f130ed0..0000000
--- a/media/api24/android/support/v4/media/session/MediaControllerCompatApi24.java
+++ /dev/null
@@ -1,51 +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 android.support.v4.media.session;
-
-import android.media.session.MediaController;
-import android.net.Uri;
-import android.os.Bundle;
-
-import androidx.annotation.RequiresApi;
-
-@RequiresApi(24)
-class MediaControllerCompatApi24 {
-
-    public static class TransportControls {
-        public static void prepare(Object controlsObj) {
-            ((MediaController.TransportControls) controlsObj).prepare();
-        }
-
-        public static void prepareFromMediaId(Object controlsObj, String mediaId, Bundle extras) {
-            ((MediaController.TransportControls) controlsObj).prepareFromMediaId(mediaId, extras);
-        }
-
-        public static void prepareFromSearch(Object controlsObj, String query, Bundle extras) {
-            ((MediaController.TransportControls) controlsObj).prepareFromSearch(query, extras);
-        }
-
-        public static void prepareFromUri(Object controlsObj, Uri uri, Bundle extras) {
-            ((MediaController.TransportControls) controlsObj).prepareFromUri(uri, extras);
-        }
-
-        private TransportControls() {
-        }
-    }
-
-    private MediaControllerCompatApi24() {
-    }
-}
diff --git a/media/api24/android/support/v4/media/session/MediaSessionCompatApi24.java b/media/api24/android/support/v4/media/session/MediaSessionCompatApi24.java
deleted file mode 100644
index 9ba0915..0000000
--- a/media/api24/android/support/v4/media/session/MediaSessionCompatApi24.java
+++ /dev/null
@@ -1,87 +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 android.support.v4.media.session;
-
-import android.media.session.MediaSession;
-import android.net.Uri;
-import android.os.Bundle;
-import android.util.Log;
-
-import androidx.annotation.RequiresApi;
-
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-
-@RequiresApi(24)
-class MediaSessionCompatApi24 {
-    private static final String TAG = "MediaSessionCompatApi24";
-
-    public static Object createCallback(Callback callback) {
-        return new CallbackProxy<Callback>(callback);
-    }
-
-    public static String getCallingPackage(Object sessionObj) {
-        MediaSession session = (MediaSession) sessionObj;
-        try {
-            Method getCallingPackageMethod = session.getClass().getMethod("getCallingPackage");
-            return (String) getCallingPackageMethod.invoke(session);
-        } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
-            Log.e(TAG, "Cannot execute MediaSession.getCallingPackage()", e);
-        }
-        return null;
-    }
-
-    public interface Callback extends MediaSessionCompatApi23.Callback {
-        public void onPrepare();
-        public void onPrepareFromMediaId(String mediaId, Bundle extras);
-        public void onPrepareFromSearch(String query, Bundle extras);
-        public void onPrepareFromUri(Uri uri, Bundle extras);
-    }
-
-    static class CallbackProxy<T extends Callback>
-            extends MediaSessionCompatApi23.CallbackProxy<T> {
-        public CallbackProxy(T callback) {
-            super(callback);
-        }
-
-        @Override
-        public void onPrepare() {
-            mCallback.onPrepare();
-        }
-
-        @Override
-        public void onPrepareFromMediaId(String mediaId, Bundle extras) {
-            MediaSessionCompat.ensureClassLoader(extras);
-            mCallback.onPrepareFromMediaId(mediaId, extras);
-        }
-
-        @Override
-        public void onPrepareFromSearch(String query, Bundle extras) {
-            MediaSessionCompat.ensureClassLoader(extras);
-            mCallback.onPrepareFromSearch(query, extras);
-        }
-
-        @Override
-        public void onPrepareFromUri(Uri uri, Bundle extras) {
-            MediaSessionCompat.ensureClassLoader(extras);
-            mCallback.onPrepareFromUri(uri, extras);
-        }
-    }
-
-    private MediaSessionCompatApi24() {
-    }
-}
diff --git a/media/api26/android/support/v4/media/MediaBrowserCompatApi26.java b/media/api26/android/support/v4/media/MediaBrowserCompatApi26.java
deleted file mode 100644
index 653f849..0000000
--- a/media/api26/android/support/v4/media/MediaBrowserCompatApi26.java
+++ /dev/null
@@ -1,73 +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 android.support.v4.media;
-
-import android.media.browse.MediaBrowser;
-import android.os.Bundle;
-import android.support.v4.media.session.MediaSessionCompat;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-
-import java.util.List;
-
-@RequiresApi(26)
-class MediaBrowserCompatApi26 {
-    static Object createSubscriptionCallback(SubscriptionCallback callback) {
-        return new SubscriptionCallbackProxy<>(callback);
-    }
-
-    public static void subscribe(Object browserObj, String parentId, Bundle options,
-            Object subscriptionCallbackObj) {
-        ((MediaBrowser) browserObj).subscribe(parentId, options,
-                (MediaBrowser.SubscriptionCallback) subscriptionCallbackObj);
-    }
-
-    public static void unsubscribe(Object browserObj, String parentId,
-            Object subscriptionCallbackObj) {
-        ((MediaBrowser) browserObj).unsubscribe(parentId,
-                (MediaBrowser.SubscriptionCallback) subscriptionCallbackObj);
-    }
-
-    interface SubscriptionCallback extends MediaBrowserCompatApi21.SubscriptionCallback {
-        void onChildrenLoaded(@NonNull String parentId, List<?> children, @NonNull Bundle options);
-        void onError(@NonNull String parentId, @NonNull  Bundle options);
-    }
-
-    static class SubscriptionCallbackProxy<T extends SubscriptionCallback>
-            extends MediaBrowserCompatApi21.SubscriptionCallbackProxy<T> {
-        SubscriptionCallbackProxy(T callback) {
-            super(callback);
-        }
-
-        @Override
-        public void onChildrenLoaded(@NonNull String parentId,
-                List<MediaBrowser.MediaItem> children, @NonNull Bundle options) {
-            MediaSessionCompat.ensureClassLoader(options);
-            mSubscriptionCallback.onChildrenLoaded(parentId, children, options);
-        }
-
-        @Override
-        public void onError(@NonNull String parentId, @NonNull Bundle options) {
-            MediaSessionCompat.ensureClassLoader(options);
-            mSubscriptionCallback.onError(parentId, options);
-        }
-    }
-
-    private MediaBrowserCompatApi26() {
-    }
-}
diff --git a/media/api26/androidx/media/MediaBrowserServiceCompatApi26.java b/media/api26/androidx/media/MediaBrowserServiceCompatApi26.java
deleted file mode 100644
index dd528c5..0000000
--- a/media/api26/androidx/media/MediaBrowserServiceCompatApi26.java
+++ /dev/null
@@ -1,97 +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.media;
-
-import android.content.Context;
-import android.media.browse.MediaBrowser;
-import android.os.Bundle;
-import android.os.Parcel;
-import android.service.media.MediaBrowserService;
-import android.support.v4.media.session.MediaSessionCompat;
-
-import androidx.annotation.RequiresApi;
-
-import java.util.ArrayList;
-import java.util.List;
-
-@RequiresApi(26)
-class MediaBrowserServiceCompatApi26 {
-    private static final String TAG = "MBSCompatApi26";
-
-    public static Object createService(Context context, ServiceCompatProxy serviceProxy) {
-        return new MediaBrowserServiceAdaptor(context, serviceProxy);
-    }
-
-    public static void notifyChildrenChanged(Object serviceObj, String parentId, Bundle options) {
-        ((MediaBrowserService) serviceObj).notifyChildrenChanged(parentId, options);
-    }
-
-    public static Bundle getBrowserRootHints(Object serviceObj) {
-        return ((MediaBrowserService) serviceObj).getBrowserRootHints();
-    }
-
-    public interface ServiceCompatProxy extends MediaBrowserServiceCompatApi23.ServiceCompatProxy {
-        void onLoadChildren(String parentId, ResultWrapper result, Bundle options);
-    }
-
-    static class ResultWrapper {
-        MediaBrowserService.Result mResultObj;
-
-        ResultWrapper(MediaBrowserService.Result result) {
-            mResultObj = result;
-        }
-
-        public void sendResult(List<Parcel> result) {
-            mResultObj.sendResult(parcelListToItemList(result));
-        }
-
-        public void detach() {
-            mResultObj.detach();
-        }
-
-        List<MediaBrowser.MediaItem> parcelListToItemList(List<Parcel> parcelList) {
-            if (parcelList == null) {
-                return null;
-            }
-            List<MediaBrowser.MediaItem> items = new ArrayList<>();
-            for (Parcel parcel : parcelList) {
-                parcel.setDataPosition(0);
-                items.add(MediaBrowser.MediaItem.CREATOR.createFromParcel(parcel));
-                parcel.recycle();
-            }
-            return items;
-        }
-    }
-
-    static class MediaBrowserServiceAdaptor extends
-            MediaBrowserServiceCompatApi23.MediaBrowserServiceAdaptor {
-        MediaBrowserServiceAdaptor(Context context, ServiceCompatProxy serviceWrapper) {
-            super(context, serviceWrapper);
-        }
-
-        @Override
-        public void onLoadChildren(String parentId, Result<List<MediaBrowser.MediaItem>> result,
-                Bundle options) {
-            MediaSessionCompat.ensureClassLoader(options);
-            ((ServiceCompatProxy) mServiceProxy).onLoadChildren(
-                    parentId, new ResultWrapper(result), options);
-        }
-    }
-
-    private MediaBrowserServiceCompatApi26() {
-    }
-}
diff --git a/media/build.gradle b/media/build.gradle
index a02908f..6bb6fdf 100644
--- a/media/build.gradle
+++ b/media/build.gradle
@@ -20,11 +20,6 @@
 android {
     sourceSets {
         main.java.srcDirs += [
-                'api21',
-                'api22',
-                'api23',
-                'api24',
-                'api26',
         ]
         main.res.srcDirs += 'src/main/res-public'
     }
diff --git a/media/src/main/java/android/support/v4/media/MediaBrowserCompat.java b/media/src/main/java/android/support/v4/media/MediaBrowserCompat.java
index e805ab3..8a1c08d 100644
--- a/media/src/main/java/android/support/v4/media/MediaBrowserCompat.java
+++ b/media/src/main/java/android/support/v4/media/MediaBrowserCompat.java
@@ -53,6 +53,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.ServiceConnection;
+import android.media.browse.MediaBrowser;
 import android.os.BadParcelableException;
 import android.os.Binder;
 import android.os.Build;
@@ -488,11 +489,11 @@
             if (itemObj == null || Build.VERSION.SDK_INT < 21) {
                 return null;
             }
-            int flags = MediaBrowserCompatApi21.MediaItem.getFlags(itemObj);
-            MediaDescriptionCompat description =
-                    MediaDescriptionCompat.fromMediaDescription(
-                            MediaBrowserCompatApi21.MediaItem.getDescription(itemObj));
-            return new MediaItem(description, flags);
+            MediaBrowser.MediaItem itemFwk = (MediaBrowser.MediaItem) itemObj;
+            int flags = itemFwk.getFlags();
+            MediaDescriptionCompat descriptionCompat =
+                    MediaDescriptionCompat.fromMediaDescription(itemFwk.getDescription());
+            return new MediaItem(descriptionCompat, flags);
         }
 
         /**
@@ -617,15 +618,14 @@
      * Callbacks for connection related events.
      */
     public static class ConnectionCallback {
-        final Object mConnectionCallbackObj;
+        final MediaBrowser.ConnectionCallback mConnectionCallbackFwk;
         ConnectionCallbackInternal mConnectionCallbackInternal;
 
         public ConnectionCallback() {
             if (Build.VERSION.SDK_INT >= 21) {
-                mConnectionCallbackObj =
-                        MediaBrowserCompatApi21.createConnectionCallback(new StubApi21());
+                mConnectionCallbackFwk = new ConnectionCallbackApi21();
             } else {
-                mConnectionCallbackObj = null;
+                mConnectionCallbackFwk = null;
             }
         }
 
@@ -669,8 +669,9 @@
             void onConnectionFailed();
         }
 
-        private class StubApi21 implements MediaBrowserCompatApi21.ConnectionCallback {
-            StubApi21() {
+        @RequiresApi(21)
+        private class ConnectionCallbackApi21 extends MediaBrowser.ConnectionCallback {
+            ConnectionCallbackApi21() {
             }
 
             @Override
@@ -703,20 +704,18 @@
      * Callbacks for subscription related events.
      */
     public static abstract class SubscriptionCallback {
-        final Object mSubscriptionCallbackObj;
+        final MediaBrowser.SubscriptionCallback mSubscriptionCallbackFwk;
         final IBinder mToken;
         WeakReference<Subscription> mSubscriptionRef;
 
         public SubscriptionCallback() {
             mToken = new Binder();
             if (Build.VERSION.SDK_INT >= 26) {
-                mSubscriptionCallbackObj =
-                        MediaBrowserCompatApi26.createSubscriptionCallback(new StubApi26());
+                mSubscriptionCallbackFwk = new SubscriptionCallbackApi26();
             } else if (Build.VERSION.SDK_INT >= 21) {
-                mSubscriptionCallbackObj =
-                        MediaBrowserCompatApi21.createSubscriptionCallback(new StubApi21());
+                mSubscriptionCallbackFwk = new SubscriptionCallbackApi21();
             } else {
-                mSubscriptionCallbackObj = null;
+                mSubscriptionCallbackFwk = null;
             }
         }
 
@@ -773,12 +772,14 @@
             mSubscriptionRef = new WeakReference<>(subscription);
         }
 
-        private class StubApi21 implements MediaBrowserCompatApi21.SubscriptionCallback {
-            StubApi21() {
+        @RequiresApi(21)
+        private class SubscriptionCallbackApi21 extends MediaBrowser.SubscriptionCallback {
+            SubscriptionCallbackApi21() {
             }
 
             @Override
-            public void onChildrenLoaded(@NonNull String parentId, List<?> children) {
+            public void onChildrenLoaded(@NonNull String parentId,
+                    List<MediaBrowser.MediaItem> children) {
                 Subscription sub = mSubscriptionRef == null ? null : mSubscriptionRef.get();
                 if (sub == null) {
                     SubscriptionCallback.this.onChildrenLoaded(
@@ -828,20 +829,23 @@
 
         }
 
-        private class StubApi26 extends StubApi21
-                implements MediaBrowserCompatApi26.SubscriptionCallback {
-            StubApi26() {
+        @RequiresApi(26)
+        private class SubscriptionCallbackApi26 extends SubscriptionCallbackApi21 {
+            SubscriptionCallbackApi26() {
             }
 
             @Override
-            public void onChildrenLoaded(@NonNull String parentId, List<?> children,
+            public void onChildrenLoaded(@NonNull String parentId,
+                    List<MediaBrowser.MediaItem> children,
                     @NonNull Bundle options) {
+                MediaSessionCompat.ensureClassLoader(options);
                 SubscriptionCallback.this.onChildrenLoaded(
                         parentId, MediaItem.fromMediaItemList(children), options);
             }
 
             @Override
             public void onError(@NonNull String parentId, @NonNull Bundle options) {
+                MediaSessionCompat.ensureClassLoader(options);
                 SubscriptionCallback.this.onError(parentId, options);
             }
         }
@@ -851,13 +855,13 @@
      * Callback for receiving the result of {@link #getItem}.
      */
     public static abstract class ItemCallback {
-        final Object mItemCallbackObj;
+        final MediaBrowser.ItemCallback mItemCallbackFwk;
 
         public ItemCallback() {
             if (Build.VERSION.SDK_INT >= 23) {
-                mItemCallbackObj = MediaBrowserCompatApi23.createItemCallback(new StubApi23());
+                mItemCallbackFwk = new ItemCallbackApi23();
             } else {
-                mItemCallbackObj = null;
+                mItemCallbackFwk = null;
             }
         }
 
@@ -877,21 +881,14 @@
         public void onError(@NonNull String itemId) {
         }
 
-        private class StubApi23 implements MediaBrowserCompatApi23.ItemCallback {
-            StubApi23() {
+        @RequiresApi(23)
+        private class ItemCallbackApi23 extends MediaBrowser.ItemCallback {
+            ItemCallbackApi23() {
             }
 
             @Override
-            public void onItemLoaded(Parcel itemParcel) {
-                if (itemParcel == null) {
-                    ItemCallback.this.onItemLoaded(null);
-                } else {
-                    itemParcel.setDataPosition(0);
-                    MediaItem item =
-                            MediaBrowserCompat.MediaItem.CREATOR.createFromParcel(itemParcel);
-                    itemParcel.recycle();
-                    ItemCallback.this.onItemLoaded(item);
-                }
+            public void onItemLoaded(MediaBrowser.MediaItem item) {
+                ItemCallback.this.onItemLoaded(MediaItem.fromMediaItem(item));
             }
 
             @Override
@@ -1620,7 +1617,7 @@
     static class MediaBrowserImplApi21 implements MediaBrowserImpl, MediaBrowserServiceCallbackImpl,
             ConnectionCallback.ConnectionCallbackInternal {
         final Context mContext;
-        protected final Object mBrowserObj;
+        protected final MediaBrowser mBrowserFwk;
         protected final Bundle mRootHints;
         protected final CallbackHandler mHandler = new CallbackHandler(this);
         private final ArrayMap<String, Subscription> mSubscriptions = new ArrayMap<>();
@@ -1637,13 +1634,13 @@
             mRootHints = (rootHints != null ? new Bundle(rootHints) : new Bundle());
             mRootHints.putInt(EXTRA_CLIENT_VERSION, CLIENT_VERSION_CURRENT);
             callback.setInternalConnectionCallback(this);
-            mBrowserObj = MediaBrowserCompatApi21.createBrowser(context, serviceComponent,
-                    callback.mConnectionCallbackObj, mRootHints);
+            mBrowserFwk = new MediaBrowser(context, serviceComponent,
+                    callback.mConnectionCallbackFwk, mRootHints);
         }
 
         @Override
         public void connect() {
-            MediaBrowserCompatApi21.connect(mBrowserObj);
+            mBrowserFwk.connect();
         }
 
         @Override
@@ -1655,29 +1652,29 @@
                     Log.i(TAG, "Remote error unregistering client messenger." );
                 }
             }
-            MediaBrowserCompatApi21.disconnect(mBrowserObj);
+            mBrowserFwk.disconnect();
         }
 
         @Override
         public boolean isConnected() {
-            return MediaBrowserCompatApi21.isConnected(mBrowserObj);
+            return mBrowserFwk.isConnected();
         }
 
         @Override
         public ComponentName getServiceComponent() {
-            return MediaBrowserCompatApi21.getServiceComponent(mBrowserObj);
+            return mBrowserFwk.getServiceComponent();
         }
 
         @NonNull
         @Override
         public String getRoot() {
-            return MediaBrowserCompatApi21.getRoot(mBrowserObj);
+            return mBrowserFwk.getRoot();
         }
 
         @Nullable
         @Override
         public Bundle getExtras() {
-            return MediaBrowserCompatApi21.getExtras(mBrowserObj);
+            return mBrowserFwk.getExtras();
         }
 
         @NonNull
@@ -1685,7 +1682,7 @@
         public MediaSessionCompat.Token getSessionToken() {
             if (mMediaSessionToken == null) {
                 mMediaSessionToken = MediaSessionCompat.Token.fromToken(
-                        MediaBrowserCompatApi21.getSessionToken(mBrowserObj));
+                        mBrowserFwk.getSessionToken());
             }
             return mMediaSessionToken;
         }
@@ -1706,8 +1703,7 @@
             if (mServiceBinderWrapper == null) {
                 // TODO: When MediaBrowser is connected to framework's MediaBrowserService,
                 // subscribe with options won't work properly.
-                MediaBrowserCompatApi21.subscribe(
-                        mBrowserObj, parentId, callback.mSubscriptionCallbackObj);
+                mBrowserFwk.subscribe(parentId, callback.mSubscriptionCallbackFwk);
             } else {
                 try {
                     mServiceBinderWrapper.addSubscription(
@@ -1729,7 +1725,7 @@
 
             if (mServiceBinderWrapper == null) {
                 if (callback == null) {
-                    MediaBrowserCompatApi21.unsubscribe(mBrowserObj, parentId);
+                    mBrowserFwk.unsubscribe(parentId);
                 } else {
                     final List<SubscriptionCallback> callbacks = sub.getCallbacks();
                     final List<Bundle> optionsList = sub.getOptionsList();
@@ -1740,7 +1736,7 @@
                         }
                     }
                     if (callbacks.size() == 0) {
-                        MediaBrowserCompatApi21.unsubscribe(mBrowserObj, parentId);
+                        mBrowserFwk.unsubscribe(parentId);
                     }
                 }
             } else {
@@ -1782,7 +1778,7 @@
             if (cb == null) {
                 throw new IllegalArgumentException("cb is null");
             }
-            if (!MediaBrowserCompatApi21.isConnected(mBrowserObj)) {
+            if (!mBrowserFwk.isConnected()) {
                 Log.i(TAG, "Not connected, unable to retrieve the MediaItem.");
                 mHandler.post(new Runnable() {
                     @Override
@@ -1889,7 +1885,7 @@
 
         @Override
         public void onConnected() {
-            Bundle extras = MediaBrowserCompatApi21.getExtras(mBrowserObj);
+            Bundle extras = mBrowserFwk.getExtras();
             if (extras == null) {
                 return;
             }
@@ -1909,7 +1905,7 @@
                     BundleCompat.getBinder(extras, EXTRA_SESSION_BINDER));
             if (sessionToken != null) {
                 mMediaSessionToken = MediaSessionCompat.Token.fromToken(
-                        MediaBrowserCompatApi21.getSessionToken(mBrowserObj), sessionToken);
+                        mBrowserFwk.getSessionToken(), sessionToken);
             }
         }
 
@@ -1993,7 +1989,7 @@
         @Override
         public void getItem(@NonNull final String mediaId, @NonNull final ItemCallback cb) {
             if (mServiceBinderWrapper == null) {
-                MediaBrowserCompatApi23.getItem(mBrowserObj, mediaId, cb.mItemCallbackObj);
+                mBrowserFwk.getItem(mediaId, cb.mItemCallbackFwk);
             } else {
                 super.getItem(mediaId, cb);
             }
@@ -2014,11 +2010,9 @@
             // This is to prevent ClassNotFoundException when options has Parcelable in it.
             if (mServiceBinderWrapper == null || mServiceVersion < SERVICE_VERSION_2) {
                 if (options == null) {
-                    MediaBrowserCompatApi21.subscribe(
-                            mBrowserObj, parentId, callback.mSubscriptionCallbackObj);
+                    mBrowserFwk.subscribe(parentId, callback.mSubscriptionCallbackFwk);
                 } else {
-                    MediaBrowserCompatApi26.subscribe(
-                            mBrowserObj, parentId, options, callback.mSubscriptionCallbackObj);
+                    mBrowserFwk.subscribe(parentId, options, callback.mSubscriptionCallbackFwk);
                 }
             } else {
                 super.subscribe(parentId, options, callback);
@@ -2031,10 +2025,9 @@
             // This is to prevent ClassNotFoundException when options has Parcelable in it.
             if (mServiceBinderWrapper == null || mServiceVersion < SERVICE_VERSION_2) {
                 if (callback == null) {
-                    MediaBrowserCompatApi21.unsubscribe(mBrowserObj, parentId);
+                    mBrowserFwk.unsubscribe(parentId);
                 } else {
-                    MediaBrowserCompatApi26.unsubscribe(mBrowserObj, parentId,
-                            callback.mSubscriptionCallbackObj);
+                    mBrowserFwk.unsubscribe(parentId, callback.mSubscriptionCallbackFwk);
                 }
             } else {
                 super.unsubscribe(parentId, callback);
diff --git a/media/src/main/java/android/support/v4/media/MediaDescriptionCompat.java b/media/src/main/java/android/support/v4/media/MediaDescriptionCompat.java
index 5d41631..d757bbf 100644
--- a/media/src/main/java/android/support/v4/media/MediaDescriptionCompat.java
+++ b/media/src/main/java/android/support/v4/media/MediaDescriptionCompat.java
@@ -18,6 +18,7 @@
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
 
 import android.graphics.Bitmap;
+import android.media.MediaDescription;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
@@ -185,7 +186,7 @@
     /**
      * A cached copy of the equivalent framework object.
      */
-    private Object mDescriptionObj;
+    private MediaDescription mDescriptionFwk;
 
     MediaDescriptionCompat(String mediaId, CharSequence title, CharSequence subtitle,
             CharSequence description, Bitmap icon, Uri iconUri, Bundle extras, Uri mediaUri) {
@@ -308,7 +309,7 @@
             dest.writeBundle(mExtras);
             dest.writeParcelable(mMediaUri, flags);
         } else {
-            MediaDescriptionCompatApi21.writeToParcel(getMediaDescription(), dest, flags);
+            ((MediaDescription) getMediaDescription()).writeToParcel(dest, flags);
         }
     }
 
@@ -329,16 +330,16 @@
      *         null if none.
      */
     public Object getMediaDescription() {
-        if (mDescriptionObj != null || Build.VERSION.SDK_INT < 21) {
-            return mDescriptionObj;
+        if (mDescriptionFwk != null || Build.VERSION.SDK_INT < 21) {
+            return mDescriptionFwk;
         }
-        Object bob = MediaDescriptionCompatApi21.Builder.newInstance();
-        MediaDescriptionCompatApi21.Builder.setMediaId(bob, mMediaId);
-        MediaDescriptionCompatApi21.Builder.setTitle(bob, mTitle);
-        MediaDescriptionCompatApi21.Builder.setSubtitle(bob, mSubtitle);
-        MediaDescriptionCompatApi21.Builder.setDescription(bob, mDescription);
-        MediaDescriptionCompatApi21.Builder.setIconBitmap(bob, mIcon);
-        MediaDescriptionCompatApi21.Builder.setIconUri(bob, mIconUri);
+        MediaDescription.Builder bob = new MediaDescription.Builder();
+        bob.setMediaId(mMediaId);
+        bob.setTitle(mTitle);
+        bob.setSubtitle(mSubtitle);
+        bob.setDescription(mDescription);
+        bob.setIconBitmap(mIcon);
+        bob.setIconUri(mIconUri);
         // Media URI was not added until API 23, so add it to the Bundle of extras to
         // ensure the data is not lost - this ensures that
         // fromMediaDescription(getMediaDescription(mediaDescriptionCompat)) returns
@@ -351,13 +352,13 @@
             }
             extras.putParcelable(DESCRIPTION_KEY_MEDIA_URI, mMediaUri);
         }
-        MediaDescriptionCompatApi21.Builder.setExtras(bob, extras);
+        bob.setExtras(extras);
         if (Build.VERSION.SDK_INT >= 23) {
-            MediaDescriptionCompatApi23.Builder.setMediaUri(bob, mMediaUri);
+            bob.setMediaUri(mMediaUri);
         }
-        mDescriptionObj = MediaDescriptionCompatApi21.Builder.build(bob);
+        mDescriptionFwk = bob.build();
 
-        return mDescriptionObj;
+        return mDescriptionFwk;
     }
 
     /**
@@ -375,13 +376,14 @@
     public static MediaDescriptionCompat fromMediaDescription(Object descriptionObj) {
         if (descriptionObj != null && Build.VERSION.SDK_INT >= 21) {
             Builder bob = new Builder();
-            bob.setMediaId(MediaDescriptionCompatApi21.getMediaId(descriptionObj));
-            bob.setTitle(MediaDescriptionCompatApi21.getTitle(descriptionObj));
-            bob.setSubtitle(MediaDescriptionCompatApi21.getSubtitle(descriptionObj));
-            bob.setDescription(MediaDescriptionCompatApi21.getDescription(descriptionObj));
-            bob.setIconBitmap(MediaDescriptionCompatApi21.getIconBitmap(descriptionObj));
-            bob.setIconUri(MediaDescriptionCompatApi21.getIconUri(descriptionObj));
-            Bundle extras = MediaDescriptionCompatApi21.getExtras(descriptionObj);
+            MediaDescription description = (MediaDescription) descriptionObj;
+            bob.setMediaId(description.getMediaId());
+            bob.setTitle(description.getTitle());
+            bob.setSubtitle(description.getSubtitle());
+            bob.setDescription(description.getDescription());
+            bob.setIconBitmap(description.getIconBitmap());
+            bob.setIconUri(description.getIconUri());
+            Bundle extras = description.getExtras();
             Uri mediaUri = null;
             if (extras != null) {
                 MediaSessionCompat.ensureClassLoader(extras);
@@ -405,10 +407,10 @@
             if (mediaUri != null) {
                 bob.setMediaUri(mediaUri);
             } else if (Build.VERSION.SDK_INT >= 23) {
-                bob.setMediaUri(MediaDescriptionCompatApi23.getMediaUri(descriptionObj));
+                bob.setMediaUri(description.getMediaUri());
             }
             MediaDescriptionCompat descriptionCompat = bob.build();
-            descriptionCompat.mDescriptionObj = descriptionObj;
+            descriptionCompat.mDescriptionFwk = description;
 
             return descriptionCompat;
         } else {
@@ -423,7 +425,7 @@
                     if (Build.VERSION.SDK_INT < 21) {
                         return new MediaDescriptionCompat(in);
                     } else {
-                        return fromMediaDescription(MediaDescriptionCompatApi21.fromParcel(in));
+                        return fromMediaDescription(MediaDescription.CREATOR.createFromParcel(in));
                     }
                 }
 
diff --git a/media/src/main/java/android/support/v4/media/MediaMetadataCompat.java b/media/src/main/java/android/support/v4/media/MediaMetadataCompat.java
index 638f13d..7227eb6 100644
--- a/media/src/main/java/android/support/v4/media/MediaMetadataCompat.java
+++ b/media/src/main/java/android/support/v4/media/MediaMetadataCompat.java
@@ -18,6 +18,7 @@
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
 
 import android.graphics.Bitmap;
+import android.media.MediaMetadata;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
@@ -367,7 +368,7 @@
     };
 
     final Bundle mBundle;
-    private Object mMetadataObj;
+    private MediaMetadata mMetadataFwk;
     private MediaDescriptionCompat mDescription;
 
     MediaMetadataCompat(Bundle bundle) {
@@ -611,11 +612,11 @@
     public static MediaMetadataCompat fromMediaMetadata(Object metadataObj) {
         if (metadataObj != null && Build.VERSION.SDK_INT >= 21) {
             Parcel p = Parcel.obtain();
-            MediaMetadataCompatApi21.writeToParcel(metadataObj, p, 0);
+            ((MediaMetadata) metadataObj).writeToParcel(p, 0);
             p.setDataPosition(0);
             MediaMetadataCompat metadata = MediaMetadataCompat.CREATOR.createFromParcel(p);
             p.recycle();
-            metadata.mMetadataObj = metadataObj;
+            metadata.mMetadataFwk = (MediaMetadata) metadataObj;
             return metadata;
         } else {
             return null;
@@ -633,14 +634,14 @@
      *         if none.
      */
     public Object getMediaMetadata() {
-        if (mMetadataObj == null && Build.VERSION.SDK_INT >= 21) {
+        if (mMetadataFwk == null && Build.VERSION.SDK_INT >= 21) {
             Parcel p = Parcel.obtain();
             writeToParcel(p, 0);
             p.setDataPosition(0);
-            mMetadataObj = MediaMetadataCompatApi21.createFromParcel(p);
+            mMetadataFwk = MediaMetadata.CREATOR.createFromParcel(p);
             p.recycle();
         }
-        return mMetadataObj;
+        return mMetadataFwk;
     }
 
     public static final Parcelable.Creator<MediaMetadataCompat> CREATOR =
diff --git a/media/src/main/java/android/support/v4/media/session/MediaControllerCompat.java b/media/src/main/java/android/support/v4/media/session/MediaControllerCompat.java
index d6b5685..c4c95dd 100644
--- a/media/src/main/java/android/support/v4/media/session/MediaControllerCompat.java
+++ b/media/src/main/java/android/support/v4/media/session/MediaControllerCompat.java
@@ -22,9 +22,15 @@
 import android.app.Activity;
 import android.app.PendingIntent;
 import android.content.Context;
+import android.media.AudioAttributes;
 import android.media.AudioManager;
+import android.media.MediaMetadata;
+import android.media.Rating;
 import android.media.session.MediaController;
+import android.media.session.MediaSession;
+import android.media.session.PlaybackState;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
@@ -171,12 +177,12 @@
                     new MediaControllerExtraData(mediaController));
         }
         if (android.os.Build.VERSION.SDK_INT >= 21) {
-            Object controllerObj = null;
+            MediaController controllerFwk = null;
             if (mediaController != null) {
                 Object sessionTokenObj = mediaController.getSessionToken().getToken();
-                controllerObj = MediaControllerCompatApi21.fromToken(activity, sessionTokenObj);
+                controllerFwk = new MediaController(activity, (MediaSession.Token) sessionTokenObj);
             }
-            MediaControllerCompatApi21.setMediaController(activity, controllerObj);
+            activity.setMediaController(controllerFwk);
         }
     }
 
@@ -197,14 +203,14 @@
                     ((ComponentActivity) activity).getExtraData(MediaControllerExtraData.class);
             return extraData != null ? extraData.getMediaController() : null;
         } else if (android.os.Build.VERSION.SDK_INT >= 21) {
-            Object controllerObj = MediaControllerCompatApi21.getMediaController(activity);
-            if (controllerObj == null) {
+            MediaController controllerFwk = activity.getMediaController();
+            if (controllerFwk == null) {
                 return null;
             }
-            Object sessionTokenObj = MediaControllerCompatApi21.getSessionToken(controllerObj);
+            MediaSession.Token sessionTokenFwk = controllerFwk.getSessionToken();
             try {
                 return new MediaControllerCompat(activity,
-                        MediaSessionCompat.Token.fromToken(sessionTokenObj));
+                        MediaSessionCompat.Token.fromToken(sessionTokenFwk));
             } catch (RemoteException e) {
                 Log.e(TAG, "Dead object in getMediaController.", e);
             }
@@ -251,11 +257,7 @@
 
         MediaControllerImpl impl = null;
         try {
-            if (android.os.Build.VERSION.SDK_INT >= 24) {
-                impl = new MediaControllerImplApi24(context, mToken);
-            } else if (android.os.Build.VERSION.SDK_INT >= 23) {
-                impl = new MediaControllerImplApi23(context, mToken);
-            } else if (android.os.Build.VERSION.SDK_INT >= 21) {
+            if (android.os.Build.VERSION.SDK_INT >= 21) {
                 impl = new MediaControllerImplApi21(context, mToken);
             } else {
                 impl = new MediaControllerImplBase(mToken);
@@ -280,11 +282,7 @@
         }
         mToken = sessionToken;
 
-        if (android.os.Build.VERSION.SDK_INT >= 24) {
-            mImpl = new MediaControllerImplApi24(context, sessionToken);
-        } else if (android.os.Build.VERSION.SDK_INT >= 23) {
-            mImpl = new MediaControllerImplApi23(context, sessionToken);
-        } else if (android.os.Build.VERSION.SDK_INT >= 21) {
+        if (android.os.Build.VERSION.SDK_INT >= 21) {
             mImpl = new MediaControllerImplApi21(context, sessionToken);
         } else {
             mImpl = new MediaControllerImplBase(sessionToken);
@@ -698,15 +696,16 @@
      * registered using {@link #registerCallback}
      */
     public static abstract class Callback implements IBinder.DeathRecipient {
-        final Object mCallbackObj;
+        final MediaController.Callback mCallbackFwk;
         MessageHandler mHandler;
         IMediaControllerCallback mIControllerCallback;
 
         public Callback() {
             if (android.os.Build.VERSION.SDK_INT >= 21) {
-                mCallbackObj = MediaControllerCompatApi21.createCallback(new StubApi21(this));
+                mCallbackFwk = new MediaControllerCallbackApi21(this);
             } else {
-                mCallbackObj = mIControllerCallback = new StubCompat(this);
+                mCallbackFwk = null;
+                mIControllerCallback = new StubCompat(this);
             }
         }
 
@@ -860,10 +859,11 @@
         }
 
         // Callback methods in this class are run on handler which was given to registerCallback().
-        private static class StubApi21 implements MediaControllerCompatApi21.Callback {
+        @RequiresApi(21)
+        private static class MediaControllerCallbackApi21 extends MediaController.Callback {
             private final WeakReference<MediaControllerCompat.Callback> mCallback;
 
-            StubApi21(MediaControllerCompat.Callback callback) {
+            MediaControllerCallbackApi21(MediaControllerCompat.Callback callback) {
                 mCallback = new WeakReference<>(callback);
             }
 
@@ -877,6 +877,7 @@
 
             @Override
             public void onSessionEvent(String event, Bundle extras) {
+                MediaSessionCompat.ensureClassLoader(extras);
                 MediaControllerCompat.Callback callback = mCallback.get();
                 if (callback != null) {
                     if (callback.mIControllerCallback != null
@@ -889,7 +890,7 @@
             }
 
             @Override
-            public void onPlaybackStateChanged(Object stateObj) {
+            public void onPlaybackStateChanged(PlaybackState stateObj) {
                 MediaControllerCompat.Callback callback = mCallback.get();
                 if (callback != null) {
                     if (callback.mIControllerCallback != null) {
@@ -902,7 +903,7 @@
             }
 
             @Override
-            public void onMetadataChanged(Object metadataObj) {
+            public void onMetadataChanged(MediaMetadata metadataObj) {
                 MediaControllerCompat.Callback callback = mCallback.get();
                 if (callback != null) {
                     callback.onMetadataChanged(MediaMetadataCompat.fromMediaMetadata(metadataObj));
@@ -910,7 +911,7 @@
             }
 
             @Override
-            public void onQueueChanged(List<?> queue) {
+            public void onQueueChanged(List<MediaSession.QueueItem> queue) {
                 MediaControllerCompat.Callback callback = mCallback.get();
                 if (callback != null) {
                     callback.onQueueChanged(QueueItem.fromQueueItemList(queue));
@@ -927,6 +928,7 @@
 
             @Override
             public void onExtrasChanged(Bundle extras) {
+                MediaSessionCompat.ensureClassLoader(extras);
                 MediaControllerCompat.Callback callback = mCallback.get();
                 if (callback != null) {
                     callback.onExtrasChanged(extras);
@@ -934,12 +936,15 @@
             }
 
             @Override
-            public void onAudioInfoChanged(
-                    int type, int stream, int control, int max, int current) {
+            public void onAudioInfoChanged(MediaController.PlaybackInfo info) {
                 MediaControllerCompat.Callback callback = mCallback.get();
                 if (callback != null) {
-                    callback.onAudioInfoChanged(
-                            new PlaybackInfo(type, stream, control, max, current));
+                    callback.onAudioInfoChanged(new PlaybackInfo(
+                            info.getPlaybackType(),
+                            PlaybackInfo.toLegacyStreamType(info.getAudioAttributes()),
+                            info.getVolumeControl(),
+                            info.getMaxVolume(),
+                            info.getCurrentVolume()));
                 }
             }
         }
@@ -1437,6 +1442,52 @@
         public int getCurrentVolume() {
             return mCurrentVolume;
         }
+
+        // This is copied from AudioAttributes.toLegacyStreamType. TODO This
+        // either needs to be kept in sync with that one or toLegacyStreamType
+        // needs to be made public so it can be used by the support lib.
+        @RequiresApi(21)
+        static int toLegacyStreamType(AudioAttributes aa) {
+            final int flagSco = 0x1 << 2;
+            final int streamBluetoothSco = 6;
+            final int streamSystemEnforced = 7;
+            // flags to stream type mapping
+            if ((aa.getFlags() & AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
+                    == AudioAttributes.FLAG_AUDIBILITY_ENFORCED) {
+                return streamSystemEnforced;
+            }
+            if ((aa.getFlags() & flagSco) == flagSco) {
+                return streamBluetoothSco;
+            }
+
+            // usage to stream type mapping
+            switch (aa.getUsage()) {
+                case AudioAttributes.USAGE_MEDIA:
+                case AudioAttributes.USAGE_GAME:
+                case AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY:
+                case AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE:
+                    return AudioManager.STREAM_MUSIC;
+                case AudioAttributes.USAGE_ASSISTANCE_SONIFICATION:
+                    return AudioManager.STREAM_SYSTEM;
+                case AudioAttributes.USAGE_VOICE_COMMUNICATION:
+                    return AudioManager.STREAM_VOICE_CALL;
+                case AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING:
+                    return AudioManager.STREAM_DTMF;
+                case AudioAttributes.USAGE_ALARM:
+                    return AudioManager.STREAM_ALARM;
+                case AudioAttributes.USAGE_NOTIFICATION_RINGTONE:
+                    return AudioManager.STREAM_RING;
+                case AudioAttributes.USAGE_NOTIFICATION:
+                case AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST:
+                case AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT:
+                case AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED:
+                case AudioAttributes.USAGE_NOTIFICATION_EVENT:
+                    return AudioManager.STREAM_NOTIFICATION;
+                case AudioAttributes.USAGE_UNKNOWN:
+                default:
+                    return AudioManager.STREAM_MUSIC;
+            }
+        }
     }
 
     interface MediaControllerImpl {
@@ -1486,7 +1537,7 @@
             }
             try {
                 mBinder.asBinder().linkToDeath(callback, 0);
-                mBinder.registerCallbackListener((IMediaControllerCallback) callback.mCallbackObj);
+                mBinder.registerCallbackListener(callback.mIControllerCallback);
                 callback.postToHandler(Callback.MessageHandler.MSG_SESSION_READY, null, null);
             } catch (RemoteException e) {
                 Log.e(TAG, "Dead object in registerCallback.", e);
@@ -1500,8 +1551,7 @@
                 throw new IllegalArgumentException("callback may not be null.");
             }
             try {
-                mBinder.unregisterCallbackListener(
-                        (IMediaControllerCallback) callback.mCallbackObj);
+                mBinder.unregisterCallbackListener(callback.mIControllerCallback);
                 mBinder.asBinder().unlinkToDeath(callback, 0);
             } catch (RemoteException e) {
                 Log.e(TAG, "Dead object in unregisterCallback.", e);
@@ -1958,7 +2008,7 @@
 
     @RequiresApi(21)
     static class MediaControllerImplApi21 implements MediaControllerImpl {
-        protected final Object mControllerObj;
+        protected final MediaController mControllerFwk;
 
         final Object mLock = new Object();
 
@@ -1972,9 +2022,9 @@
         public MediaControllerImplApi21(Context context, MediaSessionCompat.Token sessionToken)
                 throws RemoteException {
             mSessionToken = sessionToken;
-            mControllerObj = MediaControllerCompatApi21.fromToken(context,
-                    mSessionToken.getToken());
-            if (mControllerObj == null) throw new RemoteException();
+            mControllerFwk = new MediaController(context,
+                    (MediaSession.Token) mSessionToken.getToken());
+            if (mControllerFwk == null) throw new RemoteException();
             if (mSessionToken.getExtraBinder() == null) {
                 requestExtraBinder();
             }
@@ -1982,8 +2032,7 @@
 
         @Override
         public final void registerCallback(Callback callback, Handler handler) {
-            MediaControllerCompatApi21.registerCallback(
-                    mControllerObj, callback.mCallbackObj, handler);
+            mControllerFwk.registerCallback(callback.mCallbackFwk, handler);
             synchronized (mLock) {
                 if (mSessionToken.getExtraBinder() != null) {
                     ExtraCallback extraCallback = new ExtraCallback(callback);
@@ -2005,7 +2054,7 @@
 
         @Override
         public final void unregisterCallback(Callback callback) {
-            MediaControllerCompatApi21.unregisterCallback(mControllerObj, callback.mCallbackObj);
+            mControllerFwk.unregisterCallback(callback.mCallbackFwk);
             synchronized (mLock) {
                 if (mSessionToken.getExtraBinder() != null) {
                     try {
@@ -2026,13 +2075,12 @@
 
         @Override
         public boolean dispatchMediaButtonEvent(KeyEvent event) {
-            return MediaControllerCompatApi21.dispatchMediaButtonEvent(mControllerObj, event);
+            return mControllerFwk.dispatchMediaButtonEvent(event);
         }
 
         @Override
         public TransportControls getTransportControls() {
-            Object controlsObj = MediaControllerCompatApi21.getTransportControls(mControllerObj);
-            return controlsObj != null ? new TransportControlsApi21(controlsObj) : null;
+            return new TransportControlsApi21(mControllerFwk.getTransportControls());
         }
 
         @Override
@@ -2044,20 +2092,20 @@
                     Log.e(TAG, "Dead object in getPlaybackState.", e);
                 }
             }
-            Object stateObj = MediaControllerCompatApi21.getPlaybackState(mControllerObj);
-            return stateObj != null ? PlaybackStateCompat.fromPlaybackState(stateObj) : null;
+            PlaybackState stateFwk = mControllerFwk.getPlaybackState();
+            return stateFwk != null ? PlaybackStateCompat.fromPlaybackState(stateFwk) : null;
         }
 
         @Override
         public MediaMetadataCompat getMetadata() {
-            Object metadataObj = MediaControllerCompatApi21.getMetadata(mControllerObj);
-            return metadataObj != null ? MediaMetadataCompat.fromMediaMetadata(metadataObj) : null;
+            MediaMetadata metadataFwk = mControllerFwk.getMetadata();
+            return metadataFwk != null ? MediaMetadataCompat.fromMediaMetadata(metadataFwk) : null;
         }
 
         @Override
         public List<QueueItem> getQueue() {
-            List<Object> queueObjs = MediaControllerCompatApi21.getQueue(mControllerObj);
-            return queueObjs != null ? QueueItem.fromQueueItemList(queueObjs) : null;
+            List<MediaSession.QueueItem> queueFwks = mControllerFwk.getQueue();
+            return queueFwks != null ? QueueItem.fromQueueItemList(queueFwks) : null;
         }
 
         @Override
@@ -2099,12 +2147,12 @@
 
         @Override
         public CharSequence getQueueTitle() {
-            return MediaControllerCompatApi21.getQueueTitle(mControllerObj);
+            return mControllerFwk.getQueueTitle();
         }
 
         @Override
         public Bundle getExtras() {
-            return MediaControllerCompatApi21.getExtras(mControllerObj);
+            return mControllerFwk.getExtras();
         }
 
         @Override
@@ -2116,7 +2164,7 @@
                     Log.e(TAG, "Dead object in getRatingType.", e);
                 }
             }
-            return MediaControllerCompatApi21.getRatingType(mControllerObj);
+            return mControllerFwk.getRatingType();
         }
 
         @Override
@@ -2157,38 +2205,38 @@
 
         @Override
         public long getFlags() {
-            return MediaControllerCompatApi21.getFlags(mControllerObj);
+            return mControllerFwk.getFlags();
         }
 
         @Override
         public PlaybackInfo getPlaybackInfo() {
-            Object volumeInfoObj = MediaControllerCompatApi21.getPlaybackInfo(mControllerObj);
-            return volumeInfoObj != null ? new PlaybackInfo(
-                    MediaControllerCompatApi21.PlaybackInfo.getPlaybackType(volumeInfoObj),
-                    MediaControllerCompatApi21.PlaybackInfo.getLegacyAudioStream(volumeInfoObj),
-                    MediaControllerCompatApi21.PlaybackInfo.getVolumeControl(volumeInfoObj),
-                    MediaControllerCompatApi21.PlaybackInfo.getMaxVolume(volumeInfoObj),
-                    MediaControllerCompatApi21.PlaybackInfo.getCurrentVolume(volumeInfoObj)) : null;
+            MediaController.PlaybackInfo volumeInfoFwk = mControllerFwk.getPlaybackInfo();
+            return volumeInfoFwk != null ? new PlaybackInfo(
+                    volumeInfoFwk.getPlaybackType(),
+                    PlaybackInfo.toLegacyStreamType(volumeInfoFwk.getAudioAttributes()),
+                    volumeInfoFwk.getVolumeControl(),
+                    volumeInfoFwk.getMaxVolume(),
+                    volumeInfoFwk.getCurrentVolume()) : null;
         }
 
         @Override
         public PendingIntent getSessionActivity() {
-            return MediaControllerCompatApi21.getSessionActivity(mControllerObj);
+            return mControllerFwk.getSessionActivity();
         }
 
         @Override
         public void setVolumeTo(int value, int flags) {
-            MediaControllerCompatApi21.setVolumeTo(mControllerObj, value, flags);
+            mControllerFwk.setVolumeTo(value, flags);
         }
 
         @Override
         public void adjustVolume(int direction, int flags) {
-            MediaControllerCompatApi21.adjustVolume(mControllerObj, direction, flags);
+            mControllerFwk.adjustVolume(direction, flags);
         }
 
         @Override
         public void sendCommand(String command, Bundle params, ResultReceiver cb) {
-            MediaControllerCompatApi21.sendCommand(mControllerObj, command, params, cb);
+            mControllerFwk.sendCommand(command, params, cb);
         }
 
         @Override
@@ -2198,12 +2246,12 @@
 
         @Override
         public String getPackageName() {
-            return MediaControllerCompatApi21.getPackageName(mControllerObj);
+            return mControllerFwk.getPackageName();
         }
 
         @Override
         public Object getMediaController() {
-            return mControllerObj;
+            return mControllerFwk;
         }
 
         private void requestExtraBinder() {
@@ -2300,20 +2348,29 @@
         }
     }
 
+    @RequiresApi(21)
     static class TransportControlsApi21 extends TransportControls {
-        protected final Object mControlsObj;
+        protected final MediaController.TransportControls mControlsFwk;
 
-        public TransportControlsApi21(Object controlsObj) {
-            mControlsObj = controlsObj;
+        TransportControlsApi21(MediaController.TransportControls controlsFwk) {
+            mControlsFwk = controlsFwk;
         }
 
         @Override
         public void prepare() {
+            if (Build.VERSION.SDK_INT >= 24) {
+                mControlsFwk.prepare();
+                return;
+            }
             sendCustomAction(MediaSessionCompat.ACTION_PREPARE, null);
         }
 
         @Override
         public void prepareFromMediaId(String mediaId, Bundle extras) {
+            if (Build.VERSION.SDK_INT >= 24) {
+                mControlsFwk.prepareFromMediaId(mediaId, extras);
+                return;
+            }
             Bundle bundle = new Bundle();
             bundle.putString(MediaSessionCompat.ACTION_ARGUMENT_MEDIA_ID, mediaId);
             bundle.putBundle(MediaSessionCompat.ACTION_ARGUMENT_EXTRAS, extras);
@@ -2322,6 +2379,10 @@
 
         @Override
         public void prepareFromSearch(String query, Bundle extras) {
+            if (Build.VERSION.SDK_INT >= 24) {
+                mControlsFwk.prepareFromSearch(query, extras);
+                return;
+            }
             Bundle bundle = new Bundle();
             bundle.putString(MediaSessionCompat.ACTION_ARGUMENT_QUERY, query);
             bundle.putBundle(MediaSessionCompat.ACTION_ARGUMENT_EXTRAS, extras);
@@ -2330,6 +2391,10 @@
 
         @Override
         public void prepareFromUri(Uri uri, Bundle extras) {
+            if (Build.VERSION.SDK_INT >= 24) {
+                mControlsFwk.prepareFromUri(uri, extras);
+                return;
+            }
             Bundle bundle = new Bundle();
             bundle.putParcelable(MediaSessionCompat.ACTION_ARGUMENT_URI, uri);
             bundle.putBundle(MediaSessionCompat.ACTION_ARGUMENT_EXTRAS, extras);
@@ -2338,48 +2403,47 @@
 
         @Override
         public void play() {
-            MediaControllerCompatApi21.TransportControls.play(mControlsObj);
+            mControlsFwk.play();
         }
 
         @Override
         public void pause() {
-            MediaControllerCompatApi21.TransportControls.pause(mControlsObj);
+            mControlsFwk.pause();
         }
 
         @Override
         public void stop() {
-            MediaControllerCompatApi21.TransportControls.stop(mControlsObj);
+            mControlsFwk.stop();
         }
 
         @Override
         public void seekTo(long pos) {
-            MediaControllerCompatApi21.TransportControls.seekTo(mControlsObj, pos);
+            mControlsFwk.seekTo(pos);
         }
 
         @Override
         public void fastForward() {
-            MediaControllerCompatApi21.TransportControls.fastForward(mControlsObj);
+            mControlsFwk.fastForward();
         }
 
         @Override
         public void rewind() {
-            MediaControllerCompatApi21.TransportControls.rewind(mControlsObj);
+            mControlsFwk.rewind();
         }
 
         @Override
         public void skipToNext() {
-            MediaControllerCompatApi21.TransportControls.skipToNext(mControlsObj);
+            mControlsFwk.skipToNext();
         }
 
         @Override
         public void skipToPrevious() {
-            MediaControllerCompatApi21.TransportControls.skipToPrevious(mControlsObj);
+            mControlsFwk.skipToPrevious();
         }
 
         @Override
         public void setRating(RatingCompat rating) {
-            MediaControllerCompatApi21.TransportControls.setRating(mControlsObj,
-                    rating != null ? rating.getRating() : null);
+            mControlsFwk.setRating(rating != null ? (Rating) rating.getRating() : null);
         }
 
         @Override
@@ -2413,18 +2477,20 @@
 
         @Override
         public void playFromMediaId(String mediaId, Bundle extras) {
-            MediaControllerCompatApi21.TransportControls.playFromMediaId(mControlsObj, mediaId,
-                    extras);
+            mControlsFwk.playFromMediaId(mediaId, extras);
         }
 
         @Override
         public void playFromSearch(String query, Bundle extras) {
-            MediaControllerCompatApi21.TransportControls.playFromSearch(mControlsObj, query,
-                    extras);
+            mControlsFwk.playFromSearch(query, extras);
         }
 
         @Override
         public void playFromUri(Uri uri, Bundle extras) {
+            if (Build.VERSION.SDK_INT >= 23) {
+                mControlsFwk.playFromUri(uri, extras);
+                return;
+            }
             if (uri == null || Uri.EMPTY.equals(uri)) {
                 throw new IllegalArgumentException(
                         "You must specify a non-empty Uri for playFromUri.");
@@ -2437,95 +2503,19 @@
 
         @Override
         public void skipToQueueItem(long id) {
-            MediaControllerCompatApi21.TransportControls.skipToQueueItem(mControlsObj, id);
+            mControlsFwk.skipToQueueItem(id);
         }
 
         @Override
         public void sendCustomAction(CustomAction customAction, Bundle args) {
             validateCustomAction(customAction.getAction(), args);
-            MediaControllerCompatApi21.TransportControls.sendCustomAction(mControlsObj,
-                    customAction.getAction(), args);
+            mControlsFwk.sendCustomAction(customAction.getAction(), args);
         }
 
         @Override
         public void sendCustomAction(String action, Bundle args) {
             validateCustomAction(action, args);
-            MediaControllerCompatApi21.TransportControls.sendCustomAction(mControlsObj, action,
-                    args);
-        }
-    }
-
-    @RequiresApi(23)
-    static class MediaControllerImplApi23 extends MediaControllerImplApi21 {
-
-        public MediaControllerImplApi23(Context context, MediaSessionCompat.Token sessionToken)
-                throws RemoteException {
-            super(context, sessionToken);
-        }
-
-        @Override
-        public TransportControls getTransportControls() {
-            Object controlsObj = MediaControllerCompatApi21.getTransportControls(mControllerObj);
-            return controlsObj != null ? new TransportControlsApi23(controlsObj) : null;
-        }
-    }
-
-    @RequiresApi(23)
-    static class TransportControlsApi23 extends TransportControlsApi21 {
-
-        public TransportControlsApi23(Object controlsObj) {
-            super(controlsObj);
-        }
-
-        @Override
-        public void playFromUri(Uri uri, Bundle extras) {
-            MediaControllerCompatApi23.TransportControls.playFromUri(mControlsObj, uri,
-                    extras);
-        }
-    }
-
-    @RequiresApi(24)
-    static class MediaControllerImplApi24 extends MediaControllerImplApi23 {
-
-        public MediaControllerImplApi24(Context context, MediaSessionCompat.Token sessionToken)
-                throws RemoteException {
-            super(context, sessionToken);
-        }
-
-        @Override
-        public TransportControls getTransportControls() {
-            Object controlsObj = MediaControllerCompatApi21.getTransportControls(mControllerObj);
-            return controlsObj != null ? new TransportControlsApi24(controlsObj) : null;
-        }
-    }
-
-    @RequiresApi(24)
-    static class TransportControlsApi24 extends TransportControlsApi23 {
-
-        public TransportControlsApi24(Object controlsObj) {
-            super(controlsObj);
-        }
-
-        @Override
-        public void prepare() {
-            MediaControllerCompatApi24.TransportControls.prepare(mControlsObj);
-        }
-
-        @Override
-        public void prepareFromMediaId(String mediaId, Bundle extras) {
-            MediaControllerCompatApi24.TransportControls.prepareFromMediaId(
-                    mControlsObj, mediaId, extras);
-        }
-
-        @Override
-        public void prepareFromSearch(String query, Bundle extras) {
-            MediaControllerCompatApi24.TransportControls.prepareFromSearch(
-                    mControlsObj, query, extras);
-        }
-
-        @Override
-        public void prepareFromUri(Uri uri, Bundle extras) {
-            MediaControllerCompatApi24.TransportControls.prepareFromUri(mControlsObj, uri, extras);
+            mControlsFwk.sendCustomAction(action, args);
         }
     }
 }
diff --git a/media/src/main/java/android/support/v4/media/session/MediaSessionCompat.java b/media/src/main/java/android/support/v4/media/session/MediaSessionCompat.java
index 957d0ca..1e063d6 100644
--- a/media/src/main/java/android/support/v4/media/session/MediaSessionCompat.java
+++ b/media/src/main/java/android/support/v4/media/session/MediaSessionCompat.java
@@ -22,6 +22,7 @@
 import static androidx.media.MediaSessionManager.RemoteUserInfo.UNKNOWN_PID;
 import static androidx.media.MediaSessionManager.RemoteUserInfo.UNKNOWN_UID;
 
+import android.annotation.SuppressLint;
 import android.app.Activity;
 import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
@@ -29,12 +30,17 @@
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Bitmap;
+import android.media.AudioAttributes;
 import android.media.AudioManager;
+import android.media.MediaDescription;
+import android.media.MediaMetadata;
 import android.media.MediaMetadataEditor;
 import android.media.MediaMetadataRetriever;
 import android.media.Rating;
 import android.media.RemoteControlClient;
+import android.media.VolumeProvider;
 import android.media.session.MediaSession;
+import android.media.session.PlaybackState;
 import android.net.Uri;
 import android.os.BadParcelableException;
 import android.os.Binder;
@@ -76,6 +82,7 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.ref.WeakReference;
+import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -529,11 +536,6 @@
 
     private MediaSessionCompat(Context context, MediaSessionImpl impl) {
         mImpl = impl;
-        if (android.os.Build.VERSION.SDK_INT >= 21
-                && !MediaSessionCompatApi21.hasCallback(impl.getMediaSession())) {
-            // Set default callback to respond to controllers' extra binder requests.
-            setCallback(new Callback() {});
-        }
         mController = new MediaControllerCompat(context, this);
     }
 
@@ -929,7 +931,13 @@
      * Creates an instance from a framework {@link android.media.session.MediaSession} object.
      * <p>
      * This method is only supported on API 21+. On API 20 and below, it returns null.
-     * </p>
+     * <p>
+     * Note: A {@link MediaSessionCompat} object returned from this method may not provide the full
+     * functionality of {@link MediaSessionCompat} until setting a new
+     * {@link MediaSessionCompat.Callback}. To avoid this, when both a {@link MediaSessionCompat}
+     * and a framework {@link android.media.session.MediaSession} are needed, it is recommended
+     * to create a {@link MediaSessionCompat} first and get the framework session through
+     * {@link #getMediaSession()}.
      *
      * @param context The context to use to create the session.
      * @param mediaSession A {@link android.media.session.MediaSession} object.
@@ -1000,20 +1008,16 @@
      * and the system. The callback may be set using {@link #setCallback}.
      */
     public abstract static class Callback {
-        final Object mCallbackObj;
+        final MediaSession.Callback mCallbackFwk;
         WeakReference<MediaSessionImpl> mSessionImpl;
         private CallbackHandler mCallbackHandler = null;
         private boolean mMediaPlayPauseKeyPending;
 
         public Callback() {
-            if (android.os.Build.VERSION.SDK_INT >= 24) {
-                mCallbackObj = MediaSessionCompatApi24.createCallback(new StubApi24());
-            } else if (android.os.Build.VERSION.SDK_INT >= 23) {
-                mCallbackObj = MediaSessionCompatApi23.createCallback(new StubApi23());
-            } else if (android.os.Build.VERSION.SDK_INT >= 21) {
-                mCallbackObj = MediaSessionCompatApi21.createCallback(new StubApi21());
+            if (android.os.Build.VERSION.SDK_INT >= 21) {
+                mCallbackFwk = new MediaSessionCallbackApi21();
             } else {
-                mCallbackObj = null;
+                mCallbackFwk = null;
             }
         }
 
@@ -1378,13 +1382,14 @@
         }
 
         @RequiresApi(21)
-        private class StubApi21 implements MediaSessionCompatApi21.Callback {
+        private class MediaSessionCallbackApi21 extends MediaSession.Callback {
 
-            StubApi21() {
+            MediaSessionCallbackApi21() {
             }
 
             @Override
             public void onCommand(String command, Bundle extras, ResultReceiver cb) {
+                ensureClassLoader(extras);
                 setCurrentControllerInfo();
                 try {
                     if (command.equals(MediaControllerCompat.COMMAND_GET_EXTRA_BINDER)) {
@@ -1439,7 +1444,7 @@
                 setCurrentControllerInfo();
                 boolean result = Callback.this.onMediaButtonEvent(mediaButtonIntent);
                 clearCurrentControllerInfo();
-                return result;
+                return result || super.onMediaButtonEvent(mediaButtonIntent);
             }
 
             @Override
@@ -1451,6 +1456,7 @@
 
             @Override
             public void onPlayFromMediaId(String mediaId, Bundle extras) {
+                ensureClassLoader(extras);
                 setCurrentControllerInfo();
                 Callback.this.onPlayFromMediaId(mediaId, extras);
                 clearCurrentControllerInfo();
@@ -1458,11 +1464,21 @@
 
             @Override
             public void onPlayFromSearch(String search, Bundle extras) {
+                ensureClassLoader(extras);
                 setCurrentControllerInfo();
                 Callback.this.onPlayFromSearch(search, extras);
                 clearCurrentControllerInfo();
             }
 
+            @RequiresApi(23)
+            @Override
+            public void onPlayFromUri(Uri uri, Bundle extras) {
+                ensureClassLoader(extras);
+                setCurrentControllerInfo();
+                Callback.this.onPlayFromUri(uri, extras);
+                clearCurrentControllerInfo();
+            }
+
             @Override
             public void onSkipToQueueItem(long id) {
                 setCurrentControllerInfo();
@@ -1520,19 +1536,19 @@
             }
 
             @Override
-            public void onSetRating(Object ratingObj) {
+            public void onSetRating(Rating ratingFwk) {
                 setCurrentControllerInfo();
-                Callback.this.onSetRating(RatingCompat.fromRating(ratingObj));
+                Callback.this.onSetRating(RatingCompat.fromRating(ratingFwk));
                 clearCurrentControllerInfo();
             }
 
-            @Override
-            public void onSetRating(Object ratingObj, Bundle extras) {
+            public void onSetRating(Rating ratingFwk, Bundle extras) {
                 // This method will not be called.
             }
 
             @Override
             public void onCustomAction(String action, Bundle extras) {
+                ensureClassLoader(extras);
                 setCurrentControllerInfo();
                 Bundle bundle = extras.getBundle(ACTION_ARGUMENT_EXTRAS);
                 ensureClassLoader(bundle);
@@ -1569,13 +1585,23 @@
                 clearCurrentControllerInfo();
             }
 
-            // Since we can't get ControllerInfo in API 21-23, just provide a default (unknown) one.
             void setCurrentControllerInfo() {
-                MediaSessionImpl sessionImpl = mSessionImpl != null ? mSessionImpl.get() : null;
-                if (sessionImpl != null) {
-                    sessionImpl.setCurrentControllerInfo(
-                            new RemoteUserInfo(LEGACY_CONTROLLER, UNKNOWN_PID, UNKNOWN_UID));
+                if (Build.VERSION.SDK_INT >= 28) {
+                    // From API 28, this method has no effect since
+                    // MediaSessionImplApi28#getCurrentControllerInfo() returns controller info from
+                    // framework.
+                    return;
                 }
+                MediaSessionImpl sessionImpl = mSessionImpl != null ? mSessionImpl.get() : null;
+                if (sessionImpl == null) {
+                    return;
+                }
+                String packageName = sessionImpl.getCallingPackage();
+                if (TextUtils.isEmpty(packageName)) {
+                    packageName = LEGACY_CONTROLLER;
+                }
+                sessionImpl.setCurrentControllerInfo(new RemoteUserInfo(
+                        packageName, UNKNOWN_PID, UNKNOWN_UID));
             }
 
             void clearCurrentControllerInfo() {
@@ -1584,28 +1610,8 @@
                     sessionImpl.setCurrentControllerInfo(null);
                 }
             }
-        }
 
-        @RequiresApi(23)
-        private class StubApi23 extends StubApi21 implements MediaSessionCompatApi23.Callback {
-
-            StubApi23() {
-            }
-
-            @Override
-            public void onPlayFromUri(Uri uri, Bundle extras) {
-                setCurrentControllerInfo();
-                Callback.this.onPlayFromUri(uri, extras);
-                clearCurrentControllerInfo();
-            }
-        }
-
-        @RequiresApi(24)
-        private class StubApi24 extends StubApi23 implements MediaSessionCompatApi24.Callback {
-
-            StubApi24() {
-            }
-
+            @RequiresApi(24)
             @Override
             public void onPrepare() {
                 setCurrentControllerInfo();
@@ -1613,45 +1619,32 @@
                 clearCurrentControllerInfo();
             }
 
+            @RequiresApi(24)
             @Override
             public void onPrepareFromMediaId(String mediaId, Bundle extras) {
+                ensureClassLoader(extras);
                 setCurrentControllerInfo();
                 Callback.this.onPrepareFromMediaId(mediaId, extras);
                 clearCurrentControllerInfo();
             }
 
+            @RequiresApi(24)
             @Override
             public void onPrepareFromSearch(String query, Bundle extras) {
+                ensureClassLoader(extras);
                 setCurrentControllerInfo();
                 Callback.this.onPrepareFromSearch(query, extras);
                 clearCurrentControllerInfo();
             }
 
+            @RequiresApi(24)
             @Override
             public void onPrepareFromUri(Uri uri, Bundle extras) {
+                ensureClassLoader(extras);
                 setCurrentControllerInfo();
                 Callback.this.onPrepareFromUri(uri, extras);
                 clearCurrentControllerInfo();
             }
-
-            // Note: This is only for on API 24~27. From API 28, this method has no effect since
-            // MediaSessionImplApi28#getCurrentControllerInfo() returns controller info from
-            // framework.
-            @Override
-            void setCurrentControllerInfo() {
-                if (Build.VERSION.SDK_INT >= 28) {
-                    return;
-                }
-                MediaSessionImpl sessionImpl = mSessionImpl != null ? mSessionImpl.get() : null;
-                if (sessionImpl != null) {
-                    String packageName = sessionImpl.getCallingPackage();
-                    if (TextUtils.isEmpty(packageName)) {
-                        packageName = LEGACY_CONTROLLER;
-                    }
-                    sessionImpl.setCurrentControllerInfo(
-                            new RemoteUserInfo(packageName, UNKNOWN_PID, UNKNOWN_UID));
-                }
-            }
         }
     }
 
@@ -1711,7 +1704,11 @@
         @RestrictTo(LIBRARY_GROUP)
         public static Token fromToken(Object token, IMediaSession extraBinder) {
             if (token != null && android.os.Build.VERSION.SDK_INT >= 21) {
-                return new Token(MediaSessionCompatApi21.verifyToken(token), extraBinder);
+                if (!(token instanceof MediaSession.Token)) {
+                    throw new IllegalArgumentException(
+                            "token is not a valid MediaSession.Token object");
+                }
+                return new Token(token, extraBinder);
             }
             return null;
         }
@@ -1873,7 +1870,7 @@
         private final MediaDescriptionCompat mDescription;
         private final long mId;
 
-        private Object mItem;
+        private MediaSession.QueueItem mItemFwk;
 
         /**
          * Creates a new {@link MediaSessionCompat.QueueItem}.
@@ -1886,7 +1883,10 @@
             this(null, description, id);
         }
 
-        private QueueItem(Object queueItem, MediaDescriptionCompat description, long id) {
+        private QueueItem(
+                MediaSession.QueueItem queueItem,
+                MediaDescriptionCompat description,
+                long id) {
             if (description == null) {
                 throw new IllegalArgumentException("Description cannot be null.");
             }
@@ -1895,7 +1895,7 @@
             }
             mDescription = description;
             mId = id;
-            mItem = queueItem;
+            mItemFwk = queueItem;
         }
 
         QueueItem(Parcel in) {
@@ -1939,12 +1939,13 @@
          *         {@link android.media.session.MediaSession.QueueItem} or null.
          */
         public Object getQueueItem() {
-            if (mItem != null || android.os.Build.VERSION.SDK_INT < 21) {
-                return mItem;
+            if (mItemFwk != null || android.os.Build.VERSION.SDK_INT < 21) {
+                return mItemFwk;
             }
-            mItem = MediaSessionCompatApi21.QueueItem.createItem(mDescription.getMediaDescription(),
+            mItemFwk = new MediaSession.QueueItem(
+                    (MediaDescription) mDescription.getMediaDescription(),
                     mId);
-            return mItem;
+            return mItemFwk;
         }
 
         /**
@@ -1961,11 +1962,12 @@
             if (queueItem == null || Build.VERSION.SDK_INT < 21) {
                 return null;
             }
-            Object descriptionObj = MediaSessionCompatApi21.QueueItem.getDescription(queueItem);
+            MediaSession.QueueItem queueItemObj = (MediaSession.QueueItem) queueItem;
+            Object descriptionObj = queueItemObj.getDescription();
             MediaDescriptionCompat description = MediaDescriptionCompat.fromMediaDescription(
                     descriptionObj);
-            long id = MediaSessionCompatApi21.QueueItem.getQueueId(queueItem);
-            return new QueueItem(queueItem, description, id);
+            long id = queueItemObj.getQueueId();
+            return new QueueItem(queueItemObj, description, id);
         }
 
         /**
@@ -3461,7 +3463,7 @@
 
     @RequiresApi(21)
     static class MediaSessionImplApi21 implements MediaSessionImpl {
-        final Object mSessionObj;
+        final MediaSession mSessionFwk;
         final Token mToken;
         final Object mLock = new Object();
 
@@ -3481,56 +3483,60 @@
         RemoteUserInfo mRemoteUserInfo;
 
         MediaSessionImplApi21(Context context, String tag, VersionedParcelable token2) {
-            mSessionObj = MediaSessionCompatApi21.createSession(context, tag);
-            mToken = new Token(MediaSessionCompatApi21.getSessionToken(mSessionObj),
-                    new ExtraSession(), token2);
+            mSessionFwk = new MediaSession(context, tag);
+            mToken = new Token(mSessionFwk.getSessionToken(), new ExtraSession(), token2);
             // For backward compatibility, these flags are always set.
             setFlags(FLAG_HANDLES_MEDIA_BUTTONS | FLAG_HANDLES_TRANSPORT_CONTROLS);
         }
 
         MediaSessionImplApi21(Object mediaSession) {
-            mSessionObj = MediaSessionCompatApi21.verifySession(mediaSession);
-            mToken = new Token(MediaSessionCompatApi21.getSessionToken(mSessionObj),
-                    new ExtraSession());
+            if (!(mediaSession instanceof MediaSession)) {
+                throw new IllegalArgumentException(
+                        "mediaSession is not a valid MediaSession object");
+            }
+            mSessionFwk = (MediaSession) mediaSession;
+            mToken = new Token(mSessionFwk.getSessionToken(), new ExtraSession());
             // For backward compatibility, these flags are always set.
             setFlags(FLAG_HANDLES_MEDIA_BUTTONS | FLAG_HANDLES_TRANSPORT_CONTROLS);
         }
 
         @Override
         public void setCallback(Callback callback, Handler handler) {
-            MediaSessionCompatApi21.setCallback(mSessionObj,
-                    callback == null ? null : callback.mCallbackObj, handler);
+            mSessionFwk.setCallback(callback == null ? null : callback.mCallbackFwk, handler);
             if (callback != null) {
                 callback.setSessionImpl(this, handler);
             }
         }
 
+        @SuppressLint("WrongConstant")
         @Override
         public void setFlags(@SessionFlags int flags) {
             // For backward compatibility, always set these deprecated flags.
-            MediaSessionCompatApi21.setFlags(mSessionObj,
+            mSessionFwk.setFlags(
                     flags | FLAG_HANDLES_MEDIA_BUTTONS | FLAG_HANDLES_TRANSPORT_CONTROLS);
         }
 
         @Override
         public void setPlaybackToLocal(int stream) {
-            MediaSessionCompatApi21.setPlaybackToLocal(mSessionObj, stream);
+            // TODO update APIs to use support version of AudioAttributes
+            AudioAttributes.Builder bob = new AudioAttributes.Builder();
+            bob.setLegacyStreamType(stream);
+            mSessionFwk.setPlaybackToLocal(bob.build());
         }
 
         @Override
         public void setPlaybackToRemote(VolumeProviderCompat volumeProvider) {
-            MediaSessionCompatApi21.setPlaybackToRemote(mSessionObj,
-                    volumeProvider.getVolumeProvider());
+            mSessionFwk.setPlaybackToRemote((VolumeProvider) volumeProvider.getVolumeProvider());
         }
 
         @Override
         public void setActive(boolean active) {
-            MediaSessionCompatApi21.setActive(mSessionObj, active);
+            mSessionFwk.setActive(active);
         }
 
         @Override
         public boolean isActive() {
-            return MediaSessionCompatApi21.isActive(mSessionObj);
+            return mSessionFwk.isActive();
         }
 
         @Override
@@ -3546,13 +3552,13 @@
                 }
                 mExtraControllerCallbacks.finishBroadcast();
             }
-            MediaSessionCompatApi21.sendSessionEvent(mSessionObj, event, extras);
+            mSessionFwk.sendSessionEvent(event, extras);
         }
 
         @Override
         public void release() {
             mDestroyed = true;
-            MediaSessionCompatApi21.release(mSessionObj);
+            mSessionFwk.release();
         }
 
         @Override
@@ -3572,8 +3578,8 @@
                 }
             }
             mExtraControllerCallbacks.finishBroadcast();
-            MediaSessionCompatApi21.setPlaybackState(mSessionObj,
-                    state == null ? null : state.getPlaybackState());
+            mSessionFwk.setPlaybackState(
+                    state == null ? null : (PlaybackState) state.getPlaybackState());
         }
 
         @Override
@@ -3584,36 +3590,37 @@
         @Override
         public void setMetadata(MediaMetadataCompat metadata) {
             mMetadata = metadata;
-            MediaSessionCompatApi21.setMetadata(mSessionObj,
-                    metadata == null ? null : metadata.getMediaMetadata());
+            mSessionFwk.setMetadata(
+                    metadata == null ? null : (MediaMetadata) metadata.getMediaMetadata());
         }
 
         @Override
         public void setSessionActivity(PendingIntent pi) {
-            MediaSessionCompatApi21.setSessionActivity(mSessionObj, pi);
+            mSessionFwk.setSessionActivity(pi);
         }
 
         @Override
         public void setMediaButtonReceiver(PendingIntent mbr) {
-            MediaSessionCompatApi21.setMediaButtonReceiver(mSessionObj, mbr);
+            mSessionFwk.setMediaButtonReceiver(mbr);
         }
 
         @Override
         public void setQueue(List<QueueItem> queue) {
             mQueue = queue;
-            List<Object> queueObjs = null;
-            if (queue != null) {
-                queueObjs = new ArrayList<>();
-                for (QueueItem item : queue) {
-                    queueObjs.add(item.getQueueItem());
-                }
+            if (queue == null) {
+                mSessionFwk.setQueue(null);
+                return;
             }
-            MediaSessionCompatApi21.setQueue(mSessionObj, queueObjs);
+            ArrayList<MediaSession.QueueItem> queueItemFwks = new ArrayList<>();
+            for (QueueItem item : queue) {
+                queueItemFwks.add((MediaSession.QueueItem) item.getQueueItem());
+            }
+            mSessionFwk.setQueue(queueItemFwks);
         }
 
         @Override
         public void setQueueTitle(CharSequence title) {
-            MediaSessionCompatApi21.setQueueTitle(mSessionObj, title);
+            mSessionFwk.setQueueTitle(title);
         }
 
         @Override
@@ -3621,7 +3628,7 @@
             if (android.os.Build.VERSION.SDK_INT < 22) {
                 mRatingType = type;
             } else {
-                MediaSessionCompatApi22.setRatingType(mSessionObj, type);
+                mSessionFwk.setRatingType(type);
             }
         }
 
@@ -3675,12 +3682,12 @@
 
         @Override
         public void setExtras(Bundle extras) {
-            MediaSessionCompatApi21.setExtras(mSessionObj, extras);
+            mSessionFwk.setExtras(extras);
         }
 
         @Override
         public Object getMediaSession() {
-            return mSessionObj;
+            return mSessionFwk;
         }
 
         @Override
@@ -3702,7 +3709,14 @@
             if (android.os.Build.VERSION.SDK_INT < 24) {
                 return null;
             } else {
-                return MediaSessionCompatApi24.getCallingPackage(mSessionObj);
+                try {
+                    Method getCallingPackageMethod = mSessionFwk.getClass().getMethod(
+                            "getCallingPackage");
+                    return (String) getCallingPackageMethod.invoke(mSessionFwk);
+                } catch (Exception e) {
+                    Log.e(TAG, "Cannot execute MediaSession.getCallingPackage()", e);
+                }
+                return null;
             }
         }
 
@@ -4027,7 +4041,7 @@
         @Override
         public final @NonNull RemoteUserInfo getCurrentControllerInfo() {
             android.media.session.MediaSessionManager.RemoteUserInfo info =
-                    ((MediaSession) mSessionObj).getCurrentControllerInfo();
+                    ((MediaSession) mSessionFwk).getCurrentControllerInfo();
             return new RemoteUserInfo(info);
         }
     }
diff --git a/media/src/main/java/android/support/v4/media/session/PlaybackStateCompat.java b/media/src/main/java/android/support/v4/media/session/PlaybackStateCompat.java
index bed04f3..8911fd7 100644
--- a/media/src/main/java/android/support/v4/media/session/PlaybackStateCompat.java
+++ b/media/src/main/java/android/support/v4/media/session/PlaybackStateCompat.java
@@ -17,6 +17,7 @@
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
 
+import android.media.session.PlaybackState;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Parcel;
@@ -549,7 +550,7 @@
     final long mActiveItemId;
     final Bundle mExtras;
 
-    private Object mStateObj;
+    private PlaybackState mStateFwk;
 
     PlaybackStateCompat(int state, long position, long bufferedPosition,
             float rate, long actions, int errorCode, CharSequence errorMessage, long updateTime,
@@ -797,34 +798,35 @@
      */
     public static PlaybackStateCompat fromPlaybackState(Object stateObj) {
         if (stateObj != null && Build.VERSION.SDK_INT >= 21) {
-            List<Object> customActionObjs = PlaybackStateCompatApi21.getCustomActions(stateObj);
+            PlaybackState stateFwk = (PlaybackState) stateObj;
+            List<PlaybackState.CustomAction> customActionFwks = stateFwk.getCustomActions();
             List<PlaybackStateCompat.CustomAction> customActions = null;
-            if (customActionObjs != null) {
-                customActions = new ArrayList<>(customActionObjs.size());
-                for (Object customActionObj : customActionObjs) {
-                    customActions.add(CustomAction.fromCustomAction(customActionObj));
+            if (customActionFwks != null) {
+                customActions = new ArrayList<>(customActionFwks.size());
+                for (Object customActionFwk : customActionFwks) {
+                    customActions.add(CustomAction.fromCustomAction(customActionFwk));
                 }
             }
             Bundle extras;
             if (Build.VERSION.SDK_INT >= 22) {
-                extras = PlaybackStateCompatApi22.getExtras(stateObj);
+                extras = stateFwk.getExtras();
             } else {
                 extras = null;
             }
-            PlaybackStateCompat state = new PlaybackStateCompat(
-                    PlaybackStateCompatApi21.getState(stateObj),
-                    PlaybackStateCompatApi21.getPosition(stateObj),
-                    PlaybackStateCompatApi21.getBufferedPosition(stateObj),
-                    PlaybackStateCompatApi21.getPlaybackSpeed(stateObj),
-                    PlaybackStateCompatApi21.getActions(stateObj),
+            PlaybackStateCompat stateCompat = new PlaybackStateCompat(
+                    stateFwk.getState(),
+                    stateFwk.getPosition(),
+                    stateFwk.getBufferedPosition(),
+                    stateFwk.getPlaybackSpeed(),
+                    stateFwk.getActions(),
                     ERROR_CODE_UNKNOWN_ERROR,
-                    PlaybackStateCompatApi21.getErrorMessage(stateObj),
-                    PlaybackStateCompatApi21.getLastPositionUpdateTime(stateObj),
+                    stateFwk.getErrorMessage(),
+                    stateFwk.getLastPositionUpdateTime(),
                     customActions,
-                    PlaybackStateCompatApi21.getActiveQueueItemId(stateObj),
+                    stateFwk.getActiveQueueItemId(),
                     extras);
-            state.mStateObj = stateObj;
-            return state;
+            stateCompat.mStateFwk = stateFwk;
+            return stateCompat;
         } else {
             return null;
         }
@@ -839,27 +841,23 @@
      * @return An equivalent {@link android.media.session.PlaybackState} object, or null if none.
      */
     public Object getPlaybackState() {
-        if (mStateObj == null && Build.VERSION.SDK_INT >= 21) {
-            List<Object> customActions = null;
-            if (mCustomActions != null) {
-                customActions = new ArrayList<>(mCustomActions.size());
-                for (PlaybackStateCompat.CustomAction customAction : mCustomActions) {
-                    customActions.add(customAction.getCustomAction());
-                }
+        if (mStateFwk == null && Build.VERSION.SDK_INT >= 21) {
+            PlaybackState.Builder builder = new PlaybackState.Builder();
+            builder.setState(mState, mPosition, mSpeed, mUpdateTime);
+            builder.setBufferedPosition(mBufferedPosition);
+            builder.setActions(mActions);
+            builder.setErrorMessage(mErrorMessage);
+            for (PlaybackStateCompat.CustomAction customAction : mCustomActions) {
+                builder.addCustomAction(
+                        (PlaybackState.CustomAction) customAction.getCustomAction());
             }
+            builder.setActiveQueueItemId(mActiveItemId);
             if (Build.VERSION.SDK_INT >= 22) {
-                mStateObj = PlaybackStateCompatApi22.newInstance(mState, mPosition,
-                        mBufferedPosition,
-                        mSpeed, mActions, mErrorMessage, mUpdateTime,
-                        customActions, mActiveItemId, mExtras);
-            } else {
-                //noinspection AndroidLintNewApi - NewApi lint fails to handle nested checks.
-                mStateObj = PlaybackStateCompatApi21.newInstance(mState, mPosition,
-                        mBufferedPosition, mSpeed, mActions, mErrorMessage, mUpdateTime,
-                        customActions, mActiveItemId);
+                builder.setExtras(mExtras);
             }
+            mStateFwk = builder.build();
         }
-        return mStateObj;
+        return mStateFwk;
     }
 
     public static final Parcelable.Creator<PlaybackStateCompat> CREATOR =
@@ -887,7 +885,7 @@
         private final int mIcon;
         private final Bundle mExtras;
 
-        private Object mCustomActionObj;
+        private PlaybackState.CustomAction mCustomActionFwk;
 
         /**
          * Use {@link PlaybackStateCompat.CustomAction.Builder#build()}.
@@ -935,13 +933,16 @@
                 return null;
             }
 
-            PlaybackStateCompat.CustomAction customAction = new PlaybackStateCompat.CustomAction(
-                    PlaybackStateCompatApi21.CustomAction.getAction(customActionObj),
-                    PlaybackStateCompatApi21.CustomAction.getName(customActionObj),
-                    PlaybackStateCompatApi21.CustomAction.getIcon(customActionObj),
-                    PlaybackStateCompatApi21.CustomAction.getExtras(customActionObj));
-            customAction.mCustomActionObj = customActionObj;
-            return customAction;
+            PlaybackState.CustomAction customActionFwk =
+                    (PlaybackState.CustomAction) customActionObj;
+            PlaybackStateCompat.CustomAction customActionCompat =
+                    new PlaybackStateCompat.CustomAction(
+                            customActionFwk.getAction(),
+                            customActionFwk.getName(),
+                            customActionFwk.getIcon(),
+                            customActionFwk.getExtras());
+            customActionCompat.mCustomActionFwk = customActionFwk;
+            return customActionCompat;
         }
 
         /**
@@ -955,13 +956,14 @@
          * or null if none.
          */
         public Object getCustomAction() {
-            if (mCustomActionObj != null || Build.VERSION.SDK_INT < 21) {
-                return mCustomActionObj;
+            if (mCustomActionFwk != null || Build.VERSION.SDK_INT < 21) {
+                return mCustomActionFwk;
             }
 
-            mCustomActionObj = PlaybackStateCompatApi21.CustomAction.newInstance(mAction,
-                    mName, mIcon, mExtras);
-            return mCustomActionObj;
+            PlaybackState.CustomAction.Builder builder = new PlaybackState.CustomAction.Builder(
+                    mAction, mName, mIcon);
+            builder.setExtras(mExtras);
+            return builder.build();
         }
 
         public static final Parcelable.Creator<PlaybackStateCompat.CustomAction> CREATOR
diff --git a/media/api21/androidx/media/AudioAttributesImplApi21.java b/media/src/main/java/androidx/media/AudioAttributesImplApi21.java
similarity index 98%
rename from media/api21/androidx/media/AudioAttributesImplApi21.java
rename to media/src/main/java/androidx/media/AudioAttributesImplApi21.java
index 6c67cc4..4df1234 100644
--- a/media/api21/androidx/media/AudioAttributesImplApi21.java
+++ b/media/src/main/java/androidx/media/AudioAttributesImplApi21.java
@@ -171,6 +171,9 @@
     //////////////////////////////////////////////////////////////////////
     // Other public methods
 
+    /**
+     * Create AudioAttributesImpl from Bundle
+     */
     public static AudioAttributesImpl fromBundle(Bundle bundle) {
         if (bundle == null) {
             return null;
diff --git a/media/src/main/java/androidx/media/MediaBrowserServiceCompat.java b/media/src/main/java/androidx/media/MediaBrowserServiceCompat.java
index 78b9394..6341950 100644
--- a/media/src/main/java/androidx/media/MediaBrowserServiceCompat.java
+++ b/media/src/main/java/androidx/media/MediaBrowserServiceCompat.java
@@ -55,6 +55,8 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
+import android.media.browse.MediaBrowser;
+import android.media.session.MediaSession;
 import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
@@ -293,22 +295,20 @@
     }
 
     @RequiresApi(21)
-    class MediaBrowserServiceImplApi21 implements MediaBrowserServiceImpl,
-            MediaBrowserServiceCompatApi21.ServiceCompatProxy {
+    class MediaBrowserServiceImplApi21 implements MediaBrowserServiceImpl {
         final List<Bundle> mRootExtrasList = new ArrayList<>();
-        Object mServiceObj;
+        MediaBrowserService mServiceFwk;
         Messenger mMessenger;
 
         @Override
         public void onCreate() {
-            mServiceObj = MediaBrowserServiceCompatApi21.createService(
-                    MediaBrowserServiceCompat.this, this);
-            MediaBrowserServiceCompatApi21.onCreate(mServiceObj);
+            mServiceFwk = new MediaBrowserServiceApi21(MediaBrowserServiceCompat.this);
+            mServiceFwk.onCreate();
         }
 
         @Override
         public IBinder onBind(Intent intent) {
-            return MediaBrowserServiceCompatApi21.onBind(mServiceObj, intent);
+            return mServiceFwk.onBind(intent);
         }
 
         @Override
@@ -326,7 +326,7 @@
                         }
                         mRootExtrasList.clear();
                     }
-                    MediaBrowserServiceCompatApi21.setSessionToken(mServiceObj, token.getToken());
+                    mServiceFwk.setSessionToken((MediaSession.Token) token.getToken());
                 }
             });
         }
@@ -344,8 +344,7 @@
             notifyChildrenChangedForCompat(remoteUserInfo, parentId, options);
         }
 
-        @Override
-        public MediaBrowserServiceCompatApi21.BrowserRoot onGetRoot(
+        public BrowserRoot onGetRoot(
                 String clientPackageName, int clientUid, Bundle rootHints) {
             Bundle rootExtras = null;
             if (rootHints != null && rootHints.getInt(EXTRA_CLIENT_VERSION, 0) != 0) {
@@ -377,39 +376,37 @@
             } else if (root.getExtras() != null) {
                 rootExtras.putAll(root.getExtras());
             }
-            return new MediaBrowserServiceCompatApi21.BrowserRoot(
-                    root.getRootId(), rootExtras);
+            return new BrowserRoot(root.getRootId(), rootExtras);
         }
 
-        @Override
         public void onLoadChildren(String parentId,
-                final MediaBrowserServiceCompatApi21.ResultWrapper<List<Parcel>> resultWrapper) {
-            final Result<List<MediaBrowserCompat.MediaItem>> result
-                    = new Result<List<MediaBrowserCompat.MediaItem>>(parentId) {
-                @Override
-                void onResultSent(List<MediaBrowserCompat.MediaItem> list) {
-                    List<Parcel> parcelList = null;
-                    if (list != null) {
-                        parcelList = new ArrayList<>();
-                        for (MediaBrowserCompat.MediaItem item : list) {
-                            Parcel parcel = Parcel.obtain();
-                            item.writeToParcel(parcel, 0);
-                            parcelList.add(parcel);
+                final ResultWrapper<List<Parcel>> resultWrapper) {
+            final Result<List<MediaBrowserCompat.MediaItem>> result =
+                    new Result<List<MediaBrowserCompat.MediaItem>>(parentId) {
+                        @Override
+                        void onResultSent(List<MediaBrowserCompat.MediaItem> list) {
+                            List<Parcel> parcelList = null;
+                            if (list != null) {
+                                parcelList = new ArrayList<>();
+                                for (MediaBrowserCompat.MediaItem item : list) {
+                                    Parcel parcel = Parcel.obtain();
+                                    item.writeToParcel(parcel, 0);
+                                    parcelList.add(parcel);
+                                }
+                            }
+                            resultWrapper.sendResult(parcelList);
                         }
-                    }
-                    resultWrapper.sendResult(parcelList);
-                }
 
-                @Override
-                public void detach() {
-                    resultWrapper.detach();
-                }
-            };
+                        @Override
+                        public void detach() {
+                            resultWrapper.detach();
+                        }
+                    };
             MediaBrowserServiceCompat.this.onLoadChildren(parentId, result);
         }
 
         void notifyChildrenChangedForFramework(final String parentId, final Bundle options) {
-            MediaBrowserServiceCompatApi21.notifyChildrenChanged(mServiceObj, parentId);
+            mServiceFwk.notifyChildrenChanged(parentId);
         }
 
         void notifyChildrenChangedForCompat(final String parentId, final Bundle options) {
@@ -473,85 +470,114 @@
             }
             return mCurConnection.browserInfo;
         }
+
+        class MediaBrowserServiceApi21 extends MediaBrowserService {
+            MediaBrowserServiceApi21(Context context) {
+                attachBaseContext(context);
+            }
+
+            @Override
+            public MediaBrowserService.BrowserRoot onGetRoot(String clientPackageName,
+                    int clientUid, Bundle rootHints) {
+                MediaSessionCompat.ensureClassLoader(rootHints);
+                MediaBrowserServiceCompat.BrowserRoot browserRootCompat =
+                        MediaBrowserServiceImplApi21.this.onGetRoot(clientPackageName, clientUid,
+                                rootHints == null ? null : new Bundle(rootHints));
+                return browserRootCompat == null ? null : new MediaBrowserService.BrowserRoot(
+                        browserRootCompat.mRootId, browserRootCompat.mExtras);
+            }
+
+            @Override
+            public void onLoadChildren(String parentId,
+                    Result<List<MediaBrowser.MediaItem>> result) {
+                MediaBrowserServiceImplApi21.this.onLoadChildren(parentId,
+                        new ResultWrapper<List<Parcel>>(result));
+            }
+        }
     }
 
     @RequiresApi(23)
-    class MediaBrowserServiceImplApi23 extends MediaBrowserServiceImplApi21 implements
-            MediaBrowserServiceCompatApi23.ServiceCompatProxy {
+    class MediaBrowserServiceImplApi23 extends MediaBrowserServiceImplApi21 {
         @Override
         public void onCreate() {
-            mServiceObj = MediaBrowserServiceCompatApi23.createService(
-                    MediaBrowserServiceCompat.this, this);
-            MediaBrowserServiceCompatApi21.onCreate(mServiceObj);
+            mServiceFwk = new MediaBrowserServiceApi23(MediaBrowserServiceCompat.this);
+            mServiceFwk.onCreate();
         }
 
-        @Override
-        public void onLoadItem(String itemId,
-                final MediaBrowserServiceCompatApi21.ResultWrapper<Parcel> resultWrapper) {
-            final Result<MediaBrowserCompat.MediaItem> result
-                    = new Result<MediaBrowserCompat.MediaItem>(itemId) {
-                @Override
-                void onResultSent(MediaBrowserCompat.MediaItem item) {
-                    if (item == null) {
-                        resultWrapper.sendResult(null);
-                    } else {
-                        Parcel parcelItem = Parcel.obtain();
-                        item.writeToParcel(parcelItem, 0);
-                        resultWrapper.sendResult(parcelItem);
-                    }
-                }
+        public void onLoadItem(String itemId, final ResultWrapper<Parcel> resultWrapper) {
+            final Result<MediaBrowserCompat.MediaItem> result =
+                    new Result<MediaBrowserCompat.MediaItem>(itemId) {
+                        @Override
+                        void onResultSent(MediaBrowserCompat.MediaItem item) {
+                            if (item == null) {
+                                resultWrapper.sendResult(null);
+                            } else {
+                                Parcel parcelItem = Parcel.obtain();
+                                item.writeToParcel(parcelItem, 0);
+                                resultWrapper.sendResult(parcelItem);
+                            }
+                        }
 
-                @Override
-                public void detach() {
-                    resultWrapper.detach();
-                }
-            };
+                        @Override
+                        public void detach() {
+                            resultWrapper.detach();
+                        }
+                    };
             MediaBrowserServiceCompat.this.onLoadItem(itemId, result);
         }
+
+        class MediaBrowserServiceApi23 extends MediaBrowserServiceApi21 {
+            MediaBrowserServiceApi23(Context context) {
+                super(context);
+            }
+
+            @Override
+            public void onLoadItem(String itemId, Result<MediaBrowser.MediaItem> result) {
+                MediaBrowserServiceImplApi23.this.onLoadItem(itemId,
+                        new ResultWrapper<Parcel>(result));
+            }
+        }
     }
 
     @RequiresApi(26)
-    class MediaBrowserServiceImplApi26 extends MediaBrowserServiceImplApi23 implements
-            MediaBrowserServiceCompatApi26.ServiceCompatProxy {
+    class MediaBrowserServiceImplApi26 extends MediaBrowserServiceImplApi23 {
         @Override
         public void onCreate() {
-            mServiceObj = MediaBrowserServiceCompatApi26.createService(
-                    MediaBrowserServiceCompat.this, this);
-            MediaBrowserServiceCompatApi21.onCreate(mServiceObj);
+            mServiceFwk = new MediaBrowserServiceApi26(MediaBrowserServiceCompat.this);
+            mServiceFwk.onCreate();
         }
 
-        @Override
         public void onLoadChildren(String parentId,
-                final MediaBrowserServiceCompatApi26.ResultWrapper resultWrapper,
+                final ResultWrapper<List<Parcel>> resultWrapper,
                 final Bundle options) {
-            final Result<List<MediaBrowserCompat.MediaItem>> result
-                    = new Result<List<MediaBrowserCompat.MediaItem>>(parentId) {
-                @Override
-                void onResultSent(List<MediaBrowserCompat.MediaItem> list) {
-                    if (list == null) {
-                        resultWrapper.sendResult(null);
-                        return;
-                    }
-                    if ((getFlags() & RESULT_FLAG_OPTION_NOT_HANDLED) != 0) {
-                        // If onLoadChildren(options) is not overridden, the list we get here is not
-                        // paginated. Therefore, we need to manually cut the list. In other words,
-                        // we need to apply options here.
-                        list = applyOptions(list, options);
-                    }
-                    List<Parcel> parcelList = new ArrayList<>();
-                    for (MediaBrowserCompat.MediaItem item : list) {
-                        Parcel parcel = Parcel.obtain();
-                        item.writeToParcel(parcel, 0);
-                        parcelList.add(parcel);
-                    }
-                    resultWrapper.sendResult(parcelList);
-                }
+            final Result<List<MediaBrowserCompat.MediaItem>> result =
+                    new Result<List<MediaBrowserCompat.MediaItem>>(parentId) {
+                        @Override
+                        void onResultSent(List<MediaBrowserCompat.MediaItem> list) {
+                            if (list == null) {
+                                resultWrapper.sendResult(null);
+                                return;
+                            }
+                            if ((getFlags() & RESULT_FLAG_OPTION_NOT_HANDLED) != 0) {
+                                // If onLoadChildren(options) is not overridden, the list we get
+                                // here is not paginated. Therefore, we need to manually cut
+                                // the list. In other words, we need to apply options here.
+                                list = applyOptions(list, options);
+                            }
+                            List<Parcel> parcelList = new ArrayList<>();
+                            for (MediaBrowserCompat.MediaItem item : list) {
+                                Parcel parcel = Parcel.obtain();
+                                item.writeToParcel(parcel, 0);
+                                parcelList.add(parcel);
+                            }
+                            resultWrapper.sendResult(parcelList);
+                        }
 
-                @Override
-                public void detach() {
-                    resultWrapper.detach();
-                }
-            };
+                        @Override
+                        public void detach() {
+                            resultWrapper.detach();
+                        }
+                    };
             MediaBrowserServiceCompat.this.onLoadChildren(parentId, result, options);
         }
 
@@ -562,18 +588,31 @@
                 return mCurConnection.rootHints == null ? null
                         : new Bundle(mCurConnection.rootHints);
             }
-            return MediaBrowserServiceCompatApi26.getBrowserRootHints(mServiceObj);
+            return mServiceFwk.getBrowserRootHints();
         }
 
         @Override
         void notifyChildrenChangedForFramework(final String parentId, final Bundle options) {
             if (options != null) {
-                MediaBrowserServiceCompatApi26.notifyChildrenChanged(mServiceObj, parentId,
-                        options);
+                mServiceFwk.notifyChildrenChanged(parentId, options);
             } else {
                 super.notifyChildrenChangedForFramework(parentId, options);
             }
         }
+
+        class MediaBrowserServiceApi26 extends MediaBrowserServiceApi23 {
+            MediaBrowserServiceApi26(Context context) {
+                super(context);
+            }
+
+            @Override
+            public void onLoadChildren(String parentId, Result<List<MediaBrowser.MediaItem>> result,
+                    Bundle options) {
+                MediaSessionCompat.ensureClassLoader(options);
+                MediaBrowserServiceImplApi26.this.onLoadChildren(parentId,
+                        new ResultWrapper<List<Parcel>>(result), options);
+            }
+        }
     }
 
     @RequiresApi(28)
@@ -584,9 +623,9 @@
             if (mCurConnection != null) {
                 return mCurConnection.browserInfo;
             }
-            android.media.session.MediaSessionManager.RemoteUserInfo userInfoObj =
-                    ((MediaBrowserService) mServiceObj).getCurrentBrowserInfo();
-            return new RemoteUserInfo(userInfoObj);
+            android.media.session.MediaSessionManager.RemoteUserInfo userInfoFwk =
+                    mServiceFwk.getCurrentBrowserInfo();
+            return new RemoteUserInfo(userInfoFwk);
         }
     }
 
@@ -1158,6 +1197,46 @@
         }
     }
 
+    @RequiresApi(21)
+    static class ResultWrapper<T> {
+        MediaBrowserService.Result mResultFwk;
+
+        ResultWrapper(MediaBrowserService.Result result) {
+            mResultFwk = result;
+        }
+
+        public void sendResult(T result) {
+            if (result instanceof List) {
+                mResultFwk.sendResult(parcelListToItemList((List<Parcel>) result));
+            } else if (result instanceof Parcel) {
+                Parcel parcel = (Parcel) result;
+                parcel.setDataPosition(0);
+                mResultFwk.sendResult(MediaBrowser.MediaItem.CREATOR.createFromParcel(parcel));
+                parcel.recycle();
+            } else {
+                // The result is null or an invalid instance.
+                mResultFwk.sendResult(null);
+            }
+        }
+
+        public void detach() {
+            mResultFwk.detach();
+        }
+
+        List<MediaBrowser.MediaItem> parcelListToItemList(List<Parcel> parcelList) {
+            if (parcelList == null) {
+                return null;
+            }
+            List<MediaBrowser.MediaItem> items = new ArrayList<>();
+            for (Parcel parcel : parcelList) {
+                parcel.setDataPosition(0);
+                items.add(MediaBrowser.MediaItem.CREATOR.createFromParcel(parcel));
+                parcel.recycle();
+            }
+            return items;
+        }
+    }
+
     /**
      * Attaches to the base context. This method is added to change the visibility of
      * {@link Service#attachBaseContext(Context)}.
diff --git a/media/src/main/java/androidx/media/MediaSessionManagerImplApi28.java b/media/src/main/java/androidx/media/MediaSessionManagerImplApi28.java
index b2f72f52..a276db3 100644
--- a/media/src/main/java/androidx/media/MediaSessionManagerImplApi28.java
+++ b/media/src/main/java/androidx/media/MediaSessionManagerImplApi28.java
@@ -33,10 +33,16 @@
 
     @Override
     public boolean isTrustedForMediaControl(MediaSessionManager.RemoteUserInfoImpl userInfo) {
-        if (userInfo instanceof RemoteUserInfoImplApi28) {
-            return mObject.isTrustedForMediaControl(((RemoteUserInfoImplApi28) userInfo).mObject);
-        }
-        return false;
+        // Don't use framework's isTrustedForMediaControl().
+        // In P, framework's isTrustedForMediaControl() does the sanity check whether the UID, PID,
+        // and package name match. In MediaSession2/MediaController2, Context#getPackageName() is
+        // used by MediaController2 to tell MediaSession2 the package name.
+        // However, UID, PID and Context#getPackageName() may not match if a activity/service runs
+        // on the another app's process by specifying android:process in the AndroidManifest.xml.
+        // In that case, sanity check will always fail.
+        // Alternative way is to use Context#getOpPackageName() for sending the package name,
+        // but it's hidden so we cannot use it.
+        return super.isTrustedForMediaControl(userInfo);
     }
 
     static final class RemoteUserInfoImplApi28 implements MediaSessionManager.RemoteUserInfoImpl {
diff --git a/media/src/main/java/androidx/media/MediaSessionManagerImplBase.java b/media/src/main/java/androidx/media/MediaSessionManagerImplBase.java
index c4e5e82..de0c5ad 100644
--- a/media/src/main/java/androidx/media/MediaSessionManagerImplBase.java
+++ b/media/src/main/java/androidx/media/MediaSessionManagerImplBase.java
@@ -55,24 +55,18 @@
     @Override
     public boolean isTrustedForMediaControl(
             @NonNull MediaSessionManager.RemoteUserInfoImpl userInfo) {
-        ApplicationInfo applicationInfo;
         try {
-            applicationInfo = mContext.getPackageManager().getApplicationInfo(
+            ApplicationInfo applicationInfo = mContext.getPackageManager().getApplicationInfo(
                     userInfo.getPackageName(), 0);
+            if (applicationInfo == null) {
+                return false;
+            }
         } catch (PackageManager.NameNotFoundException e) {
             if (DEBUG) {
                 Log.d(TAG, "Package " + userInfo.getPackageName() + " doesn't exist");
             }
             return false;
         }
-
-        if (applicationInfo.uid != userInfo.getUid()) {
-            if (DEBUG) {
-                Log.d(TAG, "Package name " + userInfo.getPackageName()
-                        + " doesn't match with the uid " + userInfo.getUid());
-            }
-            return false;
-        }
         return isPermissionGranted(userInfo, PERMISSION_STATUS_BAR_SERVICE)
                 || isPermissionGranted(userInfo, PERMISSION_MEDIA_CONTENT_CONTROL)
                 || userInfo.getUid() == Process.SYSTEM_UID
diff --git a/media/src/main/java/androidx/media/VolumeProviderCompat.java b/media/src/main/java/androidx/media/VolumeProviderCompat.java
index 4e8beff..4ddc486 100644
--- a/media/src/main/java/androidx/media/VolumeProviderCompat.java
+++ b/media/src/main/java/androidx/media/VolumeProviderCompat.java
@@ -18,6 +18,7 @@
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
 
+import android.media.VolumeProvider;
 import android.os.Build;
 import android.support.v4.media.session.MediaSessionCompat;
 
@@ -68,7 +69,7 @@
     private int mCurrentVolume;
     private Callback mCallback;
 
-    private Object mVolumeProviderObj;
+    private VolumeProvider mVolumeProviderFwk;
 
     /**
      * Create a new volume provider for handling volume events. You must specify
@@ -121,9 +122,9 @@
      */
     public final void setCurrentVolume(int currentVolume) {
         mCurrentVolume = currentVolume;
-        Object volumeProviderObj = getVolumeProvider();
-        if (volumeProviderObj != null && Build.VERSION.SDK_INT >= 21) {
-            VolumeProviderCompatApi21.setCurrentVolume(volumeProviderObj, currentVolume);
+        if (Build.VERSION.SDK_INT >= 21) {
+            VolumeProvider volumeProviderFwk = (VolumeProvider) getVolumeProvider();
+            volumeProviderFwk.setCurrentVolume(currentVolume);
         }
         if (mCallback != null) {
             mCallback.onVolumeChanged(this);
@@ -165,23 +166,20 @@
      * @return An equivalent {@link android.media.VolumeProvider} object, or null if none.
      */
     public Object getVolumeProvider() {
-        if (mVolumeProviderObj == null && Build.VERSION.SDK_INT >= 21) {
-            mVolumeProviderObj = VolumeProviderCompatApi21.createVolumeProvider(
-                    mControlType, mMaxVolume, mCurrentVolume,
-                    new VolumeProviderCompatApi21.Delegate() {
+        if (mVolumeProviderFwk == null && Build.VERSION.SDK_INT >= 21) {
+            mVolumeProviderFwk = new VolumeProvider(mControlType, mMaxVolume, mCurrentVolume) {
+                @Override
+                public void onSetVolumeTo(int volume) {
+                    VolumeProviderCompat.this.onSetVolumeTo(volume);
+                }
 
-                        @Override
-                        public void onSetVolumeTo(int volume) {
-                            VolumeProviderCompat.this.onSetVolumeTo(volume);
-                        }
-
-                        @Override
-                        public void onAdjustVolume(int direction) {
-                            VolumeProviderCompat.this.onAdjustVolume(direction);
-                        }
-                    });
+                @Override
+                public void onAdjustVolume(int direction) {
+                    VolumeProviderCompat.this.onAdjustVolume(direction);
+                }
+            };
         }
-        return mVolumeProviderObj;
+        return mVolumeProviderFwk;
     }
 
     /**
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MediaController2ProviderService.java b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MediaController2ProviderService.java
index 427ec59..cdeab4b 100644
--- a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MediaController2ProviderService.java
+++ b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MediaController2ProviderService.java
@@ -31,9 +31,9 @@
 import androidx.media.test.lib.TestUtils.SyncHandler;
 import androidx.media2.MediaBrowser2;
 import androidx.media2.MediaController2;
-import androidx.media2.MediaItem2;
 import androidx.media2.MediaLibraryService2.LibraryParams;
 import androidx.media2.MediaMetadata2;
+import androidx.media2.MediaUtils2;
 import androidx.media2.Rating2;
 import androidx.media2.SessionCommand2;
 import androidx.media2.SessionCommandGroup2;
@@ -95,7 +95,7 @@
         @Override
         public void create(final boolean isBrowser, final String controllerId,
                 ParcelImpl tokenParcelable, boolean waitForConnection) throws RemoteException {
-            final SessionToken2 token = ParcelUtils.fromParcelable(tokenParcelable);
+            final SessionToken2 token = MediaUtils2.fromParcelable(tokenParcelable);
             final TestControllerCallback callback = new TestControllerCallback();
 
             try {
@@ -140,7 +140,7 @@
         @Override
         public ParcelImpl getConnectedSessionToken(String controllerId) throws RemoteException {
             MediaController2 controller2 = mMediaController2Map.get(controllerId);
-            return (ParcelImpl) ParcelUtils.toParcelable(controller2.getConnectedSessionToken());
+            return MediaUtils2.toParcelable(controller2.getConnectedSessionToken());
         }
 
         @Override
@@ -156,9 +156,9 @@
         }
 
         @Override
-        public void prefetch(String controllerId) throws RemoteException {
+        public void prepare(String controllerId) throws RemoteException {
             MediaController2 controller2 = mMediaController2Map.get(controllerId);
-            controller2.prefetch();
+            controller2.prepare();
         }
 
         @Override
@@ -174,58 +174,56 @@
         }
 
         @Override
-        public void setPlaylist(String controllerId, List<Bundle> list, Bundle metadata)
+        public void setPlaylist(String controllerId, List<String> list, ParcelImpl metadata)
                 throws RemoteException {
             MediaController2 controller2 = mMediaController2Map.get(controllerId);
-            controller2.setPlaylist(
-                    MediaTestUtils.mediaItem2ListFromBundleList(list),
-                    MediaMetadata2.fromBundle(metadata));
+            controller2.setPlaylist(list, (MediaMetadata2) MediaUtils2.fromParcelable(metadata));
         }
 
         @Override
-        public void createAndSetDummyPlaylist(String controllerId, int size, Bundle metadata)
+        public void createAndSetDummyPlaylist(String controllerId, int size, ParcelImpl metadata)
                 throws RemoteException {
             MediaController2 controller2 = mMediaController2Map.get(controllerId);
-            List<MediaItem2> list = new ArrayList<>();
-            MediaItem2.Builder builder = new MediaItem2.Builder(0 /* flags */);
+            List<String> list = new ArrayList<>();
             for (int i = 0; i < size; i++) {
                 // Make media ID of each item same with its index.
-                list.add(builder.setMediaId(TestUtils.getMediaIdInDummyList(i)).build());
+                list.add(TestUtils.getMediaIdInDummyList(i));
             }
-            controller2.setPlaylist(list, MediaMetadata2.fromBundle(metadata));
+            controller2.setPlaylist(list, (MediaMetadata2) MediaUtils2.fromParcelable(metadata));
         }
 
         @Override
-        public void setMediaItem(String controllerId, Bundle item) throws RemoteException {
+        public void setMediaItem(String controllerId, String mediaId) throws RemoteException {
             MediaController2 controller2 = mMediaController2Map.get(controllerId);
-            controller2.setMediaItem(MediaItem2.fromBundle(item));
+            controller2.setMediaItem(mediaId);
         }
 
         @Override
-        public void updatePlaylistMetadata(String controllerId, Bundle metadata)
+        public void updatePlaylistMetadata(String controllerId, ParcelImpl metadata)
                 throws RemoteException {
             MediaController2 controller2 = mMediaController2Map.get(controllerId);
-            controller2.updatePlaylistMetadata(MediaMetadata2.fromBundle(metadata));
+            controller2.updatePlaylistMetadata(
+                    (MediaMetadata2) MediaUtils2.fromParcelable(metadata));
         }
 
         @Override
-        public void addPlaylistItem(String controllerId, int index, Bundle item)
+        public void addPlaylistItem(String controllerId, int index, String mediaId)
                 throws RemoteException {
             MediaController2 controller2 = mMediaController2Map.get(controllerId);
-            controller2.addPlaylistItem(index, MediaItem2.fromBundle(item));
+            controller2.addPlaylistItem(index, mediaId);
         }
 
         @Override
-        public void removePlaylistItem(String controllerId, Bundle item) throws RemoteException {
+        public void removePlaylistItem(String controllerId, int index) throws RemoteException {
             MediaController2 controller2 = mMediaController2Map.get(controllerId);
-            controller2.removePlaylistItem(MediaItem2.fromBundle(item));
+            controller2.removePlaylistItem(index);
         }
 
         @Override
-        public void replacePlaylistItem(String controllerId, int index, Bundle item)
+        public void replacePlaylistItem(String controllerId, int index, String mediaId)
                 throws RemoteException {
             MediaController2 controller2 = mMediaController2Map.get(controllerId);
-            controller2.replacePlaylistItem(index, MediaItem2.fromBundle(item));
+            controller2.replacePlaylistItem(index, mediaId);
         }
 
         @Override
@@ -241,9 +239,9 @@
         }
 
         @Override
-        public void skipToPlaylistItem(String controllerId, Bundle item) throws RemoteException {
+        public void skipToPlaylistItem(String controllerId, int index) throws RemoteException {
             MediaController2 controller2 = mMediaController2Map.get(controllerId);
-            controller2.skipToPlaylistItem(MediaItem2.fromBundle(item));
+            controller2.skipToPlaylistItem(index);
         }
 
         @Override
@@ -291,6 +289,18 @@
         }
 
         @Override
+        public void skipForward(String controllerId) throws RemoteException {
+            MediaController2 controller2 = mMediaController2Map.get(controllerId);
+            controller2.skipForward();
+        }
+
+        @Override
+        public void skipBackward(String controllerId) throws RemoteException {
+            MediaController2 controller2 = mMediaController2Map.get(controllerId);
+            controller2.skipBackward();
+        }
+
+        @Override
         public void playFromMediaId(String controllerId, String mediaId, Bundle extras)
                 throws RemoteException {
             MediaController2 controller2 = mMediaController2Map.get(controllerId);
@@ -312,24 +322,24 @@
         }
 
         @Override
-        public void prefetchFromMediaId(String controllerId, String mediaId, Bundle extras)
+        public void prepareFromMediaId(String controllerId, String mediaId, Bundle extras)
                 throws RemoteException {
             MediaController2 controller2 = mMediaController2Map.get(controllerId);
-            controller2.prefetchFromMediaId(mediaId, extras);
+            controller2.prepareFromMediaId(mediaId, extras);
         }
 
         @Override
-        public void prefetchFromSearch(String controllerId, String query, Bundle extras)
+        public void prepareFromSearch(String controllerId, String query, Bundle extras)
                 throws RemoteException {
             MediaController2 controller2 = mMediaController2Map.get(controllerId);
-            controller2.prefetchFromSearch(query, extras);
+            controller2.prepareFromSearch(query, extras);
         }
 
         @Override
-        public void prefetchFromUri(String controllerId, Uri uri, Bundle extras)
+        public void prepareFromUri(String controllerId, Uri uri, Bundle extras)
                 throws RemoteException {
             MediaController2 controller2 = mMediaController2Map.get(controllerId);
-            controller2.prefetchFromUri(uri, extras);
+            controller2.prepareFromUri(uri, extras);
         }
 
         @Override
@@ -371,14 +381,14 @@
         public void getLibraryRoot(String controllerId, ParcelImpl libraryParams)
                 throws RemoteException {
             MediaBrowser2 browser2 = (MediaBrowser2) mMediaController2Map.get(controllerId);
-            browser2.getLibraryRoot((LibraryParams) ParcelUtils.fromParcelable(libraryParams));
+            browser2.getLibraryRoot((LibraryParams) MediaUtils2.fromParcelable(libraryParams));
         }
 
         @Override
         public void subscribe(String controllerId, String parentId, ParcelImpl libraryParams)
                 throws RemoteException {
             MediaBrowser2 browser2 = (MediaBrowser2) mMediaController2Map.get(controllerId);
-            browser2.subscribe(parentId, (LibraryParams) ParcelUtils.fromParcelable(libraryParams));
+            browser2.subscribe(parentId, (LibraryParams) MediaUtils2.fromParcelable(libraryParams));
         }
 
         @Override
@@ -392,7 +402,7 @@
                 ParcelImpl libraryParams) throws RemoteException {
             MediaBrowser2 browser2 = (MediaBrowser2) mMediaController2Map.get(controllerId);
             browser2.getChildren(parentId, page, pageSize,
-                    (LibraryParams) ParcelUtils.fromParcelable(libraryParams));
+                    (LibraryParams) MediaUtils2.fromParcelable(libraryParams));
         }
 
         @Override
@@ -405,7 +415,7 @@
         public void search(String controllerId, String query, ParcelImpl libraryParams)
                 throws RemoteException {
             MediaBrowser2 browser2 = (MediaBrowser2) mMediaController2Map.get(controllerId);
-            browser2.search(query, (LibraryParams) ParcelUtils.fromParcelable(libraryParams));
+            browser2.search(query, (LibraryParams) MediaUtils2.fromParcelable(libraryParams));
         }
 
         @Override
@@ -413,7 +423,7 @@
                 ParcelImpl libraryParams) throws RemoteException {
             MediaBrowser2 browser2 = (MediaBrowser2) mMediaController2Map.get(controllerId);
             browser2.getSearchResult(query, page, pageSize,
-                    (LibraryParams) ParcelUtils.fromParcelable(libraryParams));
+                    (LibraryParams) MediaUtils2.fromParcelable(libraryParams));
         }
 
         private class TestControllerCallback extends MediaBrowser2.BrowserCallback {
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MediaTestUtils.java b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MediaTestUtils.java
index e38077f..0bcb0f4 100644
--- a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MediaTestUtils.java
+++ b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MediaTestUtils.java
@@ -61,19 +61,21 @@
 //    }
 
     /**
-     * Create a playlist for testing purpose
+     * Create a list of {@link FileMediaItem2} for testing purpose.
      * <p>
      * Caller's method name will be used for prefix of each media item's media id.
      *
      * @param size list size
      * @return the newly created playlist
      */
-    public static List<MediaItem2> createPlaylist(int size) {
+    public static List<MediaItem2> createFileMediaItems(int size) {
         final List<MediaItem2> list = new ArrayList<>();
         String caller = Thread.currentThread().getStackTrace()[1].getMethodName();
         for (int i = 0; i < size; i++) {
             list.add(new FileMediaItem2.Builder(new FileDescriptor())
-                    .setMediaId(caller + "_item_" + (size + 1))
+                    .setMetadata(new MediaMetadata2.Builder()
+                            .putString(MediaMetadata2.METADATA_KEY_MEDIA_ID,
+                                    caller + "_item_" + (i + 1)).build())
                     .build());
         }
         return list;
@@ -85,7 +87,7 @@
      * @return the newly created media item
      * @see #createMetadata()
      */
-    public static MediaItem2 createMediaItemWithMetadata() {
+    public static MediaItem2 createFileMediaItemWithMetadata() {
         return new FileMediaItem2.Builder(new FileDescriptor())
                 .setMetadata(createMetadata())
                 .build();
@@ -115,17 +117,6 @@
         return result;
     }
 
-    public static List<MediaItem2> playlistFromParcelableList(List<Parcelable> parcelables) {
-        if (parcelables == null) {
-            return null;
-        }
-        List<MediaItem2> result = new ArrayList<>();
-        for (Parcelable item : parcelables) {
-            result.add(MediaItem2.fromBundle((Bundle) item));
-        }
-        return result;
-    }
-
     public static List<Bundle> mediaItem2ListToBundleList(List<MediaItem2> list) {
         if (list == null) {
             return null;
@@ -137,17 +128,6 @@
         return result;
     }
 
-    public static List<MediaItem2> mediaItem2ListFromBundleList(List<Bundle> list) {
-        if (list == null) {
-            return null;
-        }
-        List<MediaItem2> result = new ArrayList<>();
-        for (int i = 0; i < list.size(); i++) {
-            result.add(MediaItem2.fromBundle(list.get(i)));
-        }
-        return result;
-    }
-
     public static LibraryParams createLibraryParams() {
         String callingTestName = Thread.currentThread().getStackTrace()[3].getMethodName();
 
@@ -156,41 +136,68 @@
         return new LibraryParams.Builder().setExtras(extras).build();
     }
 
-    public static void assertLibraryParamsEquals(LibraryParams a, LibraryParams b) {
+    // Note: It's not assertEquals() to avoid issue with the static import of JUnit's assertEquals.
+    // Otherwise, this API hides the statically imported JUnit's assertEquals and compile will fail.
+    public static void assertEqualLibraryParams(LibraryParams a, LibraryParams b) {
         if (a == null || b == null) {
             assertEquals(a, b);
         } else {
+            assertEquals(a.isRecent(), b.isRecent());
+            assertEquals(a.isOffline(), b.isOffline());
+            assertEquals(a.isSuggested(), b.isSuggested());
             assertTrue(TestUtils.equals(a.getExtras(), b.getExtras()));
         }
     }
 
-    public static void assertLibraryParamsWithBundle(LibraryParams a, Bundle b) {
-        if (a == null || b == null) {
-            assertEquals(a, b);
+    public static void assertEqualLibraryParams(LibraryParams params, Bundle rootExtras) {
+        if (params == null || rootExtras == null) {
+            assertEquals(params, rootExtras);
         } else {
-            assertEquals(a.isRecent(), b.getBoolean(BrowserRoot.EXTRA_RECENT));
-            assertEquals(a.isOffline(), b.getBoolean(BrowserRoot.EXTRA_OFFLINE));
-            assertEquals(a.isSuggested(), b.getBoolean(BrowserRoot.EXTRA_SUGGESTED));
-            assertTrue(TestUtils.contains(b, a.getExtras()));
+            assertEquals(params.isRecent(), rootExtras.getBoolean(BrowserRoot.EXTRA_RECENT));
+            assertEquals(params.isOffline(), rootExtras.getBoolean(BrowserRoot.EXTRA_OFFLINE));
+            assertEquals(params.isSuggested(), rootExtras.getBoolean(BrowserRoot.EXTRA_SUGGESTED));
+            assertTrue(TestUtils.contains(rootExtras, params.getExtras()));
         }
     }
 
-    public static void assertMediaItemWithId(String expectedId, MediaItem2 item) {
+    public static void assertMediaItemHasId(MediaItem2 item, String expectedId) {
         assertNotNull(item);
         assertNotNull(item.getMetadata());
         assertEquals(expectedId, item.getMetadata().getString(
                 MediaMetadata2.METADATA_KEY_MEDIA_ID));
     }
 
-    public static void assertPaginatedListWithIds(List<String> fullIdList, int page, int pageSize,
-            List<MediaItem2> paginatedList) {
+    public static void assertPaginatedListHasIds(List<MediaItem2> paginatedList,
+            List<String> fullIdList, int page, int pageSize) {
         int fromIndex = page * pageSize;
         int toIndex = Math.min((page + 1) * pageSize, fullIdList.size());
         // Compare the given results with originals.
         for (int originalIndex = fromIndex; originalIndex < toIndex; originalIndex++) {
             int relativeIndex = originalIndex - fromIndex;
-            assertMediaItemWithId(fullIdList.get(originalIndex),
-                    paginatedList.get(relativeIndex));
+            assertMediaItemHasId(paginatedList.get(relativeIndex), fullIdList.get(originalIndex)
+            );
         }
     }
+
+    public static void assertEqualMediaIds(MediaItem2 a, MediaItem2 b) {
+        assertEquals(a.getMetadata().getString(MediaMetadata2.METADATA_KEY_MEDIA_ID),
+                b.getMetadata().getString(MediaMetadata2.METADATA_KEY_MEDIA_ID));
+    }
+
+    public static void assertEqualMediaIds(List<MediaItem2> a, List<MediaItem2> b) {
+        assertEquals(a.size(), b.size());
+        for (int i = 0; i < a.size(); i++) {
+            assertEqualMediaIds(a.get(i), b.get(i));
+        }
+    }
+
+    public static void assertNotMediaItemSubclass(List<MediaItem2> list) {
+        for (MediaItem2 item : list) {
+            assertNotMediaItemSubclass(item);
+        }
+    }
+
+    public static void assertNotMediaItemSubclass(MediaItem2 item) {
+        assertEquals(MediaItem2.class, item.getClass());
+    }
 }
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/RemoteMediaSession2.java b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/RemoteMediaSession2.java
index c568ac3..a33ebf3 100644
--- a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/RemoteMediaSession2.java
+++ b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/RemoteMediaSession2.java
@@ -54,6 +54,7 @@
 import androidx.media2.MediaSession2;
 import androidx.media2.MediaSession2.CommandButton;
 import androidx.media2.MediaSession2.ControllerInfo;
+import androidx.media2.MediaUtils2;
 import androidx.media2.SessionCommand2;
 import androidx.media2.SessionCommandGroup2;
 import androidx.media2.SessionPlayer2;
@@ -169,7 +170,7 @@
             bundle.putBundle(KEY_MEDIA_ITEM, currentItem.toBundle());
         }
         if (metadata != null) {
-            bundle.putBundle(KEY_METADATA, metadata.toBundle());
+            ParcelUtils.putVersionedParcelable(bundle, KEY_METADATA, metadata);
         }
         return bundle;
     }
@@ -187,7 +188,7 @@
     public SessionToken2 getToken() {
         SessionToken2 token = null;
         try {
-            token = ParcelUtils.fromParcelable(mBinder.getToken(mSessionId));
+            token = MediaUtils2.fromParcelable(mBinder.getToken(mSessionId));
         } catch (RemoteException ex) {
             Log.e(TAG, "Failed to get session token. sessionId=" + mSessionId);
         }
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/RemoteMediaSessionCompat.java b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/RemoteMediaSessionCompat.java
index 6f46722..5bf0ad0 100644
--- a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/RemoteMediaSessionCompat.java
+++ b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/RemoteMediaSessionCompat.java
@@ -125,6 +125,18 @@
         }
     }
 
+    /**
+     * Since we cannot pass VolumeProviderCompat directly,
+     * we pass volumeControl, maxVolume, currentVolume instead.
+     */
+    public void setPlaybackToRemote(int volumeControl, int maxVolume, int currentVolume) {
+        try {
+            mBinder.setPlaybackToRemote(mSessionTag, volumeControl, maxVolume, currentVolume);
+        } catch (RemoteException ex) {
+            Log.e(TAG, "Failed to call setPlaybackToRemote()");
+        }
+    }
+
     public void setPlaybackState(PlaybackStateCompat state) {
         try {
             mBinder.setPlaybackState(mSessionTag,
@@ -202,6 +214,14 @@
         }
     }
 
+    public void sendSessionEvent(String event, Bundle extras) {
+        try {
+            mBinder.sendSessionEvent(mSessionTag, event, extras);
+        } catch (RemoteException ex) {
+            Log.e(TAG, "Failed to call sendSessionEvent()");
+        }
+    }
+
     ////////////////////////////////////////////////////////////////////////////////
     // Non-public methods
     ////////////////////////////////////////////////////////////////////////////////
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/tests/MediaBrowser2CallbackTest.java b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/tests/MediaBrowser2CallbackTest.java
index e29d308..55c8697 100644
--- a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/tests/MediaBrowser2CallbackTest.java
+++ b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/tests/MediaBrowser2CallbackTest.java
@@ -16,10 +16,9 @@
 
 package androidx.media.test.client.tests;
 
-import static androidx.media.test.client.MediaTestUtils.assertLibraryParamsEquals;
-import static androidx.media.test.client.MediaTestUtils.assertLibraryParamsWithBundle;
-import static androidx.media.test.client.MediaTestUtils.assertMediaItemWithId;
-import static androidx.media.test.client.MediaTestUtils.assertPaginatedListWithIds;
+import static androidx.media.test.client.MediaTestUtils.assertEqualLibraryParams;
+import static androidx.media.test.client.MediaTestUtils.assertMediaItemHasId;
+import static androidx.media.test.client.MediaTestUtils.assertPaginatedListHasIds;
 import static androidx.media.test.client.MediaTestUtils.createLibraryParams;
 import static androidx.media.test.lib.CommonConstants.MOCK_MEDIA_LIBRARY_SERVICE;
 import static androidx.media.test.lib.MediaBrowser2Constants.CUSTOM_ACTION_ASSERT_PARAMS;
@@ -172,7 +171,7 @@
         BrowserResult result = createBrowser().getItem(mediaId)
                 .get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
         assertEquals(RESULT_CODE_SUCCESS, result.getResultCode());
-        assertMediaItemWithId(mediaId, result.getMediaItem());
+        assertMediaItemHasId(result.getMediaItem(), mediaId);
     }
 
     @Test
@@ -202,9 +201,9 @@
         assertEquals(RESULT_CODE_SUCCESS, result.getResultCode());
         assertNull(result.getLibraryParams());
 
-        MediaTestUtils.assertPaginatedListWithIds(
-                MediaBrowser2Constants.GET_CHILDREN_RESULT,
-                page, pageSize, result.getMediaItems());
+        MediaTestUtils.assertPaginatedListHasIds(
+                result.getMediaItems(), MediaBrowser2Constants.GET_CHILDREN_RESULT,
+                page, pageSize);
     }
 
     @Test
@@ -269,7 +268,7 @@
             public void onSearchResultChanged(MediaBrowser2 browser,
                     String queryOut, int itemCount, LibraryParams params) {
                 assertEquals(query, queryOut);
-                assertLibraryParamsEquals(testParams, params);
+                assertEqualLibraryParams(testParams, params);
                 assertEquals(MediaBrowser2Constants.SEARCH_RESULT_COUNT, itemCount);
                 latchForSearch.countDown();
             }
@@ -286,8 +285,8 @@
         result = browser.getSearchResult(query, page, pageSize, testParams)
                 .get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
         assertEquals(RESULT_CODE_SUCCESS, result.getResultCode());
-        assertPaginatedListWithIds(MediaBrowser2Constants.SEARCH_RESULT, page, pageSize,
-                result.getMediaItems());
+        assertPaginatedListHasIds(result.getMediaItems(),
+                MediaBrowser2Constants.SEARCH_RESULT, page, pageSize);
     }
 
     @Test
@@ -305,7 +304,7 @@
             public void onSearchResultChanged(
                     MediaBrowser2 browser, String queryOut, int itemCount, LibraryParams params) {
                 assertEquals(query, queryOut);
-                assertLibraryParamsEquals(testParams, params);
+                assertEqualLibraryParams(testParams, params);
                 assertEquals(MediaBrowser2Constants.LONG_LIST_COUNT, itemCount);
                 latch.countDown();
             }
@@ -339,7 +338,7 @@
             public void onSearchResultChanged(
                     MediaBrowser2 browser, String queryOut, int itemCount, LibraryParams params) {
                 assertEquals(query, queryOut);
-                assertTrue(TestUtils.equals(testParams, params));
+                assertEqualLibraryParams(testParams, params);
                 assertEquals(MediaBrowser2Constants.SEARCH_RESULT_COUNT, itemCount);
                 latch.countDown();
             }
@@ -365,7 +364,7 @@
             public void onSearchResultChanged(
                     MediaBrowser2 browser, String queryOut, int itemCount, LibraryParams params) {
                 assertEquals(query, queryOut);
-                assertLibraryParamsEquals(testParams, params);
+                assertEqualLibraryParams(testParams, params);
                 assertEquals(0, itemCount);
                 latch.countDown();
             }
@@ -391,7 +390,7 @@
                     int itemCount, LibraryParams params) {
                 assertEquals(expectedParentId, parentId);
                 assertEquals(NOTIFY_CHILDREN_CHANGED_ITEM_COUNT, itemCount);
-                assertLibraryParamsWithBundle(params, NOTIFY_CHILDREN_CHANGED_EXTRAS);
+                MediaTestUtils.assertEqualLibraryParams(params, NOTIFY_CHILDREN_CHANGED_EXTRAS);
                 latch.countDown();
             }
         };
@@ -419,7 +418,7 @@
                     int itemCount, LibraryParams params) {
                 assertEquals(expectedParentId, parentId);
                 assertEquals(NOTIFY_CHILDREN_CHANGED_ITEM_COUNT, itemCount);
-                assertLibraryParamsWithBundle(params, NOTIFY_CHILDREN_CHANGED_EXTRAS);
+                assertEqualLibraryParams(params, NOTIFY_CHILDREN_CHANGED_EXTRAS);
                 latch.countDown();
             }
         };
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/tests/MediaBrowserCompatTestWithMediaLibraryService2.java b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/tests/MediaBrowserCompatTestWithMediaLibraryService2.java
index b5cd7e3..cf5fe1a 100644
--- a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/tests/MediaBrowserCompatTestWithMediaLibraryService2.java
+++ b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/tests/MediaBrowserCompatTestWithMediaLibraryService2.java
@@ -410,7 +410,7 @@
     public void testSubscribe() throws InterruptedException {
 //        prepareLooper();
 //        final String testParentId = "testSubscribeId";
-//        final List<MediaItem2> testList = TestUtils.createPlaylist(3);
+//        final List<MediaItem2> testList = TestUtils.createMediaItems(3);
 //
 //        final CountDownLatch latch = new CountDownLatch(1);
 //        final MediaLibrarySessionCallback callback = new MediaLibrarySessionCallback() {
@@ -458,7 +458,7 @@
 //        final String testParentId = "testSubscribe_withExtras";
 //        final Bundle testExtras = new Bundle();
 //        testExtras.putString(testParentId, testParentId);
-//        final List<MediaItem2> testList = TestUtils.createPlaylist(3);
+//        final List<MediaItem2> testList = TestUtils.createMediaItems(3);
 //
 //        final CountDownLatch latch = new CountDownLatch(1);
 //        final MediaLibrarySessionCallback callback = new MediaLibrarySessionCallback() {
@@ -508,7 +508,7 @@
     public void testSubscribe_withPagination() throws InterruptedException {
 //        prepareLooper();
 //        final String testParentId = "testSubscribe_pagination_ID";
-//        final List<MediaItem2> testList = TestUtils.createPlaylist(3);
+//        final List<MediaItem2> testList = TestUtils.createMediaItems(3);
 //        final int testPage = 2;
 //        final int testPageSize = 3;
 //        final Bundle testExtras = new Bundle();
@@ -619,7 +619,7 @@
 //        final String testUnsubscribedParentId = "testNotifyChildrenChanged22";
 //        final Bundle testExtras = new Bundle();
 //        testExtras.putString(testSubscribedParentId, testSubscribedParentId);
-//        final List<MediaItem2> testList = TestUtils.createPlaylist(3);
+//        final List<MediaItem2> testList = TestUtils.createMediaItems(3);
 //
 //        final CountDownLatch subscribeLatch = new CountDownLatch(1);
 //        final MediaLibrarySessionCallback callback = new MediaLibrarySessionCallback() {
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/tests/MediaController2CallbackTest.java b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/tests/MediaController2CallbackTest.java
index 222e94e..4fb8047 100644
--- a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/tests/MediaController2CallbackTest.java
+++ b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/tests/MediaController2CallbackTest.java
@@ -19,6 +19,8 @@
 import static android.media.AudioAttributes.CONTENT_TYPE_MUSIC;
 
 import static androidx.media.VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE;
+import static androidx.media.test.client.MediaTestUtils.assertEqualMediaIds;
+import static androidx.media.test.client.MediaTestUtils.assertNotMediaItemSubclass;
 import static androidx.media.test.lib.CommonConstants.DEFAULT_TEST_NAME;
 import static androidx.media.test.lib.CommonConstants.INDEX_FOR_NULL_ITEM;
 import static androidx.media.test.lib.CommonConstants.INDEX_FOR_UNKONWN_ITEM;
@@ -214,7 +216,7 @@
     public void testControllerCallback_sessionUpdatePlayer() throws InterruptedException {
         prepareLooper();
         final int testState = SessionPlayer2.PLAYER_STATE_PLAYING;
-        final List<MediaItem2> testPlaylist = MediaTestUtils.createPlaylist(3);
+        final List<MediaItem2> testPlaylist = MediaTestUtils.createFileMediaItems(3);
         final AudioAttributesCompat testAudioAttributes = new AudioAttributesCompat.Builder()
                 .setLegacyStreamType(AudioManager.STREAM_RING).build();
         final CountDownLatch latch = new CountDownLatch(3);
@@ -231,7 +233,8 @@
                     public void onPlaylistChanged(MediaController2 controller,
                             List<MediaItem2> list, MediaMetadata2 metadata) {
                         assertEquals(mController, controller);
-                        assertEquals(testPlaylist, list);
+                        assertNotMediaItemSubclass(list);
+                        assertEqualMediaIds(testPlaylist, list);
                         assertNull(metadata);
                         latch.countDown();
                     }
@@ -258,7 +261,7 @@
     public void testOnCurrentMediaItemChanged() throws Exception {
         prepareLooper();
         final int listSize = 5;
-        final List<MediaItem2> list = MediaTestUtils.createPlaylist(listSize);
+        final List<MediaItem2> list = MediaTestUtils.createFileMediaItems(listSize);
         mRemoteSession2.getMockPlayer().setPlaylistWithDummyItem(list);
 
         final int currentItemIndex = 3;
@@ -274,6 +277,7 @@
                                 // No check needed..
                                 break;
                             case 2:
+                                assertNotMediaItemSubclass(item);
                                 assertEquals(currentItem.getMediaId(), item.getMediaId());
                                 break;
                             case 1:
@@ -389,7 +393,7 @@
     @Test
     public void testOnPlaylistChanged() throws InterruptedException {
         prepareLooper();
-        final List<MediaItem2> testList = MediaTestUtils.createPlaylist(2);
+        final List<MediaItem2> testList = MediaTestUtils.createFileMediaItems(2);
         final AtomicReference<List<MediaItem2>> listFromCallback = new AtomicReference<>();
         final CountDownLatch latch = new CountDownLatch(1);
         final MediaController2.ControllerCallback callback =
@@ -398,11 +402,8 @@
                     public void onPlaylistChanged(MediaController2 controller,
                             List<MediaItem2> playlist, MediaMetadata2 metadata) {
                         assertNotNull(playlist);
-                        assertEquals(testList.size(), playlist.size());
-                        for (int i = 0; i < playlist.size(); i++) {
-                            assertEquals(
-                                    testList.get(i).getMediaId(), playlist.get(i).getMediaId());
-                        }
+                        assertNotMediaItemSubclass(playlist);
+                        assertEqualMediaIds(testList, playlist);
                         listFromCallback.set(playlist);
                         latch.countDown();
                     }
@@ -589,7 +590,7 @@
         prepareLooper();
         final CountDownLatch latch = new CountDownLatch(1);
 
-        final List<MediaItem2> testPlaylist = MediaTestUtils.createPlaylist(3);
+        final List<MediaItem2> testPlaylist = MediaTestUtils.createFileMediaItems(3);
         final int targetItemIndex = 0;
         final int testBufferingState = SessionPlayer2.BUFFERING_STATE_BUFFERING_AND_PLAYABLE;
         final long testBufferingPosition = 500;
@@ -600,6 +601,7 @@
             public void onBufferingStateChanged(MediaController2 controller, MediaItem2 item,
                     int state) {
                 controller.setTimeDiff(0L);
+                assertNotMediaItemSubclass(item);
                 assertEquals(testPlaylist.get(targetItemIndex).getMediaId(), item.getMediaId());
                 assertEquals(testBufferingState, state);
                 assertEquals(testBufferingState, controller.getBufferingState());
@@ -680,7 +682,7 @@
     public void testOnCustomCommand() throws InterruptedException {
         prepareLooper();
         final SessionCommand2 testCommand = new SessionCommand2(
-                SessionCommand2.COMMAND_CODE_PLAYER_PREFETCH);
+                SessionCommand2.COMMAND_CODE_PLAYER_PREPARE);
         final Bundle testArgs = TestUtils.createTestBundle();
 
         final CountDownLatch latch = new CountDownLatch(2);
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/tests/MediaController2LegacyTest.java b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/tests/MediaController2LegacyTest.java
index 05bd360..9f5ebbc 100644
--- a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/tests/MediaController2LegacyTest.java
+++ b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/tests/MediaController2LegacyTest.java
@@ -18,11 +18,14 @@
 
 import static android.support.mediacompat.testlib.util.IntentUtil.SERVICE_PACKAGE_NAME;
 
+import static androidx.media.test.client.MediaTestUtils.assertEqualMediaIds;
+import static androidx.media.test.client.MediaTestUtils.assertMediaItemHasId;
 import static androidx.media.test.lib.CommonConstants.DEFAULT_TEST_NAME;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 import android.app.PendingIntent;
@@ -30,26 +33,30 @@
 import android.content.Intent;
 import android.media.AudioManager;
 import android.os.Build;
+import android.os.Bundle;
 import android.support.v4.media.MediaMetadataCompat;
 import android.support.v4.media.session.MediaSessionCompat;
 import android.support.v4.media.session.MediaSessionCompat.QueueItem;
 import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.Log;
 
+import androidx.media.VolumeProviderCompat;
 import androidx.media.test.client.MediaTestUtils;
 import androidx.media.test.client.RemoteMediaSessionCompat;
 import androidx.media.test.lib.MockActivity;
+import androidx.media.test.lib.TestUtils;
 import androidx.media2.MediaController2;
 import androidx.media2.MediaController2.ControllerCallback;
 import androidx.media2.MediaItem2;
 import androidx.media2.MediaMetadata2;
 import androidx.media2.MediaUtils2;
+import androidx.media2.RemoteSessionPlayer2;
+import androidx.media2.SessionCommand2;
 import androidx.media2.SessionPlayer2;
-import androidx.test.filters.FlakyTest;
 import androidx.test.filters.SmallTest;
 
 import org.junit.After;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 
 import java.util.List;
@@ -59,9 +66,12 @@
 
 /**
  * Tests {@link MediaController2} interacting with {@link MediaSessionCompat}.
+ *
+ * TODO: Pull out callback tests to a separate file (i.e. MediaController2LegacyCallbackTest).
  */
 @SmallTest
 public class MediaController2LegacyTest extends MediaSession2TestBase {
+    private static final String TAG = "MediaController2LegacyTest";
 
     AudioManager mAudioManager;
     RemoteMediaSessionCompat mSession;
@@ -139,13 +149,57 @@
     }
 
     /**
+     * This also tests {@link ControllerCallback#onRepeatModeChanged(MediaController2, int)}.
+     */
+    @Test
+    public void testGetRepeatMode() throws Exception {
+        prepareLooper();
+        final int testRepeatMode = SessionPlayer2.REPEAT_MODE_GROUP;
+        final CountDownLatch latch = new CountDownLatch(1);
+        final ControllerCallback callback = new ControllerCallback() {
+            @Override
+            public void onRepeatModeChanged(MediaController2 controller, int repeatMode) {
+                assertEquals(testRepeatMode, repeatMode);
+                latch.countDown();
+            }
+        };
+        mController = createController(mSession.getSessionToken(), true, callback);
+
+        mSession.setRepeatMode(testRepeatMode);
+        assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertEquals(testRepeatMode, mController.getRepeatMode());
+    }
+
+    /**
+     * This also tests {@link ControllerCallback#onShuffleModeChanged(MediaController2, int)}.
+     */
+    @Test
+    public void testGetShuffleMode() throws Exception {
+        prepareLooper();
+        final int testShuffleMode = SessionPlayer2.SHUFFLE_MODE_GROUP;
+        final CountDownLatch latch = new CountDownLatch(1);
+        final ControllerCallback callback = new ControllerCallback() {
+            @Override
+            public void onShuffleModeChanged(MediaController2 controller, int shuffleMode) {
+                assertEquals(testShuffleMode, shuffleMode);
+                latch.countDown();
+            }
+        };
+        mController = createController(mSession.getSessionToken(), true, callback);
+
+        mSession.setShuffleMode(testShuffleMode);
+        assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertEquals(testShuffleMode, mController.getShuffleMode());
+    }
+
+    /**
      * This also tests {@link ControllerCallback#onPlaylistChanged(
      * MediaController2, List, MediaMetadata2)}.
      */
     @Test
     public void testGetPlaylist() throws Exception {
         prepareLooper();
-        final List<MediaItem2> testList = MediaTestUtils.createPlaylist(2);
+        final List<MediaItem2> testList = MediaTestUtils.createFileMediaItems(2);
         final List<QueueItem> testQueue = MediaUtils2.convertToQueueItemList(testList);
         final AtomicReference<List<MediaItem2>> listFromCallback = new AtomicReference<>();
         final CountDownLatch latch = new CountDownLatch(1);
@@ -194,7 +248,81 @@
     }
 
     @Test
-    @Ignore("b/110738672")
+    public void testGetCurrentMediaItemAfterConnected() throws Exception {
+        prepareLooper();
+        mController = createController(mSession.getSessionToken(), true, null);
+        assertNull(mController.getCurrentMediaItem());
+    }
+
+    @Test
+    public void testGetCurrentMediaItemAfterConnected_metadata() throws Exception {
+        prepareLooper();
+        final String testMediaId = "testGetCurrentMediaItemWhenConnected_metadata";
+        MediaMetadataCompat metadata = new MediaMetadataCompat.Builder()
+                .putText(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, testMediaId)
+                .build();
+        mSession.setMetadata(metadata);
+
+        mController = createController(mSession.getSessionToken(), true, null);
+        assertEquals(testMediaId, mController.getCurrentMediaItem().getMediaId());
+    }
+
+    @Test
+    public void testControllerCallback_onCurrentMediaItemChanged_byMetadataChange()
+            throws Exception {
+        prepareLooper();
+        final String testMediaId = "testControllerCallback_onCurrentMediaItemChanged_bySetMetadata";
+        final CountDownLatch latch = new CountDownLatch(1);
+        final ControllerCallback callback = new ControllerCallback() {
+            @Override
+            public void onCurrentMediaItemChanged(MediaController2 controller, MediaItem2 item) {
+                assertMediaItemHasId(item, testMediaId);
+                latch.countDown();
+            }
+        };
+        mController = createController(mSession.getSessionToken(), true, callback);
+        MediaMetadataCompat metadata = new MediaMetadataCompat.Builder()
+                .putText(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, testMediaId)
+                .build();
+        mSession.setMetadata(metadata);
+        assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testControllerCallback_onCurrentMediaItemChanged_byActiveQueueItemChange()
+            throws Exception {
+        prepareLooper();
+        final List<MediaItem2> testList = MediaTestUtils.createFileMediaItems(2);
+        final List<QueueItem> testQueue = MediaUtils2.convertToQueueItemList(testList);
+        mSession.setQueue(testQueue);
+
+        PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder();
+
+        // Set the current active queue item to index 'oldItemIndex'.
+        final int oldItemIndex = 0;
+        builder.setActiveQueueItemId(testQueue.get(oldItemIndex).getQueueId());
+        mSession.setPlaybackState(builder.build());
+
+        final int newItemIndex = 1;
+        final CountDownLatch latch = new CountDownLatch(1);
+        final ControllerCallback callback = new ControllerCallback() {
+            @Override
+            public void onCurrentMediaItemChanged(MediaController2 controller, MediaItem2 item) {
+                assertEqualMediaIds(testList.get(newItemIndex), item);
+                latch.countDown();
+            }
+        };
+        mController = createController(mSession.getSessionToken(), true, callback);
+
+        // The new playbackState will tell the controller that the active queue item is changed to
+        // 'newItemIndex'.
+        builder.setActiveQueueItemId(testQueue.get(newItemIndex).getQueueId());
+        mSession.setPlaybackState(builder.build());
+
+        assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
     public void testControllerCallback_onSeekCompleted() throws Exception {
         prepareLooper();
         final long testSeekPosition = 400;
@@ -222,13 +350,11 @@
     }
 
     @Test
-    @Ignore("b/110738672")
-    public void testControllerCallbackBufferingCompleted() throws Exception {
+    public void testControllerCallback_onBufferingCompleted() throws Exception {
         prepareLooper();
-        final List<MediaItem2> testPlaylist = MediaTestUtils.createPlaylist(1);
-        final List<QueueItem> testQueue = MediaUtils2.convertToQueueItemList(testPlaylist);
+        final List<MediaItem2> testPlaylist = MediaTestUtils.createFileMediaItems(1);
         final MediaMetadataCompat metadata = MediaUtils2.convertToMediaMetadataCompat(
-                testQueue.get(0).getDescription());
+                testPlaylist.get(0).getMetadata());
 
         final int testBufferingState = SessionPlayer2.BUFFERING_STATE_COMPLETE;
         final long testBufferingPosition = 500;
@@ -261,14 +387,12 @@
         assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
     }
 
-    @FlakyTest(bugId = 111433579)
     @Test
-    public void testControllerCallbackBufferingStarved() throws Exception {
+    public void testControllerCallback_onBufferingStarved() throws Exception {
         prepareLooper();
-        final List<MediaItem2> testPlaylist = MediaTestUtils.createPlaylist(1);
-        final List<QueueItem> testQueue = MediaUtils2.convertToQueueItemList(testPlaylist);
+        final List<MediaItem2> testPlaylist = MediaTestUtils.createFileMediaItems(1);
         final MediaMetadataCompat metadata = MediaUtils2.convertToMediaMetadataCompat(
-                testQueue.get(0).getDescription());
+                testPlaylist.get(0).getMetadata());
 
         final int testBufferingState = SessionPlayer2.BUFFERING_STATE_BUFFERING_AND_STARVED;
         final long testBufferingPosition = 0;
@@ -302,7 +426,6 @@
     }
 
     @Test
-    @Ignore("b/110738672")
     public void testControllerCallback_onPlayerStateChanged() throws Exception {
         prepareLooper();
         final int testPlayerState = SessionPlayer2.PLAYER_STATE_PLAYING;
@@ -331,13 +454,117 @@
     }
 
     @Test
+    public void testControllerCallback_onPlaybackSpeedChanged() throws Exception {
+        prepareLooper();
+        final float testSpeed = 3.0f;
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        final ControllerCallback callback = new ControllerCallback() {
+            @Override
+            public void onPlaybackSpeedChanged(MediaController2 controller, float speed) {
+                assertEquals(testSpeed, speed, 0.0f);
+                latch.countDown();
+            }
+        };
+        mController = createController(mSession.getSessionToken(), true, callback);
+        mSession.setPlaybackState(new PlaybackStateCompat.Builder()
+                .setState(PlaybackStateCompat.STATE_PLAYING, 0 /* position */,
+                        testSpeed /* playbackSpeed */)
+                .build());
+        assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testControllerCallback_onPlaybackInfoChanged_byPlaybackTypeChangeToRemote()
+            throws Exception {
+        prepareLooper();
+        final int volumeControlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE;
+        final int maxVolume = 100;
+        final int currentVolume = 45;
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        final ControllerCallback callback = new ControllerCallback() {
+            @Override
+            public void onPlaybackInfoChanged(MediaController2 controller,
+                    MediaController2.PlaybackInfo info) {
+                // Here, we are intentionally avoid using assertEquals(), since this callback
+                // can be called many times which of them have inaccurate values.
+                Log.d(TAG, "Given playbackType=" + info.getPlaybackType()
+                        + " controlType=" + info.getControlType()
+                        + " maxVolume=" + info.getMaxVolume()
+                        + " currentVolume=" + info.getCurrentVolume()
+                        + " audioAttrs=" + info.getAudioAttributes());
+                if (MediaController2.PlaybackInfo.PLAYBACK_TYPE_REMOTE == info.getPlaybackType()
+                        && volumeControlType == info.getControlType()
+                        && maxVolume == info.getMaxVolume()
+                        && currentVolume == info.getCurrentVolume()) {
+                    latch.countDown();
+                }
+            }
+        };
+        mController = createController(mSession.getSessionToken(), true, callback);
+        mSession.setPlaybackToRemote(volumeControlType, maxVolume, currentVolume);
+        assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testControllerCallback_onPlaybackInfoChanged_byPlaybackTypeChangeToLocal()
+            throws Exception {
+        prepareLooper();
+        mSession.setPlaybackToRemote(VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE, 100, 45);
+
+        final int testLocalStreamType = AudioManager.STREAM_ALARM;
+        final int maxVolume = mAudioManager.getStreamMaxVolume(testLocalStreamType);
+        final int currentVolume = mAudioManager.getStreamVolume(testLocalStreamType);
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        final ControllerCallback callback = new ControllerCallback() {
+            @Override
+            public void onPlaybackInfoChanged(MediaController2 controller,
+                    MediaController2.PlaybackInfo info) {
+                assertEquals(MediaController2.PlaybackInfo.PLAYBACK_TYPE_LOCAL,
+                        info.getPlaybackType());
+                assertEquals(RemoteSessionPlayer2.VOLUME_CONTROL_ABSOLUTE, info.getControlType());
+                assertEquals(maxVolume, info.getMaxVolume());
+                assertEquals(currentVolume, info.getCurrentVolume());
+                latch.countDown();
+            }
+        };
+        mController = createController(mSession.getSessionToken(), true, callback);
+        mSession.setPlaybackToLocal(testLocalStreamType);
+        assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testControllerCallback_onCustomCommand() throws Exception {
+        prepareLooper();
+        final String event = "testControllerCallback_onCustomCommand";
+        final Bundle extras = TestUtils.createTestBundle();
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        final ControllerCallback callback = new ControllerCallback() {
+            @Override
+            public MediaController2.ControllerResult onCustomCommand(MediaController2 controller,
+                    SessionCommand2 command, Bundle args) {
+                assertEquals(event, command.getCustomCommand());
+                assertTrue(TestUtils.equals(extras, args));
+                latch.countDown();
+                return null;
+            }
+        };
+        mController = createController(mSession.getSessionToken(), true, callback);
+        mSession.sendSessionEvent(event, extras);
+        assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
     public void testControllerCallback_onConnected() throws Exception {
         prepareLooper();
         mController = createController(mSession.getSessionToken());
     }
 
     @Test
-    public void testControllerCallback_releaseSession() throws Exception {
+    public void testControllerCallback_onDisconnected() throws Exception {
         prepareLooper();
         mController = createController(mSession.getSessionToken());
         mSession.release();
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/tests/MediaController2Test.java b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/tests/MediaController2Test.java
index f572560..3f224e1 100644
--- a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/tests/MediaController2Test.java
+++ b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/tests/MediaController2Test.java
@@ -21,6 +21,8 @@
 import static androidx.media.AudioAttributesCompat.CONTENT_TYPE_MUSIC;
 import static androidx.media.VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE;
 import static androidx.media.VolumeProviderCompat.VOLUME_CONTROL_FIXED;
+import static androidx.media.test.client.MediaTestUtils.assertEqualMediaIds;
+import static androidx.media.test.client.MediaTestUtils.assertNotMediaItemSubclass;
 import static androidx.media.test.lib.CommonConstants.DEFAULT_TEST_NAME;
 import static androidx.media.test.lib.MediaSession2Constants.TEST_GET_SESSION_ACTIVITY;
 
@@ -254,7 +256,7 @@
         final long bufferedPosition = 900000;
         final float speed = 0.5f;
         final long timeDiff = 102;
-        final MediaItem2 currentMediaItem = MediaTestUtils.createMediaItemWithMetadata();
+        final MediaItem2 currentMediaItem = MediaTestUtils.createFileMediaItemWithMetadata();
 
         Bundle config = RemoteMediaSession2.createMockPlayerConnectorConfig(
                 state, bufferingState, position, bufferedPosition, speed, null /* audioAttrs */,
@@ -267,7 +269,8 @@
         assertEquals(bufferedPosition, controller.getBufferedPosition());
         assertEquals(speed, controller.getPlaybackSpeed(), 0.0f);
         assertEquals(position + (long) (speed * timeDiff), controller.getCurrentPosition());
-        assertEquals(currentMediaItem, controller.getCurrentMediaItem());
+        assertNotMediaItemSubclass(controller.getCurrentMediaItem());
+        assertEqualMediaIds(currentMediaItem, controller.getCurrentMediaItem());
     }
 
     @Test
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/tests/MediaControllerCompatCallbackTestWithMediaSession2.java b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/tests/MediaControllerCompatCallbackTestWithMediaSession2.java
index 79c2da86..53023b2 100644
--- a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/tests/MediaControllerCompatCallbackTestWithMediaSession2.java
+++ b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/tests/MediaControllerCompatCallbackTestWithMediaSession2.java
@@ -132,7 +132,7 @@
         final int testState = SessionPlayer2.PLAYER_STATE_PLAYING;
         final int testBufferingPosition = 1500;
         final float testSpeed = 1.5f;
-        final List<MediaItem2> testPlaylist = MediaTestUtils.createPlaylist(3);
+        final List<MediaItem2> testPlaylist = MediaTestUtils.createFileMediaItems(3);
         final String testPlaylistTitle = "testPlaylistTitle";
         final MediaMetadata2 testPlaylistMetadata = new MediaMetadata2.Builder()
                 .putText(MediaMetadata2.METADATA_KEY_DISPLAY_TITLE, testPlaylistTitle).build();
@@ -374,7 +374,7 @@
     @Test
     public void testBufferingStateChange() throws Exception {
         prepareLooper();
-        final List<MediaItem2> testPlaylist = MediaTestUtils.createPlaylist(3);
+        final List<MediaItem2> testPlaylist = MediaTestUtils.createFileMediaItems(3);
         final int testItemIndex = 0;
         final int testBufferingState = SessionPlayer2.BUFFERING_STATE_BUFFERING_AND_PLAYABLE;
         final long testBufferingPosition = 500;
@@ -420,7 +420,7 @@
                 .setMetadata(metadata)
                 .build();
 
-        List<MediaItem2> playlist = MediaTestUtils.createPlaylist(5);
+        List<MediaItem2> playlist = MediaTestUtils.createFileMediaItems(5);
         final int testItemIndex = 3;
         playlist.set(testItemIndex, currentMediaItem);
         mSession.getMockPlayer().setPlaylistWithDummyItem(playlist);
@@ -441,7 +441,7 @@
     @Test
     public void testPlaylistAndPlaylistMetadataChange() throws Exception {
         prepareLooper();
-        final List<MediaItem2> playlist = MediaTestUtils.createPlaylist(5);
+        final List<MediaItem2> playlist = MediaTestUtils.createFileMediaItems(5);
         final String playlistTitle = "playlistTitle";
         MediaMetadata2 playlistMetadata = new MediaMetadata2.Builder()
                 .putText(MediaMetadata2.METADATA_KEY_DISPLAY_TITLE, playlistTitle).build();
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/tests/MediaItem2Test.java b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/tests/MediaItem2Test.java
new file mode 100644
index 0000000..222f0e1
--- /dev/null
+++ b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/tests/MediaItem2Test.java
@@ -0,0 +1,114 @@
+/*
+ * 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.media.test.client.tests;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Parcel;
+
+import androidx.media.test.lib.TestUtils;
+import androidx.media2.MediaItem2;
+import androidx.media2.MediaMetadata2;
+import androidx.media2.MediaUtils2;
+import androidx.media2.UriMediaItem2;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+import androidx.versionedparcelable.ParcelImpl;
+import androidx.versionedparcelable.ParcelUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests {@link MediaItem2}.
+ */
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN)
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class MediaItem2Test {
+    private Context mContext;
+
+    @Before
+    public void setUp() {
+        mContext = InstrumentationRegistry.getTargetContext();
+    }
+
+    @Test
+    public void testSubclass_sameProcess() {
+        final UriMediaItem2 testUriItem = createUriMediaItem2();
+        final ParcelImpl parcel = MediaUtils2.toParcelable(testUriItem);
+
+        final MediaItem2 testRemoteItem = MediaUtils2.fromParcelable(parcel);
+        assertEquals(testUriItem, testRemoteItem);
+    }
+
+    @Test
+    public void testSubclass_acrossProcessWithMediaUtils() {
+        final UriMediaItem2 testUriItem = createUriMediaItem2();
+
+        // Mocks the binder call across the processes by using writeParcelable/readParcelable
+        // which only happens between processes. Code snippets are copied from
+        // VersionedParcelIntegTest#parcelCopy.
+        final Parcel p = Parcel.obtain();
+        p.writeParcelable(MediaUtils2.toParcelable(testUriItem), 0);
+        p.setDataPosition(0);
+        final MediaItem2 testRemoteItem = MediaUtils2.fromParcelable(
+                (ParcelImpl) p.readParcelable(MediaItem2.class.getClassLoader()));
+
+        assertFalse(testRemoteItem instanceof UriMediaItem2);
+        assertEquals(testUriItem.getStartPosition(), testRemoteItem.getStartPosition());
+        assertEquals(testUriItem.getEndPosition(), testRemoteItem.getEndPosition());
+        TestUtils.equals(testUriItem.getMetadata().toBundle(),
+                testRemoteItem.getMetadata().toBundle());
+    }
+
+    @Test
+    public void testSubclass_acrossProcessWithParcelUtils() {
+        final UriMediaItem2 testUriItem = createUriMediaItem2();
+
+        // Mocks the binder call across the processes by using writeParcelable/readParcelable
+        // which only happens between processes. Code snippets are copied from
+        // VersionedParcelIntegTest#parcelCopy.
+        try {
+            final Parcel p = Parcel.obtain();
+            p.writeParcelable(ParcelUtils.toParcelable(testUriItem), 0);
+            p.setDataPosition(0);
+            final MediaItem2 testRemoteItem = ParcelUtils.fromParcelable(
+                    (ParcelImpl) p.readParcelable(MediaItem2.class.getClassLoader()));
+            fail("Write to parcel should fail for subclass of MediaItem2");
+        } catch (Exception e) {
+        }
+    }
+
+    private UriMediaItem2 createUriMediaItem2() {
+        final MediaMetadata2 testMetadata = new MediaMetadata2.Builder()
+                .putString("MediaItem2Test", "MediaItem2Test").build();
+        return new UriMediaItem2.Builder(mContext, Uri.parse("test://test"))
+                        .setMetadata(testMetadata)
+                        .setStartPosition(1)
+                        .setEndPosition(1000)
+                        .build();
+    }
+}
diff --git a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/MediaSession2ProviderService.java b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/MediaSession2ProviderService.java
index 1b3d89b..0d323a3 100644
--- a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/MediaSession2ProviderService.java
+++ b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/MediaSession2ProviderService.java
@@ -56,6 +56,7 @@
 import androidx.media2.MediaMetadata2;
 import androidx.media2.MediaSession2;
 import androidx.media2.MediaSession2.ControllerInfo;
+import androidx.media2.MediaUtils2;
 import androidx.media2.SessionCommand2;
 import androidx.media2.SessionCommandGroup2;
 import androidx.media2.SessionPlayer2;
@@ -176,7 +177,7 @@
         public ParcelImpl getToken(String sessionId) throws RemoteException {
             MediaSession2 session2 = mSession2Map.get(sessionId);
             return session2 != null
-                    ? (ParcelImpl) ParcelUtils.toParcelable(session2.getToken()) : null;
+                    ? MediaUtils2.toParcelable(session2.getToken()) : null;
         }
 
         @Override
@@ -216,8 +217,7 @@
                         config.getParcelableArrayList(KEY_PLAYLIST), false /* createItem */);
                 localPlayer.mCurrentMediaItem = MediaItem2.fromBundle(
                         config.getBundle(KEY_MEDIA_ITEM));
-                localPlayer.mMetadata = MediaMetadata2.fromBundle(
-                        config.getBundle(KEY_METADATA));
+                localPlayer.mMetadata = ParcelUtils.getVersionedParcelable(config, KEY_METADATA);
                 player = localPlayer;
             }
             player.setAudioAttributes(
@@ -393,8 +393,10 @@
 
             List<MediaItem2> list = new ArrayList<>();
             for (int i = 0; i < size; i++) {
-                list.add(new MediaItem2.Builder(0)
-                        .setMediaId(TestUtils.getMediaIdInDummyList(i))
+                list.add(new MediaItem2.Builder()
+                        .setMetadata(new MediaMetadata2.Builder()
+                                .putString(MediaMetadata2.METADATA_KEY_MEDIA_ID,
+                                        TestUtils.getMediaIdInDummyList(i)).build())
                         .build());
             }
             player.mPlaylist = list;
@@ -411,7 +413,6 @@
                 MediaItem2 item = MediaItem2.fromBundle(bundle);
                 list.add(new FileMediaItem2.Builder(new FileDescriptor())
                         .setMetadata(item.getMetadata())
-                        .setMediaId(item.getMediaId())
                         .build());
             }
             player.mPlaylist = list;
diff --git a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/MediaSessionCompatProviderService.java b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/MediaSessionCompatProviderService.java
index 09aaa9a..ba7cf78 100644
--- a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/MediaSessionCompatProviderService.java
+++ b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/MediaSessionCompatProviderService.java
@@ -35,6 +35,7 @@
 import android.support.v4.media.session.PlaybackStateCompat;
 import android.util.Log;
 
+import androidx.media.VolumeProviderCompat;
 import androidx.media.test.lib.TestUtils.SyncHandler;
 
 import java.util.HashMap;
@@ -119,6 +120,24 @@
         }
 
         @Override
+        public void setPlaybackToRemote(String sessionTag, int volumeControl, int maxVolume,
+                int currentVolume) throws RemoteException {
+            MediaSessionCompat session = mSessionMap.get(sessionTag);
+            session.setPlaybackToRemote(new VolumeProviderCompat(
+                    volumeControl, maxVolume, currentVolume) {
+                @Override
+                public void onSetVolumeTo(int volume) {
+                    setCurrentVolume(volume);
+                }
+
+                @Override
+                public void onAdjustVolume(int direction) {
+                    setCurrentVolume(getCurrentVolume() + direction);
+                }
+            });
+        }
+
+        @Override
         public void release(String sessionTag) throws RemoteException {
             MediaSessionCompat session = mSessionMap.get(sessionTag);
             session.release();
@@ -183,5 +202,12 @@
             MediaSessionCompat session = mSessionMap.get(sessionTag);
             session.setRatingType(type);
         }
+
+        @Override
+        public void sendSessionEvent(String sessionTag, String event, Bundle extras)
+                throws RemoteException {
+            MediaSessionCompat session = mSessionMap.get(sessionTag);
+            session.sendSessionEvent(event, extras);
+        }
     }
 }
diff --git a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/MediaTestUtils.java b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/MediaTestUtils.java
index b8af663..91f7776 100644
--- a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/MediaTestUtils.java
+++ b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/MediaTestUtils.java
@@ -79,15 +79,29 @@
         final List<MediaItem2> list = new ArrayList<>();
         String caller = Thread.currentThread().getStackTrace()[1].getMethodName();
         for (int i = 0; i < size; i++) {
-            list.add(new FileMediaItem2.Builder(new FileDescriptor())
-                    .setMediaId(caller + "_item_" + (size + 1))
-                    .build());
+            list.add(createMediaItem(caller + "_item_" + (size + 1)));
         }
         return list;
     }
 
     public static MediaItem2 createMediaItem(String id) {
-        return new FileMediaItem2.Builder(new FileDescriptor()).setMediaId(id).build();
+        return new FileMediaItem2.Builder(new FileDescriptor())
+                .setMetadata(new MediaMetadata2.Builder()
+                        .putString(MediaMetadata2.METADATA_KEY_MEDIA_ID, id)
+                        .putLong(MediaMetadata2.METADATA_KEY_BROWSABLE,
+                                MediaMetadata2.BROWSABLE_TYPE_NONE)
+                        .putLong(MediaMetadata2.METADATA_KEY_PLAYABLE, 1)
+                        .build())
+                .build();
+    }
+
+    public static List<String> createMediaIds(int size) {
+        final List<String> list = new ArrayList<>();
+        String caller = Thread.currentThread().getStackTrace()[1].getMethodName();
+        for (int i = 0; i < size; i++) {
+            list.add(caller + "_item_" + (size + 1));
+        }
+        return list;
     }
 
     /**
@@ -112,18 +126,10 @@
     public static MediaMetadata2 createMetadata() {
         String mediaId = Thread.currentThread().getStackTrace()[1].getMethodName();
         return new MediaMetadata2.Builder()
-                .putString(MediaMetadata2.METADATA_KEY_MEDIA_ID, mediaId).build();
-    }
-
-    public static List<Bundle> playlistToBundleList(List<MediaItem2> playlist) {
-        if (playlist == null) {
-            return null;
-        }
-        List<Bundle> result = new ArrayList<>();
-        for (int i = 0; i < playlist.size(); i++) {
-            result.add(playlist.get(i).toBundle());
-        }
-        return result;
+                .putString(MediaMetadata2.METADATA_KEY_MEDIA_ID, mediaId)
+                .putLong(MediaMetadata2.METADATA_KEY_BROWSABLE, MediaMetadata2.BROWSABLE_TYPE_NONE)
+                .putLong(MediaMetadata2.METADATA_KEY_PLAYABLE, 1)
+                .build();
     }
 
     public static List<MediaItem2> playlistFromParcelableList(List<Parcelable> parcelables,
@@ -138,7 +144,6 @@
                 MediaItem2 item = MediaItem2.fromBundle((Bundle) itemBundle);
                 result.add(new FileMediaItem2.Builder(new FileDescriptor())
                         .setMetadata(item.getMetadata())
-                        .setMediaId(item.getMediaId())
                         .build());
             }
         } else {
@@ -181,7 +186,7 @@
         return new LibraryParams.Builder().setExtras(extras).build();
     }
 
-    public static void assertLibraryParamsEquals(LibraryParams a, LibraryParams b) {
+    public static void assertEqualLibraryParams(LibraryParams a, LibraryParams b) {
         if (a == null || b == null) {
             assertEquals(a, b);
         } else {
@@ -189,14 +194,14 @@
         }
     }
 
-    public static void assertLibraryParamsWithBundle(LibraryParams a, Bundle b) {
-        if (a == null || b == null) {
-            assertEquals(a, b);
+    public static void assertEqualLibraryParams(LibraryParams params, Bundle rootExtras) {
+        if (params == null || rootExtras == null) {
+            assertEquals(params, rootExtras);
         } else {
-            assertEquals(a.isRecent(), b.getBoolean(BrowserRoot.EXTRA_RECENT));
-            assertEquals(a.isOffline(), b.getBoolean(BrowserRoot.EXTRA_OFFLINE));
-            assertEquals(a.isSuggested(), b.getBoolean(BrowserRoot.EXTRA_SUGGESTED));
-            assertTrue(TestUtils.contains(b, a.getExtras()));
+            assertEquals(params.isRecent(), rootExtras.getBoolean(BrowserRoot.EXTRA_RECENT));
+            assertEquals(params.isOffline(), rootExtras.getBoolean(BrowserRoot.EXTRA_OFFLINE));
+            assertEquals(params.isSuggested(), rootExtras.getBoolean(BrowserRoot.EXTRA_SUGGESTED));
+            assertTrue(TestUtils.contains(rootExtras, params.getExtras()));
         }
     }
 }
diff --git a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/MockMediaLibraryService2.java b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/MockMediaLibraryService2.java
index 35d37f0..8a226a8 100644
--- a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/MockMediaLibraryService2.java
+++ b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/MockMediaLibraryService2.java
@@ -46,11 +46,14 @@
         .SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ONE;
 import static androidx.media.test.lib.MediaBrowser2Constants
         .SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ONE_WITH_NON_SUBSCRIBED_ID;
-import static androidx.media.test.service.MediaTestUtils.assertLibraryParamsEquals;
+import static androidx.media.test.service.MediaTestUtils.assertEqualLibraryParams;
 import static androidx.media2.MediaLibraryService2.LibraryResult.RESULT_CODE_BAD_VALUE;
 import static androidx.media2.MediaLibraryService2.LibraryResult.RESULT_CODE_SUCCESS;
-import static androidx.media2.MediaMetadata2.FLAG_BROWSABLE;
+import static androidx.media2.MediaMetadata2.BROWSABLE_TYPE_MIXED;
+import static androidx.media2.MediaMetadata2.BROWSABLE_TYPE_NONE;
+import static androidx.media2.MediaMetadata2.METADATA_KEY_BROWSABLE;
 import static androidx.media2.MediaMetadata2.METADATA_KEY_MEDIA_ID;
+import static androidx.media2.MediaMetadata2.METADATA_KEY_PLAYABLE;
 
 import android.app.Service;
 import android.content.Context;
@@ -83,9 +86,11 @@
      * ID of the session that this service will create.
      */
     public static final String ID = "TestLibrary";
-    public static final MediaItem2 ROOT_ITEM = new MediaItem2.Builder(FLAG_BROWSABLE)
+    public static final MediaItem2 ROOT_ITEM = new MediaItem2.Builder()
             .setMetadata(new MediaMetadata2.Builder()
                     .putString(METADATA_KEY_MEDIA_ID, ROOT_ID)
+                    .putLong(METADATA_KEY_BROWSABLE, BROWSABLE_TYPE_MIXED)
+                    .putLong(METADATA_KEY_PLAYABLE, 0)
                     .build()).build();
     public static final LibraryParams ROOT_PARAMS = new LibraryParams.Builder()
             .setExtras(ROOT_EXTRAS).build();
@@ -210,10 +215,16 @@
                         getPaginatedResult(GET_CHILDREN_RESULT, page, pageSize), null);
             } else if (PARENT_ID_LONG_LIST.equals(parentId)) {
                 List<MediaItem2> list = new ArrayList<>(LONG_LIST_COUNT);
-                MediaItem2.Builder builder = new MediaItem2.Builder(0);
+                MediaItem2.Builder builder = new MediaItem2.Builder();
                 for (int i = 0; i < LONG_LIST_COUNT; i++) {
                     list.add(builder
-                            .setMediaId(TestUtils.getMediaIdInDummyList(i))
+                            .setMetadata(new MediaMetadata2.Builder()
+                                    .putString(MediaMetadata2.METADATA_KEY_MEDIA_ID,
+                                            TestUtils.getMediaIdInDummyList(i))
+                                    .putLong(MediaMetadata2.METADATA_KEY_BROWSABLE,
+                                            MediaMetadata2.BROWSABLE_TYPE_NONE)
+                                    .putLong(MediaMetadata2.METADATA_KEY_PLAYABLE, 1)
+                                    .build())
                             .build());
                 }
                 return new LibraryResult(RESULT_CODE_SUCCESS, list, null);
@@ -260,11 +271,9 @@
                         getPaginatedResult(SEARCH_RESULT, page, pageSize), null);
             } else if (SEARCH_QUERY_LONG_LIST.equals(query)) {
                 List<MediaItem2> list = new ArrayList<>(LONG_LIST_COUNT);
-                MediaItem2.Builder builder = new MediaItem2.Builder(0);
+                MediaItem2.Builder builder = new MediaItem2.Builder();
                 for (int i = 0; i < LONG_LIST_COUNT; i++) {
-                    list.add(builder
-                            .setMediaId(TestUtils.getMediaIdInDummyList(i))
-                            .build());
+                    list.add(createMediaItem(TestUtils.getMediaIdInDummyList(i)));
                 }
                 return new LibraryResult(RESULT_CODE_SUCCESS, list, null);
             } else if (SEARCH_QUERY_EMPTY_RESULT.equals(query)) {
@@ -330,7 +339,7 @@
         private void assertLibraryParams(LibraryParams params) {
             synchronized (MockMediaLibraryService2.class) {
                 if (sAssertLibraryParams) {
-                    assertLibraryParamsEquals(sExpectedParams, params);
+                    assertEqualLibraryParams(sExpectedParams, params);
                 }
             }
         }
@@ -367,9 +376,10 @@
     private MediaItem2 createMediaItem(String mediaId) {
         MediaMetadata2 metadata =  new MediaMetadata2.Builder()
                 .putString(MediaMetadata2.METADATA_KEY_MEDIA_ID, mediaId)
+                .putLong(MediaMetadata2.METADATA_KEY_BROWSABLE, BROWSABLE_TYPE_NONE)
+                .putLong(MediaMetadata2.METADATA_KEY_PLAYABLE, 1)
                 .build();
-        return new MediaItem2.Builder(MediaItem2.FLAG_PLAYABLE)
-                .setMediaId(mediaId)
+        return new MediaItem2.Builder()
                 .setMetadata(metadata)
                 .build();
     }
diff --git a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/RemoteMediaBrowser2.java b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/RemoteMediaBrowser2.java
index baf50de..a81a4d75 100644
--- a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/RemoteMediaBrowser2.java
+++ b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/RemoteMediaBrowser2.java
@@ -24,9 +24,8 @@
 import androidx.annotation.Nullable;
 import androidx.media2.MediaBrowser2;
 import androidx.media2.MediaLibraryService2.LibraryParams;
+import androidx.media2.MediaUtils2;
 import androidx.media2.SessionToken2;
-import androidx.versionedparcelable.ParcelImpl;
-import androidx.versionedparcelable.ParcelUtils;
 
 /**
  * Represents remote {@link MediaBrowser2} the client app's MediaController2Service.
@@ -51,8 +50,7 @@
 
     public void getLibraryRoot(@Nullable LibraryParams params) {
         try {
-            mBinder.getLibraryRoot(mControllerId,
-                    (ParcelImpl) ParcelUtils.toParcelable(params));
+            mBinder.getLibraryRoot(mControllerId, MediaUtils2.toParcelable(params));
         } catch (RemoteException ex) {
             Log.e(TAG, "Failed to call getLibraryRoot()");
         }
@@ -60,8 +58,7 @@
 
     public void subscribe(@NonNull String parentId, @Nullable LibraryParams params) {
         try {
-            mBinder.subscribe(mControllerId, parentId,
-                    (ParcelImpl) ParcelUtils.toParcelable(params));
+            mBinder.subscribe(mControllerId, parentId, MediaUtils2.toParcelable(params));
         } catch (RemoteException ex) {
             Log.e(TAG, "Failed to call subscribe()");
         }
@@ -79,7 +76,7 @@
             @Nullable LibraryParams params) {
         try {
             mBinder.getChildren(mControllerId, parentId, page, pageSize,
-                    (ParcelImpl) ParcelUtils.toParcelable(params));
+                    MediaUtils2.toParcelable(params));
         } catch (RemoteException ex) {
             Log.e(TAG, "Failed to call getChildren()");
         }
@@ -95,7 +92,7 @@
 
     public void search(@NonNull String query, @Nullable LibraryParams params) {
         try {
-            mBinder.search(mControllerId, query, (ParcelImpl) ParcelUtils.toParcelable(params));
+            mBinder.search(mControllerId, query, MediaUtils2.toParcelable(params));
         } catch (RemoteException ex) {
             Log.e(TAG, "Failed to call search()");
         }
@@ -105,7 +102,7 @@
             @Nullable LibraryParams params) {
         try {
             mBinder.getSearchResult(mControllerId, query, page, pageSize,
-                    (ParcelImpl) ParcelUtils.toParcelable(params));
+                    MediaUtils2.toParcelable(params));
         } catch (RemoteException ex) {
             Log.e(TAG, "Failed to call getSearchResult()");
         }
@@ -125,7 +122,7 @@
     void create(SessionToken2 token, boolean waitForConnection) {
         try {
             mBinder.create(true /* isBrowser */, mControllerId,
-                    (ParcelImpl) ParcelUtils.toParcelable(token), waitForConnection);
+                    MediaUtils2.toParcelable(token), waitForConnection);
         } catch (RemoteException ex) {
             Log.e(TAG, "Failed to create default browser with given token.");
         }
diff --git a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/RemoteMediaController2.java b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/RemoteMediaController2.java
index c64c83d..6781f63 100644
--- a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/RemoteMediaController2.java
+++ b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/RemoteMediaController2.java
@@ -37,13 +37,11 @@
 import androidx.annotation.Nullable;
 import androidx.media.test.lib.TestUtils;
 import androidx.media2.MediaController2;
-import androidx.media2.MediaItem2;
 import androidx.media2.MediaMetadata2;
+import androidx.media2.MediaUtils2;
 import androidx.media2.Rating2;
 import androidx.media2.SessionCommand2;
 import androidx.media2.SessionToken2;
-import androidx.versionedparcelable.ParcelImpl;
-import androidx.versionedparcelable.ParcelUtils;
 
 import java.util.List;
 import java.util.UUID;
@@ -93,7 +91,7 @@
 
     public SessionToken2 getConnectedSessionToken() {
         try {
-            return ParcelUtils.fromParcelable(mBinder.getConnectedSessionToken(mControllerId));
+            return MediaUtils2.fromParcelable(mBinder.getConnectedSessionToken(mControllerId));
         } catch (RemoteException ex) {
             Log.e(TAG, "Failed to call getConnectedSessionToken()");
             return null;
@@ -116,11 +114,11 @@
         }
     }
 
-    public void prefetch() {
+    public void prepare() {
         try {
-            mBinder.prefetch(mControllerId);
+            mBinder.prepare(mControllerId);
         } catch (RemoteException ex) {
-            Log.e(TAG, "Failed to call prefetch()");
+            Log.e(TAG, "Failed to call prepare()");
         }
     }
 
@@ -140,10 +138,9 @@
         }
     }
 
-    public void setPlaylist(@NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) {
+    public void setPlaylist(@NonNull List<String> list, @Nullable MediaMetadata2 metadata) {
         try {
-            mBinder.setPlaylist(mControllerId, MediaTestUtils.playlistToBundleList(list),
-                    metadata == null ? null : metadata.toBundle());
+            mBinder.setPlaylist(mControllerId, list, MediaUtils2.toParcelable(metadata));
         } catch (RemoteException ex) {
             Log.e(TAG, "Failed to call setPlaylist()");
         }
@@ -158,15 +155,15 @@
     public void createAndSetDummyPlaylist(int size, @Nullable MediaMetadata2 metadata) {
         try {
             mBinder.createAndSetDummyPlaylist(mControllerId, size,
-                    metadata == null ? null : metadata.toBundle());
+                    MediaUtils2.toParcelable(metadata));
         } catch (RemoteException ex) {
             Log.e(TAG, "Failed to call createAndSetDummyPlaylist()");
         }
     }
 
-    public void setMediaItem(@NonNull MediaItem2 item) {
+    public void setMediaItem(@NonNull String mediaId) {
         try {
-            mBinder.setMediaItem(mControllerId, item.toBundle());
+            mBinder.setMediaItem(mControllerId, mediaId);
         } catch (RemoteException ex) {
             Log.e(TAG, "Failed to call setMediaItem()");
         }
@@ -174,32 +171,31 @@
 
     public void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata) {
         try {
-            mBinder.updatePlaylistMetadata(mControllerId,
-                    metadata == null ? null : metadata.toBundle());
+            mBinder.updatePlaylistMetadata(mControllerId, MediaUtils2.toParcelable(metadata));
         } catch (RemoteException ex) {
             Log.e(TAG, "Failed to call updatePlaylistMetadata()");
         }
     }
 
-    public void addPlaylistItem(int index, @NonNull MediaItem2 item) {
+    public void addPlaylistItem(int index, @NonNull String mediaId) {
         try {
-            mBinder.addPlaylistItem(mControllerId, index, item.toBundle());
+            mBinder.addPlaylistItem(mControllerId, index, mediaId);
         } catch (RemoteException ex) {
             Log.e(TAG, "Failed to call addPlaylistItem()");
         }
     }
 
-    public void removePlaylistItem(@NonNull MediaItem2 item) {
+    public void removePlaylistItem(int index) {
         try {
-            mBinder.removePlaylistItem(mControllerId, item.toBundle());
+            mBinder.removePlaylistItem(mControllerId, index);
         } catch (RemoteException ex) {
             Log.e(TAG, "Failed to call removePlaylistItem()");
         }
     }
 
-    public void replacePlaylistItem(int index, @NonNull MediaItem2 item) {
+    public void replacePlaylistItem(int index, @NonNull String media) {
         try {
-            mBinder.replacePlaylistItem(mControllerId, index, item.toBundle());
+            mBinder.replacePlaylistItem(mControllerId, index, media);
         } catch (RemoteException ex) {
             Log.e(TAG, "Failed to call replacePlaylistItem()");
         }
@@ -221,9 +217,9 @@
         }
     }
 
-    public void skipToPlaylistItem(@NonNull MediaItem2 item) {
+    public void skipToPlaylistItem(int index) {
         try {
-            mBinder.skipToPlaylistItem(mControllerId, item.toBundle());
+            mBinder.skipToPlaylistItem(mControllerId, index);
         } catch (RemoteException ex) {
             Log.e(TAG, "Failed to call skipToPlaylistItem()");
         }
@@ -285,6 +281,22 @@
         }
     }
 
+    public void skipForward() {
+        try {
+            mBinder.skipForward(mControllerId);
+        } catch (RemoteException ex) {
+            Log.e(TAG, "Failed to call skipForward()");
+        }
+    }
+
+    public void skipBackward() {
+        try {
+            mBinder.skipBackward(mControllerId);
+        } catch (RemoteException ex) {
+            Log.e(TAG, "Failed to call skipBackward()");
+        }
+    }
+
     public void playFromMediaId(@NonNull String mediaId, @Nullable Bundle extras) {
         try {
             mBinder.playFromMediaId(mControllerId, mediaId, extras);
@@ -309,34 +321,33 @@
         }
     }
 
-    public void prefetchFromMediaId(@NonNull String mediaId, @Nullable Bundle extras) {
+    public void prepareFromMediaId(@NonNull String mediaId, @Nullable Bundle extras) {
         try {
-            mBinder.prefetchFromMediaId(mControllerId, mediaId, extras);
+            mBinder.prepareFromMediaId(mControllerId, mediaId, extras);
         } catch (RemoteException ex) {
-            Log.e(TAG, "Failed to call prefetchFromMediaId()");
+            Log.e(TAG, "Failed to call prepareFromMediaId()");
         }
     }
 
-    public void prefetchFromSearch(@NonNull String query, @Nullable Bundle extras) {
+    public void prepareFromSearch(@NonNull String query, @Nullable Bundle extras) {
         try {
-            mBinder.prefetchFromSearch(mControllerId, query, extras);
+            mBinder.prepareFromSearch(mControllerId, query, extras);
         } catch (RemoteException ex) {
-            Log.e(TAG, "Failed to call prefetchFromSearch()");
+            Log.e(TAG, "Failed to call prepareFromSearch()");
         }
     }
 
-    public void prefetchFromUri(@NonNull Uri uri, @Nullable Bundle extras) {
+    public void prepareFromUri(@NonNull Uri uri, @Nullable Bundle extras) {
         try {
-            mBinder.prefetchFromUri(mControllerId, uri, extras);
+            mBinder.prepareFromUri(mControllerId, uri, extras);
         } catch (RemoteException ex) {
-            Log.e(TAG, "Failed to call prefetchFromUri()");
+            Log.e(TAG, "Failed to call prepareFromUri()");
         }
     }
 
     public void setRating(@NonNull String mediaId, @NonNull Rating2 rating) {
         try {
-            mBinder.setRating(mControllerId, mediaId,
-                    (ParcelImpl) ParcelUtils.toParcelable(rating));
+            mBinder.setRating(mControllerId, mediaId, MediaUtils2.toParcelable(rating));
         } catch (RemoteException ex) {
             Log.e(TAG, "Failed to call setRating()");
         }
@@ -425,7 +436,7 @@
     void create(SessionToken2 token, boolean waitForConnection) {
         try {
             mBinder.create(false /* isBrowser */, mControllerId,
-                    (ParcelImpl) ParcelUtils.toParcelable(token), waitForConnection);
+                    MediaUtils2.toParcelable(token), waitForConnection);
         } catch (RemoteException ex) {
             Log.e(TAG, "Failed to create default controller with given token.");
         }
diff --git a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/RemoteMediaControllerCompat.java b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/RemoteMediaControllerCompat.java
index ceb683b..4b59b66 100644
--- a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/RemoteMediaControllerCompat.java
+++ b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/RemoteMediaControllerCompat.java
@@ -155,7 +155,7 @@
             try {
                 mBinder.prepare(mControllerId);
             } catch (RemoteException ex) {
-                Log.e(TAG, "Failed to call prefetch()");
+                Log.e(TAG, "Failed to call prepare()");
             }
         }
 
@@ -163,7 +163,7 @@
             try {
                 mBinder.prepareFromMediaId(mControllerId, mediaId, extras);
             } catch (RemoteException ex) {
-                Log.e(TAG, "Failed to call prefetchFromMediaId()");
+                Log.e(TAG, "Failed to call prepareFromMediaId()");
             }
         }
 
@@ -171,7 +171,7 @@
             try {
                 mBinder.prepareFromSearch(mControllerId, query, extras);
             } catch (RemoteException ex) {
-                Log.e(TAG, "Failed to call prefetchFromSearch()");
+                Log.e(TAG, "Failed to call prepareFromSearch()");
             }
         }
 
@@ -179,7 +179,7 @@
             try {
                 mBinder.prepareFromUri(mControllerId, uri, extras);
             } catch (RemoteException ex) {
-                Log.e(TAG, "Failed to call prefetchFromUri()");
+                Log.e(TAG, "Failed to call prepareFromUri()");
             }
         }
 
diff --git a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/TestServiceRegistry.java b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/TestServiceRegistry.java
index 3cfdde4..55953f0 100644
--- a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/TestServiceRegistry.java
+++ b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/TestServiceRegistry.java
@@ -19,6 +19,7 @@
 import static org.junit.Assert.fail;
 
 import android.os.Handler;
+import android.util.Log;
 
 import androidx.annotation.GuardedBy;
 import androidx.media.test.lib.TestUtils.SyncHandler;
@@ -35,6 +36,9 @@
  * It only support only one service at a time.
  */
 public class TestServiceRegistry {
+    private static final String TAG = "TestServiceRegistry";
+    private static final boolean DEBUG = true;
+
     @GuardedBy("TestServiceRegistry.class")
     private static TestServiceRegistry sInstance;
     @GuardedBy("TestServiceRegistry.class")
@@ -113,6 +117,18 @@
                 fail("Previous service instance is still running. Clean up manually to ensure"
                         + " previously running service doesn't break current test");
             }
+            if (DEBUG) {
+                Log.d(TAG, "setServiceInstance(): service=" + service);
+                if (service != null) {
+                    Log.d(TAG, "setServiceInstance(): service=" + service + ", session size="
+                            + service.getSessions().size());
+                    for (MediaSession2 session : service.getSessions()) {
+                        Log.d(TAG, "   session id=" + session.getId());
+                    }
+                } else {
+                    Log.d(TAG, "setServiceInstance, service=" + service);
+                }
+            }
             mService = service;
             if (mSessionServiceCallback != null) {
                 mSessionServiceCallback.onCreated();
@@ -129,6 +145,13 @@
     public void cleanUp() {
         synchronized (TestServiceRegistry.class) {
             if (mService != null) {
+                if (DEBUG) {
+                    Log.d(TAG, "cleanUp(): service=" + mService + ", session size="
+                            + mService.getSessions().size());
+                    for (MediaSession2 session : mService.getSessions()) {
+                        Log.d(TAG, "   session id=" + session.getId());
+                    }
+                }
                 // TODO(jaewan): Remove this, and override SessionService#onDestroy() to do this
                 List<MediaSession2> sessions = mService.getSessions();
                 for (int i = 0; i < sessions.size(); i++) {
@@ -139,6 +162,8 @@
                 // So stopSelf() isn't really needed, but just for sure.
                 mService.stopSelf();
                 mService = null;
+            } else if (DEBUG) {
+                Log.d(TAG, "cleanUp(): service=" + mService);
             }
             if (mHandler != null) {
                 mHandler.removeCallbacksAndMessages(null);
diff --git a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/MediaBrowserServiceCompatCallbackTestWithMediaBrowser2.java b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/MediaBrowserServiceCompatCallbackTestWithMediaBrowser2.java
index d0669db..acee00e 100644
--- a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/MediaBrowserServiceCompatCallbackTestWithMediaBrowser2.java
+++ b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/MediaBrowserServiceCompatCallbackTestWithMediaBrowser2.java
@@ -18,7 +18,7 @@
 
 import static android.support.mediacompat.testlib.util.IntentUtil.CLIENT_PACKAGE_NAME;
 
-import static androidx.media.test.service.MediaTestUtils.assertLibraryParamsWithBundle;
+import static androidx.media.test.service.MediaTestUtils.assertEqualLibraryParams;
 import static androidx.media.test.service.MediaTestUtils.createLibraryParams;
 
 import static org.junit.Assert.assertEquals;
@@ -88,7 +88,7 @@
                     Bundle rootHints) {
                 assertEquals(CLIENT_PACKAGE_NAME, clientPackageName);
                 if (rootHints.keySet().contains(testMediaId)) {
-                    assertLibraryParamsWithBundle(testParams, rootHints);
+                    assertEqualLibraryParams(testParams, rootHints);
                     // This should happen because getLibraryRoot() is called with testExtras.
                     latch.countDown();
                 }
@@ -185,7 +185,7 @@
             public void onLoadChildren(String parentId, Result<List<MediaItem>> result,
                     Bundle option) {
                 assertEquals(testParentId, parentId);
-                assertLibraryParamsWithBundle(testParams, option);
+                assertEqualLibraryParams(testParams, option);
                 result.sendResult(null);
                 subscribeLatch.countDown();
             }
@@ -210,7 +210,7 @@
             @Override
             public void onSearch(String query, Bundle extras, Result<List<MediaItem>> result) {
                 assertEquals(testQuery, query);
-                assertLibraryParamsWithBundle(testParams, extras);
+                assertEqualLibraryParams(testParams, extras);
                 result.sendResult(testFullSearchResult);
                 latch.countDown();
             }
@@ -234,7 +234,7 @@
             @Override
             public void onSearch(String query, Bundle extras, Result<List<MediaItem>> result) {
                 assertEquals(testQuery, query);
-                assertLibraryParamsWithBundle(testParams, extras);
+                assertEqualLibraryParams(testParams, extras);
                 assertEquals(testPage, extras.getInt(MediaBrowserCompat.EXTRA_PAGE));
                 assertEquals(testPageSize, extras.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE));
                 result.sendResult(null);
diff --git a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/MediaLibrarySessionCallbackTest.java b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/MediaLibrarySessionCallbackTest.java
index 92cfd89..fab1480 100644
--- a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/MediaLibrarySessionCallbackTest.java
+++ b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/MediaLibrarySessionCallbackTest.java
@@ -16,7 +16,7 @@
 
 package androidx.media.test.service.tests;
 
-import static androidx.media.test.service.MediaTestUtils.assertLibraryParamsEquals;
+import static androidx.media.test.service.MediaTestUtils.assertEqualLibraryParams;
 import static androidx.media2.MediaLibraryService2.LibraryResult.RESULT_CODE_SUCCESS;
 
 import static org.junit.Assert.assertEquals;
@@ -55,7 +55,6 @@
 public class MediaLibrarySessionCallbackTest extends MediaSession2TestBase {
 
     MockPlayer mPlayer;
-    RemoteMediaBrowser2 mBrowser2;
 
     @Before
     @Override
@@ -84,7 +83,7 @@
                             MediaSession2.ControllerInfo controller, String parentId,
                             LibraryParams params) {
                         assertEquals(testParentId, parentId);
-                        assertLibraryParamsEquals(testParams, params);
+                        assertEqualLibraryParams(testParams, params);
                         latch.countDown();
                         return RESULT_CODE_SUCCESS;
                     }
@@ -97,8 +96,8 @@
                 service, mPlayer, sHandlerExecutor, sessionCallback)
                 .setId("testOnSubscribe")
                 .build()) {
-            mBrowser2 = createRemoteBrowser2(session.getToken());
-            mBrowser2.subscribe(testParentId, testParams);
+            RemoteMediaBrowser2 browser = createRemoteBrowser2(session.getToken());
+            browser.subscribe(testParentId, testParams);
             assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
         }
     }
@@ -127,8 +126,8 @@
                 service, mPlayer, sHandlerExecutor, sessionCallback)
                 .setId("testOnUnsubscribe")
                 .build()) {
-            mBrowser2 = createRemoteBrowser2(session.getToken());
-            mBrowser2.unsubscribe(testParentId);
+            RemoteMediaBrowser2 browser = createRemoteBrowser2(session.getToken());
+            browser.unsubscribe(testParentId);
             assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
         }
     }
diff --git a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/MediaMetadata2Test.java b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/MediaMetadata2Test.java
index 77458d6..0db3899 100644
--- a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/MediaMetadata2Test.java
+++ b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/MediaMetadata2Test.java
@@ -19,6 +19,8 @@
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertTrue;
 
+import static org.junit.Assert.fail;
+
 import android.os.Build;
 import android.os.Bundle;
 
@@ -40,22 +42,35 @@
 public class MediaMetadata2Test {
     @Test
     public void testBuilder() {
-        final Bundle extras = new Bundle();
-        extras.putString("MediaMetadata2Test", "testBuilder");
         final String title = "title";
         final long discNumber = 10;
         final Rating2 rating = new ThumbRating2(true);
 
         Builder builder = new Builder();
-        builder.setExtras(extras);
         builder.putString(MediaMetadata2.METADATA_KEY_DISPLAY_TITLE, title);
         builder.putLong(MediaMetadata2.METADATA_KEY_DISC_NUMBER, discNumber);
         builder.putRating(MediaMetadata2.METADATA_KEY_USER_RATING, rating);
 
         MediaMetadata2 metadata = builder.build();
-        assertTrue(TestUtils.equals(extras, metadata.getExtras()));
         assertEquals(title, metadata.getString(MediaMetadata2.METADATA_KEY_DISPLAY_TITLE));
         assertEquals(discNumber, metadata.getLong(MediaMetadata2.METADATA_KEY_DISC_NUMBER));
         assertEquals(rating, metadata.getRating(MediaMetadata2.METADATA_KEY_USER_RATING));
     }
+
+    @Test
+    public void testSetExtra() {
+        final Bundle extras = new Bundle();
+        extras.putString("MediaMetadata2Test", "testBuilder");
+
+        Builder builder = new Builder();
+        try {
+            builder.putLong(MediaMetadata2.METADATA_KEY_EXTRAS, 1);
+            fail();
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+        builder.setExtras(extras);
+        MediaMetadata2 metadata = builder.build();
+        assertTrue(TestUtils.equals(extras, metadata.getExtras()));
+    }
 }
diff --git a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/MediaSession2CallbackTest.java b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/MediaSession2CallbackTest.java
index 57bc51c..c5437b7 100644
--- a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/MediaSession2CallbackTest.java
+++ b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/MediaSession2CallbackTest.java
@@ -36,6 +36,7 @@
 import androidx.media.test.service.MockPlayer;
 import androidx.media.test.service.RemoteMediaController2;
 import androidx.media2.MediaItem2;
+import androidx.media2.MediaMetadata2;
 import androidx.media2.MediaSession2;
 import androidx.media2.MediaSession2.ControllerInfo;
 import androidx.media2.Rating2;
@@ -67,7 +68,6 @@
     private static final String TAG = "MediaSession2CallbackTest";
 
     MockPlayer mPlayer;
-    RemoteMediaController2 mController2;
 
     @Before
     @Override
@@ -92,16 +92,16 @@
                 .setSessionCallback(sHandlerExecutor, callback)
                 .setId("testOnCommandRequest")
                 .build()) {
-            mController2 = createRemoteController2(session.getToken());
+            RemoteMediaController2 controller = createRemoteController2(session.getToken());
 
-            mController2.pause();
+            controller.pause();
             assertFalse(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
             assertFalse(mPlayer.mPauseCalled);
             assertEquals(1, callback.commands.size());
             assertEquals(SessionCommand2.COMMAND_CODE_PLAYER_PAUSE,
                     (long) callback.commands.get(0).getCommandCode());
 
-            mController2.play();
+            controller.play();
             assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
             assertTrue(mPlayer.mPlayCalled);
             assertFalse(mPlayer.mPauseCalled);
@@ -117,15 +117,15 @@
         prepareLooper();
         mPlayer = new MockPlayer(1);
 
-        final List<MediaItem2> list = MediaTestUtils.createPlaylist(3);
+        final List<String> list = MediaTestUtils.createMediaIds(3);
         final List<MediaItem2> convertedList = MediaTestUtils.createPlaylist(list.size());
 
         final MockOnCommandCallback callback = new MockOnCommandCallback() {
             @Override
             public MediaItem2 onCreateMediaItem(MediaSession2 session,
-                    ControllerInfo controller, MediaItem2 item) {
+                    ControllerInfo controller, String mediaId) {
                 for (int i = 0; i < list.size(); i++) {
-                    if (Objects.equals(item, list.get(i))) {
+                    if (Objects.equals(mediaId, list.get(i))) {
                         return convertedList.get(i);
                     }
                 }
@@ -137,15 +137,19 @@
                 .setSessionCallback(sHandlerExecutor, callback)
                 .setId("testOnCreateMediaItem")
                 .build()) {
-            mController2 = createRemoteController2(session.getToken());
+            RemoteMediaController2 controller = createRemoteController2(session.getToken());
 
-            mController2.setPlaylist(list, null);
+            controller.setPlaylist(list, null);
             assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
 
             List<MediaItem2> playerList = mPlayer.getPlaylist();
             assertEquals(convertedList.size(), playerList.size());
             for (int i = 0; i < playerList.size(); i++) {
-                assertEquals(convertedList.get(i), playerList.get(i));
+                String expected = convertedList.get(i).getMetadata().getString(
+                        MediaMetadata2.METADATA_KEY_MEDIA_ID);
+                String actual = playerList.get(i).getMetadata().getString(
+                        MediaMetadata2.METADATA_KEY_MEDIA_ID);
+                assertEquals(expected, actual);
             }
         }
     }
@@ -186,8 +190,8 @@
                 .setSessionCallback(sHandlerExecutor, callback)
                 .setId("testOnCustomCommand")
                 .build()) {
-            mController2 = createRemoteController2(session.getToken());
-            mController2.sendCustomCommand(testCommand, testArgs);
+            RemoteMediaController2 controller = createRemoteController2(session.getToken());
+            controller.sendCustomCommand(testCommand, testArgs);
             assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
         }
     }
@@ -207,8 +211,8 @@
         try (MediaSession2 session = new MediaSession2.Builder(mContext, mPlayer)
                 .setSessionCallback(sHandlerExecutor, callback)
                 .setId("testOnFastForward").build()) {
-            mController2 = createRemoteController2(session.getToken());
-            mController2.fastForward();
+            RemoteMediaController2 controller = createRemoteController2(session.getToken());
+            controller.fastForward();
             assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
         }
     }
@@ -228,8 +232,50 @@
         try (MediaSession2 session = new MediaSession2.Builder(mContext, mPlayer)
                 .setSessionCallback(sHandlerExecutor, callback)
                 .setId("testOnRewind").build()) {
-            mController2 = createRemoteController2(session.getToken());
-            mController2.rewind();
+            RemoteMediaController2 controller = createRemoteController2(session.getToken());
+            controller.rewind();
+            assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        }
+    }
+
+    @Test
+    public void testOnSkipForward() throws InterruptedException {
+        prepareLooper();
+        final CountDownLatch latch = new CountDownLatch(1);
+        final MediaSession2.SessionCallback callback = new MediaSession2.SessionCallback() {
+            @Override
+            public int onSkipForward(MediaSession2 session, ControllerInfo controller) {
+                assertEquals(CLIENT_PACKAGE_NAME, controller.getPackageName());
+                latch.countDown();
+                return RESULT_CODE_SUCCESS;
+            }
+        };
+        try (MediaSession2 session = new MediaSession2.Builder(mContext, mPlayer)
+                .setSessionCallback(sHandlerExecutor, callback)
+                .setId("testOnSkipForward").build()) {
+            RemoteMediaController2 controller = createRemoteController2(session.getToken());
+            controller.skipForward();
+            assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        }
+    }
+
+    @Test
+    public void testOnSkipBackward() throws InterruptedException {
+        prepareLooper();
+        final CountDownLatch latch = new CountDownLatch(1);
+        final MediaSession2.SessionCallback callback = new MediaSession2.SessionCallback() {
+            @Override
+            public int onSkipBackward(MediaSession2 session, ControllerInfo controller) {
+                assertEquals(CLIENT_PACKAGE_NAME, controller.getPackageName());
+                latch.countDown();
+                return RESULT_CODE_SUCCESS;
+            }
+        };
+        try (MediaSession2 session = new MediaSession2.Builder(mContext, mPlayer)
+                .setSessionCallback(sHandlerExecutor, callback)
+                .setId("testOnSkipBackward").build()) {
+            RemoteMediaController2 controller = createRemoteController2(session.getToken());
+            controller.skipBackward();
             assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
         }
     }
@@ -254,9 +300,9 @@
         try (MediaSession2 session = new MediaSession2.Builder(mContext, mPlayer)
                 .setSessionCallback(sHandlerExecutor, callback)
                 .setId("testOnPlayFromSearch").build()) {
-            mController2 = createRemoteController2(session.getToken());
+            RemoteMediaController2 controller = createRemoteController2(session.getToken());
 
-            mController2.playFromSearch(testQuery, testExtras);
+            controller.playFromSearch(testQuery, testExtras);
             assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
         }
     }
@@ -282,9 +328,9 @@
                 .setSessionCallback(sHandlerExecutor, callback)
                 .setId("testOnPlayFromUri")
                 .build()) {
-            mController2 = createRemoteController2(session.getToken());
+            RemoteMediaController2 controller = createRemoteController2(session.getToken());
 
-            mController2.playFromUri(testUri, testExtras);
+            controller.playFromUri(testUri, testExtras);
             assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
         }
     }
@@ -309,9 +355,9 @@
         try (MediaSession2 session = new MediaSession2.Builder(mContext, mPlayer)
                 .setSessionCallback(sHandlerExecutor, callback)
                 .setId("testOnPlayFromMediaId").build()) {
-            mController2 = createRemoteController2(session.getToken());
+            RemoteMediaController2 controller = createRemoteController2(session.getToken());
 
-            mController2.playFromMediaId(testMediaId, testExtras);
+            controller.playFromMediaId(testMediaId, testExtras);
             assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
         }
     }
@@ -324,7 +370,7 @@
         final CountDownLatch latch = new CountDownLatch(1);
         final MediaSession2.SessionCallback callback = new MediaSession2.SessionCallback() {
             @Override
-            public int onPrefetchFromSearch(MediaSession2 session, ControllerInfo controller,
+            public int onPrepareFromSearch(MediaSession2 session, ControllerInfo controller,
                     String query, Bundle extras) {
                 assertEquals(CLIENT_PACKAGE_NAME, controller.getPackageName());
                 assertEquals(testQuery, query);
@@ -336,9 +382,9 @@
         try (MediaSession2 session = new MediaSession2.Builder(mContext, mPlayer)
                 .setSessionCallback(sHandlerExecutor, callback)
                 .setId("testOnPrepareFromSearch").build()) {
-            mController2 = createRemoteController2(session.getToken());
+            RemoteMediaController2 controller = createRemoteController2(session.getToken());
 
-            mController2.prefetchFromSearch(testQuery, testExtras);
+            controller.prepareFromSearch(testQuery, testExtras);
             assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
         }
     }
@@ -351,7 +397,7 @@
         final CountDownLatch latch = new CountDownLatch(1);
         final MediaSession2.SessionCallback callback = new MediaSession2.SessionCallback() {
             @Override
-            public int onPrefetchFromUri(MediaSession2 session, ControllerInfo controller, Uri uri,
+            public int onPrepareFromUri(MediaSession2 session, ControllerInfo controller, Uri uri,
                     Bundle extras) {
                 assertEquals(CLIENT_PACKAGE_NAME, controller.getPackageName());
                 assertEquals(testUri, uri);
@@ -363,9 +409,9 @@
         try (MediaSession2 session = new MediaSession2.Builder(mContext, mPlayer)
                 .setSessionCallback(sHandlerExecutor, callback)
                 .setId("testOnPrepareFromUri").build()) {
-            mController2 = createRemoteController2(session.getToken());
+            RemoteMediaController2 controller = createRemoteController2(session.getToken());
 
-            mController2.prefetchFromUri(testUri, testExtras);
+            controller.prepareFromUri(testUri, testExtras);
             assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
         }
     }
@@ -378,7 +424,7 @@
         final CountDownLatch latch = new CountDownLatch(1);
         final MediaSession2.SessionCallback callback = new MediaSession2.SessionCallback() {
             @Override
-            public int onPrefetchFromMediaId(MediaSession2 session, ControllerInfo controller,
+            public int onPrepareFromMediaId(MediaSession2 session, ControllerInfo controller,
                     String mediaId, Bundle extras) {
                 assertEquals(CLIENT_PACKAGE_NAME, controller.getPackageName());
                 assertEquals(testMediaId, mediaId);
@@ -390,9 +436,9 @@
         try (MediaSession2 session = new MediaSession2.Builder(mContext, mPlayer)
                 .setSessionCallback(sHandlerExecutor, callback)
                 .setId("testOnPrepareFromMediaId").build()) {
-            mController2 = createRemoteController2(session.getToken());
+            RemoteMediaController2 controller = createRemoteController2(session.getToken());
 
-            mController2.prefetchFromMediaId(testMediaId, testExtras);
+            controller.prepareFromMediaId(testMediaId, testExtras);
             assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
         }
     }
@@ -420,9 +466,9 @@
         try (MediaSession2 session = new MediaSession2.Builder(mContext, mPlayer)
                 .setSessionCallback(sHandlerExecutor, callback)
                 .setId("testOnSetRating").build()) {
-            mController2 = createRemoteController2(session.getToken());
+            RemoteMediaController2 controller = createRemoteController2(session.getToken());
 
-            mController2.setRating(testMediaId, testRating);
+            controller.setRating(testMediaId, testRating);
             assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
         }
     }
@@ -443,10 +489,10 @@
                 .setSessionCallback(sHandlerExecutor, callback)
                 .setId("testOnSubscribeRoutesInfo")
                 .build()) {
-            mController2 = createRemoteController2(session.getToken());
+            RemoteMediaController2 controller = createRemoteController2(session.getToken());
 
             callback.resetLatchCount(1);
-            mController2.subscribeRoutesInfo();
+            controller.subscribeRoutesInfo();
             assertTrue(callback.mLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
         }
     }
@@ -467,10 +513,10 @@
                 .setSessionCallback(sHandlerExecutor, callback)
                 .setId("testOnUnsubscribeRoutesInfo")
                 .build()) {
-            mController2 = createRemoteController2(session.getToken());
+            RemoteMediaController2 controller = createRemoteController2(session.getToken());
 
             callback.resetLatchCount(1);
-            mController2.unsubscribeRoutesInfo();
+            controller.unsubscribeRoutesInfo();
             assertTrue(callback.mLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
         }
     }
@@ -493,10 +539,10 @@
                 .setSessionCallback(sHandlerExecutor, callback)
                 .setId("testOnSelectRoute")
                 .build()) {
-            mController2 = createRemoteController2(session.getToken());
+            RemoteMediaController2 controller = createRemoteController2(session.getToken());
 
             callback.resetLatchCount(1);
-            mController2.selectRoute(testRoute);
+            controller.selectRoute(testRoute);
             assertTrue(callback.mLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
         }
     }
@@ -519,7 +565,7 @@
                         return super.onConnect(session, controller);
                     }
                 }).build()) {
-            mController2 = createRemoteController2(
+            RemoteMediaController2 controller = createRemoteController2(
                     session.getToken(), false /* waitForConnection */);
             assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
         }
@@ -540,8 +586,8 @@
                         latch.countDown();
                     }
                 }).build()) {
-            mController2 = createRemoteController2(session.getToken());
-            mController2.close();
+            RemoteMediaController2 controller = createRemoteController2(session.getToken());
+            controller.close();
             assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
         }
     }
diff --git a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/MediaSession2LegacyCallbackTest.java b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/MediaSession2LegacyCallbackTest.java
index b619dfe..bc56b7d 100644
--- a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/MediaSession2LegacyCallbackTest.java
+++ b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/MediaSession2LegacyCallbackTest.java
@@ -113,6 +113,12 @@
                         }
                         return null;
                     }
+
+                    @Override
+                    public MediaItem2 onCreateMediaItem(MediaSession2 session,
+                            ControllerInfo controller, String mediaId) {
+                        return MediaTestUtils.createMediaItem(mediaId);
+                    }
                 })
                 .setSessionActivity(mIntent)
                 .setId(TAG).build();
@@ -128,6 +134,10 @@
         if (mSession != null) {
             mSession.close();
         }
+        if (mController != null) {
+            mController.cleanUp();
+            mController = null;
+        }
     }
 
     @Test
@@ -650,7 +660,7 @@
         final CountDownLatch latch = new CountDownLatch(1);
         final SessionCallback callback = new SessionCallback() {
             @Override
-            public int onPrefetchFromSearch(MediaSession2 session, ControllerInfo controller,
+            public int onPrepareFromSearch(MediaSession2 session, ControllerInfo controller,
                     String query, Bundle extras) {
                 assertEquals(EXPECTED_CONTROLLER_PACKAGE_NAME, controller.getPackageName());
                 assertEquals(request, query);
@@ -678,7 +688,7 @@
         final CountDownLatch latch = new CountDownLatch(1);
         final SessionCallback callback = new SessionCallback() {
             @Override
-            public int onPrefetchFromUri(MediaSession2 session, ControllerInfo controller, Uri uri,
+            public int onPrepareFromUri(MediaSession2 session, ControllerInfo controller, Uri uri,
                     Bundle extras) {
                 assertEquals(EXPECTED_CONTROLLER_PACKAGE_NAME, controller.getPackageName());
                 assertEquals(request, uri);
@@ -706,7 +716,7 @@
         final CountDownLatch latch = new CountDownLatch(1);
         final SessionCallback callback = new SessionCallback() {
             @Override
-            public int onPrefetchFromMediaId(MediaSession2 session, ControllerInfo controller,
+            public int onPrepareFromMediaId(MediaSession2 session, ControllerInfo controller,
                     String mediaId, Bundle extras) {
                 assertEquals(EXPECTED_CONTROLLER_PACKAGE_NAME, controller.getPackageName());
                 assertEquals(request, mediaId);
diff --git a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/MediaSession2_PermissionTest.java b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/MediaSession2_PermissionTest.java
index 252236d..d361b6a 100644
--- a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/MediaSession2_PermissionTest.java
+++ b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/MediaSession2_PermissionTest.java
@@ -35,11 +35,13 @@
 import static androidx.media2.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID;
 import static androidx.media2.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_SEARCH;
 import static androidx.media2.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_URI;
-import static androidx.media2.SessionCommand2.COMMAND_CODE_SESSION_PREFETCH_FROM_MEDIA_ID;
-import static androidx.media2.SessionCommand2.COMMAND_CODE_SESSION_PREFETCH_FROM_SEARCH;
-import static androidx.media2.SessionCommand2.COMMAND_CODE_SESSION_PREFETCH_FROM_URI;
+import static androidx.media2.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID;
+import static androidx.media2.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH;
+import static androidx.media2.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_URI;
 import static androidx.media2.SessionCommand2.COMMAND_CODE_SESSION_REWIND;
 import static androidx.media2.SessionCommand2.COMMAND_CODE_SESSION_SET_RATING;
+import static androidx.media2.SessionCommand2.COMMAND_CODE_SESSION_SKIP_BACKWARD;
+import static androidx.media2.SessionCommand2.COMMAND_CODE_SESSION_SKIP_FORWARD;
 import static androidx.media2.SessionCommand2.COMMAND_CODE_VOLUME_ADJUST_VOLUME;
 import static androidx.media2.SessionCommand2.COMMAND_CODE_VOLUME_SET_VOLUME;
 
@@ -222,13 +224,12 @@
     @Test
     public void testSkipToPlaylistItem() throws InterruptedException {
         prepareLooper();
-        final MediaItem2 testItem = MediaTestUtils.createMediaItemWithMetadata();
         testOnCommandRequest(
                 COMMAND_CODE_PLAYER_SKIP_TO_PLAYLIST_ITEM,
                 new PermissionTestTask() {
                     @Override
                     public void run(RemoteMediaController2 controller) {
-                        controller.skipToPlaylistItem(testItem);
+                        controller.skipToPlaylistItem(0);
                     }
                 });
     }
@@ -236,7 +237,7 @@
     @Test
     public void testSetPlaylist() throws InterruptedException {
         prepareLooper();
-        final List<MediaItem2> list = MediaTestUtils.createPlaylist(2);
+        final List<String> list = MediaTestUtils.createMediaIds(2);
         testOnCommandRequest(COMMAND_CODE_PLAYER_SET_PLAYLIST, new PermissionTestTask() {
             @Override
             public void run(RemoteMediaController2 controller) {
@@ -248,11 +249,11 @@
     @Test
     public void testSetMediaItem() throws InterruptedException {
         prepareLooper();
-        final MediaItem2 item = MediaTestUtils.createMediaItemWithMetadata();
+        final String testMediaId = "testSetMediaItem";
         testOnCommandRequest(COMMAND_CODE_PLAYER_SET_MEDIA_ITEM, new PermissionTestTask() {
             @Override
             public void run(RemoteMediaController2 controller) {
-                controller.setMediaItem(item);
+                controller.setMediaItem(testMediaId);
             }
         });
     }
@@ -272,11 +273,11 @@
     @Test
     public void testAddPlaylistItem() throws InterruptedException {
         prepareLooper();
-        final MediaItem2 testItem = MediaTestUtils.createMediaItemWithMetadata();
+        final String testMediaId = "testAddPlaylistItem";
         testOnCommandRequest(COMMAND_CODE_PLAYER_ADD_PLAYLIST_ITEM, new PermissionTestTask() {
             @Override
             public void run(RemoteMediaController2 controller) {
-                controller.addPlaylistItem(0, testItem);
+                controller.addPlaylistItem(0, testMediaId);
             }
         });
     }
@@ -289,7 +290,7 @@
                 new PermissionTestTask() {
                     @Override
                     public void run(RemoteMediaController2 controller) {
-                        controller.removePlaylistItem(testItem);
+                        controller.removePlaylistItem(0);
                     }
                 });
     }
@@ -297,12 +298,12 @@
     @Test
     public void testReplacePlaylistItem() throws InterruptedException {
         prepareLooper();
-        final MediaItem2 testItem = MediaTestUtils.createMediaItemWithMetadata();
+        final String testMediaId = "testReplacePlaylistItem";
         testOnCommandRequest(COMMAND_CODE_PLAYER_REPLACE_PLAYLIST_ITEM,
                 new PermissionTestTask() {
                     @Override
                     public void run(RemoteMediaController2 controller) {
-                        controller.replacePlaylistItem(0, testItem);
+                        controller.replacePlaylistItem(0, testMediaId);
                     }
                 });
     }
@@ -364,6 +365,40 @@
     }
 
     @Test
+    public void testSkipForward() throws InterruptedException {
+        prepareLooper();
+        createSessionWithAllowedActions(
+                createCommandGroupWith(COMMAND_CODE_SESSION_SKIP_FORWARD));
+        createRemoteController2(mSession.getToken()).skipForward();
+
+        assertTrue(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertTrue(mCallback.mOnSkipForwardCalled);
+
+        createSessionWithAllowedActions(
+                createCommandGroupWithout(COMMAND_CODE_SESSION_SKIP_FORWARD));
+        createRemoteController2(mSession.getToken()).skipForward();
+        assertFalse(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertFalse(mCallback.mOnSkipForwardCalled);
+    }
+
+    @Test
+    public void testSkipBackward() throws InterruptedException {
+        prepareLooper();
+        createSessionWithAllowedActions(
+                createCommandGroupWith(COMMAND_CODE_SESSION_SKIP_BACKWARD));
+        createRemoteController2(mSession.getToken()).skipBackward();
+
+        assertTrue(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertTrue(mCallback.mOnSkipBackwardCalled);
+
+        createSessionWithAllowedActions(
+                createCommandGroupWithout(COMMAND_CODE_SESSION_SKIP_BACKWARD));
+        createRemoteController2(mSession.getToken()).skipBackward();
+        assertFalse(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertFalse(mCallback.mOnSkipBackwardCalled);
+    }
+
+    @Test
     public void testPlayFromMediaId() throws InterruptedException {
         prepareLooper();
         final String mediaId = "testPlayFromMediaId";
@@ -428,8 +463,8 @@
         prepareLooper();
         final String mediaId = "testPrepareFromMediaId";
         createSessionWithAllowedActions(
-                createCommandGroupWith(COMMAND_CODE_SESSION_PREFETCH_FROM_MEDIA_ID));
-        createRemoteController2(mSession.getToken()).prefetchFromMediaId(mediaId, null);
+                createCommandGroupWith(COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID));
+        createRemoteController2(mSession.getToken()).prepareFromMediaId(mediaId, null);
 
         assertTrue(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
         assertTrue(mCallback.mOnPrepareFromMediaIdCalled);
@@ -437,8 +472,8 @@
         assertNull(mCallback.mExtras);
 
         createSessionWithAllowedActions(
-                createCommandGroupWithout(COMMAND_CODE_SESSION_PREFETCH_FROM_MEDIA_ID));
-        createRemoteController2(mSession.getToken()).prefetchFromMediaId(mediaId, null);
+                createCommandGroupWithout(COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID));
+        createRemoteController2(mSession.getToken()).prepareFromMediaId(mediaId, null);
         assertFalse(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
         assertFalse(mCallback.mOnPrepareFromMediaIdCalled);
     }
@@ -446,10 +481,10 @@
     @Test
     public void testPrepareFromUri() throws InterruptedException {
         prepareLooper();
-        final Uri uri = Uri.parse("prefetch://from.uri");
+        final Uri uri = Uri.parse("prepare://from.uri");
         createSessionWithAllowedActions(
-                createCommandGroupWith(COMMAND_CODE_SESSION_PREFETCH_FROM_URI));
-        createRemoteController2(mSession.getToken()).prefetchFromUri(uri, null);
+                createCommandGroupWith(COMMAND_CODE_SESSION_PREPARE_FROM_URI));
+        createRemoteController2(mSession.getToken()).prepareFromUri(uri, null);
 
         assertTrue(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
         assertTrue(mCallback.mOnPrepareFromUriCalled);
@@ -457,8 +492,8 @@
         assertNull(mCallback.mExtras);
 
         createSessionWithAllowedActions(
-                createCommandGroupWithout(COMMAND_CODE_SESSION_PREFETCH_FROM_URI));
-        createRemoteController2(mSession.getToken()).prefetchFromUri(uri, null);
+                createCommandGroupWithout(COMMAND_CODE_SESSION_PREPARE_FROM_URI));
+        createRemoteController2(mSession.getToken()).prepareFromUri(uri, null);
         assertFalse(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
         assertFalse(mCallback.mOnPrepareFromUriCalled);
     }
@@ -468,8 +503,8 @@
         prepareLooper();
         final String query = "testPrepareFromSearch";
         createSessionWithAllowedActions(
-                createCommandGroupWith(COMMAND_CODE_SESSION_PREFETCH_FROM_SEARCH));
-        createRemoteController2(mSession.getToken()).prefetchFromSearch(query, null);
+                createCommandGroupWith(COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH));
+        createRemoteController2(mSession.getToken()).prepareFromSearch(query, null);
 
         assertTrue(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
         assertTrue(mCallback.mOnPrepareFromSearchCalled);
@@ -477,8 +512,8 @@
         assertNull(mCallback.mExtras);
 
         createSessionWithAllowedActions(
-                createCommandGroupWithout(COMMAND_CODE_SESSION_PREFETCH_FROM_SEARCH));
-        createRemoteController2(mSession.getToken()).prefetchFromSearch(query, null);
+                createCommandGroupWithout(COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH));
+        createRemoteController2(mSession.getToken()).prepareFromSearch(query, null);
         assertFalse(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
         assertFalse(mCallback.mOnPrepareFromSearchCalled);
     }
@@ -509,10 +544,10 @@
         prepareLooper();
         final String query = "testChangingPermissionWithSetAllowedCommands";
         createSessionWithAllowedActions(
-                createCommandGroupWith(COMMAND_CODE_SESSION_PREFETCH_FROM_SEARCH));
+                createCommandGroupWith(COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH));
         RemoteMediaController2 controller = createRemoteController2(mSession.getToken());
 
-        controller.prefetchFromSearch(query, null);
+        controller.prepareFromSearch(query, null);
         assertTrue(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
         assertTrue(mCallback.mOnPrepareFromSearchCalled);
         assertEquals(query, mCallback.mQuery);
@@ -521,10 +556,10 @@
 
         // Change allowed commands.
         mSession.setAllowedCommands(getTestControllerInfo(),
-                createCommandGroupWithout(COMMAND_CODE_SESSION_PREFETCH_FROM_SEARCH));
+                createCommandGroupWithout(COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH));
         Thread.sleep(TIMEOUT_MS);
 
-        controller.prefetchFromSearch(query, null);
+        controller.prepareFromSearch(query, null);
         assertFalse(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
     }
 
@@ -564,6 +599,8 @@
         public boolean mOnPrepareFromUriCalled;
         public boolean mOnFastForwardCalled;
         public boolean mOnRewindCalled;
+        public boolean mOnSkipForwardCalled;
+        public boolean mOnSkipBackwardCalled;
         public boolean mOnSetRatingCalled;
 
 
@@ -619,6 +656,22 @@
         }
 
         @Override
+        public int onSkipForward(MediaSession2 session, ControllerInfo controller) {
+            assertTrue(TextUtils.equals(CLIENT_PACKAGE_NAME, controller.getPackageName()));
+            mOnSkipForwardCalled = true;
+            mCountDownLatch.countDown();
+            return RESULT_CODE_SUCCESS;
+        }
+
+        @Override
+        public int onSkipBackward(MediaSession2 session, ControllerInfo controller) {
+            assertTrue(TextUtils.equals(CLIENT_PACKAGE_NAME, controller.getPackageName()));
+            mOnSkipBackwardCalled = true;
+            mCountDownLatch.countDown();
+            return RESULT_CODE_SUCCESS;
+        }
+
+        @Override
         public int onPlayFromMediaId(MediaSession2 session, ControllerInfo controller,
                 String mediaId, Bundle extras) {
             assertTrue(TextUtils.equals(CLIENT_PACKAGE_NAME, controller.getPackageName()));
@@ -652,7 +705,7 @@
         }
 
         @Override
-        public int onPrefetchFromMediaId(MediaSession2 session, ControllerInfo controller,
+        public int onPrepareFromMediaId(MediaSession2 session, ControllerInfo controller,
                 String mediaId, Bundle extras) {
             assertTrue(TextUtils.equals(CLIENT_PACKAGE_NAME, controller.getPackageName()));
             mOnPrepareFromMediaIdCalled = true;
@@ -663,7 +716,7 @@
         }
 
         @Override
-        public int onPrefetchFromSearch(MediaSession2 session, ControllerInfo controller,
+        public int onPrepareFromSearch(MediaSession2 session, ControllerInfo controller,
                 String query, Bundle extras) {
             assertTrue(TextUtils.equals(CLIENT_PACKAGE_NAME, controller.getPackageName()));
             mOnPrepareFromSearchCalled = true;
@@ -674,7 +727,7 @@
         }
 
         @Override
-        public int onPrefetchFromUri(MediaSession2 session, ControllerInfo controller,
+        public int onPrepareFromUri(MediaSession2 session, ControllerInfo controller,
                 Uri uri, Bundle extras) {
             assertTrue(TextUtils.equals(CLIENT_PACKAGE_NAME, controller.getPackageName()));
             mOnPrepareFromUriCalled = true;
diff --git a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/MediaSessionCompatCallbackTestWithMediaController2.java b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/MediaSessionCompatCallbackTestWithMediaController2.java
index 7b34e14..0998f10 100644
--- a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/MediaSessionCompatCallbackTestWithMediaController2.java
+++ b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/MediaSessionCompatCallbackTestWithMediaController2.java
@@ -57,13 +57,13 @@
 
 import org.junit.After;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
 
 /**
  * Tests {@link MediaController2}.
@@ -81,7 +81,6 @@
     MediaSessionCompat mSession;
     MediaSessionCallback mSessionCallback;
     AudioManager mAudioManager;
-    RemoteMediaController2 mController;
 
     @Before
     @Override
@@ -108,35 +107,32 @@
             mSession.release();
             mSession = null;
         }
-
-        if (mController != null) {
-            mController.close();
-            mController = null;
-        }
     }
 
-    private void createControllerAndWaitConnection() throws Exception {
+    private RemoteMediaController2 createControllerAndWaitConnection() throws Exception {
         final CountDownLatch latch = new CountDownLatch(1);
+        final AtomicReference<SessionToken2> sessionToken2 = new AtomicReference<>();
         SessionToken2.createSessionToken2(mContext, mSession.getSessionToken(),
                 sHandlerExecutor, new SessionToken2.OnSessionToken2CreatedListener() {
                     @Override
                     public void onSessionToken2Created(
                             MediaSessionCompat.Token token, SessionToken2 token2) {
                         assertTrue(token2.isLegacySession());
-                        mController = new RemoteMediaController2(mContext, token2, true);
+                        sessionToken2.set(token2);
                         latch.countDown();
                     }
                 });
         assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        return createRemoteController2(sessionToken2.get(), true);
     }
 
     @Test
     public void testPlay() throws Exception {
         prepareLooper();
-        createControllerAndWaitConnection();
+        RemoteMediaController2 controller = createControllerAndWaitConnection();
         mSessionCallback.reset(1);
 
-        mController.play();
+        controller.play();
         assertTrue(mSessionCallback.await(TIME_OUT_MS));
         assertEquals(1, mSessionCallback.mOnPlayCalledCount);
     }
@@ -144,10 +140,10 @@
     @Test
     public void testPause() throws Exception {
         prepareLooper();
-        createControllerAndWaitConnection();
+        RemoteMediaController2 controller = createControllerAndWaitConnection();
         mSessionCallback.reset(1);
 
-        mController.pause();
+        controller.pause();
         assertTrue(mSessionCallback.await(TIME_OUT_MS));
         assertEquals(true, mSessionCallback.mOnPauseCalled);
     }
@@ -155,10 +151,10 @@
     @Test
     public void testPrepare() throws Exception {
         prepareLooper();
-        createControllerAndWaitConnection();
+        RemoteMediaController2 controller = createControllerAndWaitConnection();
         mSessionCallback.reset(1);
 
-        mController.prefetch();
+        controller.prepare();
         assertTrue(mSessionCallback.await(TIME_OUT_MS));
         assertEquals(true, mSessionCallback.mOnPrepareCalled);
     }
@@ -166,11 +162,11 @@
     @Test
     public void testSeekTo() throws Exception {
         prepareLooper();
-        createControllerAndWaitConnection();
+        RemoteMediaController2 controller = createControllerAndWaitConnection();
         mSessionCallback.reset(1);
 
         final long seekPosition = 12125L;
-        mController.seekTo(seekPosition);
+        controller.seekTo(seekPosition);
         assertTrue(mSessionCallback.await(TIME_OUT_MS));
         assertTrue(mSessionCallback.mOnSeekToCalled);
         assertEquals(seekPosition, mSessionCallback.mSeekPosition);
@@ -181,21 +177,20 @@
         prepareLooper();
         final List<MediaItem2> testList = MediaTestUtils.createPlaylist(2);
         final List<QueueItem> testQueue = MediaUtils2.convertToQueueItemList(testList);
-        final MediaItem2 testMediaItem2ToAdd = MediaTestUtils.createMediaItemWithMetadata();
+        final String testMediaId = "testAddPlaylistItem";
 
         mSession.setQueue(testQueue);
         mSession.setFlags(FLAG_HANDLES_QUEUE_COMMANDS);
-        createControllerAndWaitConnection();
+        RemoteMediaController2 controller = createControllerAndWaitConnection();
 
         final int testIndex = 1;
-        mController.addPlaylistItem(testIndex, testMediaItem2ToAdd);
+        controller.addPlaylistItem(testIndex, testMediaId);
         assertTrue(mSessionCallback.await(TIMEOUT_MS));
         assertTrue(mSessionCallback.mOnAddQueueItemAtCalled);
 
         assertEquals(testIndex, mSessionCallback.mQueueIndex);
         assertNotNull(mSessionCallback.mQueueDescriptionForAdd);
-        assertEquals(testMediaItem2ToAdd.getMediaId(),
-                mSessionCallback.mQueueDescriptionForAdd.getMediaId());
+        assertEquals(testMediaId, mSessionCallback.mQueueDescriptionForAdd.getMediaId());
     }
 
     @Test
@@ -206,10 +201,10 @@
 
         mSession.setQueue(testQueue);
         mSession.setFlags(FLAG_HANDLES_QUEUE_COMMANDS);
-        createControllerAndWaitConnection();
+        RemoteMediaController2 controller = createControllerAndWaitConnection();
 
         final MediaItem2 itemToRemove = testList.get(1);
-        mController.removePlaylistItem(itemToRemove);
+        controller.removePlaylistItem(1);
         assertTrue(mSessionCallback.await(TIMEOUT_MS));
         assertTrue(mSessionCallback.mOnRemoveQueueItemCalled);
 
@@ -225,14 +220,14 @@
         // replace = remove + add
         final List<MediaItem2> testList = MediaTestUtils.createPlaylist(2);
         final List<QueueItem> testQueue = MediaUtils2.convertToQueueItemList(testList);
-        final MediaItem2 testMediaItem2ToReplace = MediaTestUtils.createMediaItemWithMetadata();
+        final String testMediaId = "testReplacePlaylistItem";
 
         mSession.setQueue(testQueue);
         mSession.setFlags(FLAG_HANDLES_QUEUE_COMMANDS);
-        createControllerAndWaitConnection();
+        RemoteMediaController2 controller = createControllerAndWaitConnection();
 
         mSessionCallback.reset(2);
-        mController.replacePlaylistItem(1, testMediaItem2ToReplace);
+        controller.replacePlaylistItem(testReplaceIndex, testMediaId);
         assertTrue(mSessionCallback.await(TIMEOUT_MS));
         assertTrue(mSessionCallback.mOnRemoveQueueItemCalled);
         assertTrue(mSessionCallback.mOnAddQueueItemAtCalled);
@@ -242,17 +237,16 @@
                 mSessionCallback.mQueueDescriptionForRemove.getMediaId());
 
         assertNotNull(mSessionCallback.mQueueDescriptionForAdd);
-        assertEquals(testMediaItem2ToReplace.getMediaId(),
-                mSessionCallback.mQueueDescriptionForAdd.getMediaId());
+        assertEquals(testMediaId, mSessionCallback.mQueueDescriptionForAdd.getMediaId());
     }
 
     @Test
     public void testSkipToPreviousItem() throws Exception {
         prepareLooper();
-        createControllerAndWaitConnection();
+        RemoteMediaController2 controller = createControllerAndWaitConnection();
         mSessionCallback.reset(1);
 
-        mController.skipToPreviousItem();
+        controller.skipToPreviousItem();
         assertTrue(mSessionCallback.await(TIME_OUT_MS));
         assertTrue(mSessionCallback.mOnSkipToPreviousCalled);
     }
@@ -260,31 +254,31 @@
     @Test
     public void testSkipToNextItem() throws Exception {
         prepareLooper();
-        createControllerAndWaitConnection();
+        RemoteMediaController2 controller = createControllerAndWaitConnection();
         mSessionCallback.reset(1);
 
-        mController.skipToNextItem();
+        controller.skipToNextItem();
         assertTrue(mSessionCallback.await(TIME_OUT_MS));
         assertTrue(mSessionCallback.mOnSkipToNextCalled);
     }
 
-    //@Test see: b/110738672
+    @Test
     public void testSkipToPlaylistItem() throws Exception {
         prepareLooper();
-        createControllerAndWaitConnection();
+
+        final int testSkipToIndex = 1;
+        final List<MediaItem2> testList = MediaTestUtils.createPlaylist(2);
+        final List<QueueItem> testQueue = MediaUtils2.convertToQueueItemList(testList);
+
+        mSession.setQueue(testQueue);
+        mSession.setFlags(FLAG_HANDLES_QUEUE_COMMANDS);
+        RemoteMediaController2 controller = createControllerAndWaitConnection();
+
         mSessionCallback.reset(1);
-
-        final long queueItemId = 1;
-        final QueueItem queueItem = new QueueItem(
-                MediaUtils2.convertToMediaMetadataCompat(MediaTestUtils.createMetadata())
-                        .getDescription(),
-                queueItemId);
-        final MediaItem2 mediaItem2 = MediaUtils2.convertToMediaItem2(queueItem);
-
-        mController.skipToPlaylistItem(mediaItem2);
+        controller.skipToPlaylistItem(testSkipToIndex);
         assertTrue(mSessionCallback.await(TIME_OUT_MS));
         assertTrue(mSessionCallback.mOnSkipToQueueItemCalled);
-        assertEquals(queueItemId, mSessionCallback.mQueueItemId);
+        assertEquals(testQueue.get(testSkipToIndex).getQueueId(), mSessionCallback.mQueueItemId);
     }
 
     @Test
@@ -293,10 +287,10 @@
         final int testShuffleMode = SessionPlayer2.SHUFFLE_MODE_GROUP;
 
         mSession.setShuffleMode(PlaybackStateCompat.SHUFFLE_MODE_NONE);
-        createControllerAndWaitConnection();
+        RemoteMediaController2 controller = createControllerAndWaitConnection();
         mSessionCallback.reset(1);
 
-        mController.setShuffleMode(testShuffleMode);
+        controller.setShuffleMode(testShuffleMode);
         assertTrue(mSessionCallback.await(TIME_OUT_MS));
         assertTrue(mSessionCallback.mOnSetShuffleModeCalled);
         assertEquals(testShuffleMode, mSessionCallback.mShuffleMode);
@@ -308,10 +302,10 @@
         final int testRepeatMode = SessionPlayer2.REPEAT_MODE_ALL;
 
         mSession.setRepeatMode(PlaybackStateCompat.REPEAT_MODE_NONE);
-        createControllerAndWaitConnection();
+        RemoteMediaController2 controller = createControllerAndWaitConnection();
         mSessionCallback.reset(1);
 
-        mController.setRepeatMode(testRepeatMode);
+        controller.setRepeatMode(testRepeatMode);
         assertTrue(mSessionCallback.await(TIME_OUT_MS));
         assertTrue(mSessionCallback.mOnSetRepeatModeCalled);
         assertEquals(testRepeatMode, mSessionCallback.mRepeatMode);
@@ -326,10 +320,10 @@
         TestVolumeProvider volumeProvider =
                 new TestVolumeProvider(volumeControlType, maxVolume, currentVolume);
         mSession.setPlaybackToRemote(volumeProvider);
-        createControllerAndWaitConnection();
+        RemoteMediaController2 controller = createControllerAndWaitConnection();
 
         final int targetVolume = 50;
-        mController.setVolumeTo(targetVolume, 0 /* flags */);
+        controller.setVolumeTo(targetVolume, 0 /* flags */);
         assertTrue(volumeProvider.mLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
         assertTrue(volumeProvider.mSetVolumeToCalled);
         assertEquals(targetVolume, volumeProvider.mVolume);
@@ -344,10 +338,10 @@
         TestVolumeProvider volumeProvider =
                 new TestVolumeProvider(volumeControlType, maxVolume, currentVolume);
         mSession.setPlaybackToRemote(volumeProvider);
-        createControllerAndWaitConnection();
+        RemoteMediaController2 controller = createControllerAndWaitConnection();
 
         final int direction = AudioManager.ADJUST_RAISE;
-        mController.adjustVolume(direction, 0 /* flags */);
+        controller.adjustVolume(direction, 0 /* flags */);
         assertTrue(volumeProvider.mLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
         assertTrue(volumeProvider.mAdjustVolumeCalled);
         assertEquals(direction, volumeProvider.mDirection);
@@ -371,12 +365,12 @@
         }
         // Set stream of the session.
         mSession.setPlaybackToLocal(stream);
-        createControllerAndWaitConnection();
+        RemoteMediaController2 controller = createControllerAndWaitConnection();
         final int originalVolume = mAudioManager.getStreamVolume(stream);
         final int targetVolume = originalVolume == minVolume
                 ? originalVolume + 1 : originalVolume - 1;
 
-        mController.setVolumeTo(targetVolume, AudioManager.FLAG_SHOW_UI);
+        controller.setVolumeTo(targetVolume, AudioManager.FLAG_SHOW_UI);
         new PollingCheck(TIMEOUT_MS) {
             @Override
             protected boolean check() {
@@ -406,14 +400,14 @@
         }
         // Set stream of the session.
         mSession.setPlaybackToLocal(stream);
-        createControllerAndWaitConnection();
+        RemoteMediaController2 controller = createControllerAndWaitConnection();
 
         final int originalVolume = mAudioManager.getStreamVolume(stream);
         final int direction = originalVolume == minVolume
                 ? AudioManager.ADJUST_RAISE : AudioManager.ADJUST_LOWER;
         final int targetVolume = originalVolume + direction;
 
-        mController.adjustVolume(direction, AudioManager.FLAG_SHOW_UI);
+        controller.adjustVolume(direction, AudioManager.FLAG_SHOW_UI);
         new PollingCheck(TIMEOUT_MS) {
             @Override
             protected boolean check() {
@@ -432,10 +426,10 @@
         final Bundle testArgs = new Bundle();
         testArgs.putString("args", "test_args");
         final SessionCommand2 testCommand = new SessionCommand2(command, null);
-        createControllerAndWaitConnection();
+        RemoteMediaController2 controller = createControllerAndWaitConnection();
         mSessionCallback.reset(1);
 
-        mController.sendCustomCommand(testCommand, testArgs);
+        controller.sendCustomCommand(testCommand, testArgs);
         assertTrue(mSessionCallback.await(TIME_OUT_MS));
         assertEquals(true, mSessionCallback.mOnCommandCalled);
         assertEquals(command, mSessionCallback.mCommand);
@@ -445,10 +439,10 @@
     @Test
     public void testFastForward() throws Exception {
         prepareLooper();
-        createControllerAndWaitConnection();
+        RemoteMediaController2 controller = createControllerAndWaitConnection();
         mSessionCallback.reset(1);
 
-        mController.fastForward();
+        controller.fastForward();
         assertTrue(mSessionCallback.await(TIME_OUT_MS));
         assertEquals(true, mSessionCallback.mOnFastForwardCalled);
     }
@@ -456,10 +450,10 @@
     @Test
     public void testRewind() throws Exception {
         prepareLooper();
-        createControllerAndWaitConnection();
+        RemoteMediaController2 controller = createControllerAndWaitConnection();
         mSessionCallback.reset(1);
 
-        mController.rewind();
+        controller.rewind();
         assertTrue(mSessionCallback.await(TIME_OUT_MS));
         assertEquals(true, mSessionCallback.mOnRewindCalled);
     }
@@ -470,10 +464,10 @@
         final String request = "random query";
         final Bundle bundle = new Bundle();
         bundle.putString("key", "value");
-        createControllerAndWaitConnection();
+        RemoteMediaController2 controller = createControllerAndWaitConnection();
         mSessionCallback.reset(1);
 
-        mController.playFromSearch(request, bundle);
+        controller.playFromSearch(request, bundle);
         assertTrue(mSessionCallback.await(TIME_OUT_MS));
         assertEquals(true, mSessionCallback.mOnPlayFromSearchCalled);
         assertEquals(request, mSessionCallback.mQuery);
@@ -486,10 +480,10 @@
         final Uri request = Uri.parse("foo://boo");
         final Bundle bundle = new Bundle();
         bundle.putString("key", "value");
-        createControllerAndWaitConnection();
+        RemoteMediaController2 controller = createControllerAndWaitConnection();
         mSessionCallback.reset(1);
 
-        mController.playFromUri(request, bundle);
+        controller.playFromUri(request, bundle);
         assertTrue(mSessionCallback.await(TIME_OUT_MS));
         assertEquals(true, mSessionCallback.mOnPlayFromUriCalled);
         assertEquals(request, mSessionCallback.mUri);
@@ -502,10 +496,10 @@
         final String request = "media_id";
         final Bundle bundle = new Bundle();
         bundle.putString("key", "value");
-        createControllerAndWaitConnection();
+        RemoteMediaController2 controller = createControllerAndWaitConnection();
         mSessionCallback.reset(1);
 
-        mController.playFromMediaId(request, bundle);
+        controller.playFromMediaId(request, bundle);
         assertTrue(mSessionCallback.await(TIME_OUT_MS));
         assertEquals(true, mSessionCallback.mOnPlayFromMediaIdCalled);
         assertEquals(request, mSessionCallback.mMediaId);
@@ -513,16 +507,15 @@
     }
 
     @Test
-    @Ignore("b/110738672")
     public void testPrepareFromSearch() throws Exception {
         prepareLooper();
         final String request = "random query";
         final Bundle bundle = new Bundle();
         bundle.putString("key", "value");
-        createControllerAndWaitConnection();
+        RemoteMediaController2 controller = createControllerAndWaitConnection();
         mSessionCallback.reset(1);
 
-        mController.prefetchFromSearch(request, bundle);
+        controller.prepareFromSearch(request, bundle);
         assertTrue(mSessionCallback.await(TIME_OUT_MS));
         assertEquals(true, mSessionCallback.mOnPrepareFromSearchCalled);
         assertEquals(request, mSessionCallback.mQuery);
@@ -535,10 +528,10 @@
         final Uri request = Uri.parse("foo://boo");
         final Bundle bundle = new Bundle();
         bundle.putString("key", "value");
-        createControllerAndWaitConnection();
+        RemoteMediaController2 controller = createControllerAndWaitConnection();
         mSessionCallback.reset(1);
 
-        mController.prefetchFromUri(request, bundle);
+        controller.prepareFromUri(request, bundle);
         assertTrue(mSessionCallback.await(TIME_OUT_MS));
         assertEquals(true, mSessionCallback.mOnPrepareFromUriCalled);
         assertEquals(request, mSessionCallback.mUri);
@@ -551,10 +544,10 @@
         final String request = "media_id";
         final Bundle bundle = new Bundle();
         bundle.putString("key", "value");
-        createControllerAndWaitConnection();
+        RemoteMediaController2 controller = createControllerAndWaitConnection();
         mSessionCallback.reset(1);
 
-        mController.prefetchFromMediaId(request, bundle);
+        controller.prepareFromMediaId(request, bundle);
         assertTrue(mSessionCallback.await(TIME_OUT_MS));
         assertEquals(true, mSessionCallback.mOnPrepareFromMediaIdCalled);
         assertEquals(request, mSessionCallback.mMediaId);
@@ -570,10 +563,10 @@
         final MediaMetadataCompat metadata = new MediaMetadataCompat.Builder()
                 .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mediaId).build();
         mSession.setMetadata(metadata);
-        createControllerAndWaitConnection();
+        RemoteMediaController2 controller = createControllerAndWaitConnection();
         mSessionCallback.reset(1);
 
-        mController.setRating(mediaId, rating2);
+        controller.setRating(mediaId, rating2);
         assertTrue(mSessionCallback.await(TIME_OUT_MS));
         assertTrue(mSessionCallback.mOnSetRatingCalled);
         assertEquals(rating2, MediaUtils2.convertToRating2(mSessionCallback.mRating));
diff --git a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/MediaSessionService2NotificationTest.java b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/MediaSessionService2NotificationTest.java
index 9a576a6..f3347c3 100644
--- a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/MediaSessionService2NotificationTest.java
+++ b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/MediaSessionService2NotificationTest.java
@@ -94,22 +94,23 @@
         TestServiceRegistry.getInstance().setSessionCallback(sessionCallback);
 
         // Create a controller to start the service.
-        RemoteMediaController2 controller = new RemoteMediaController2(
-                mContext, new SessionToken2(mContext, MOCK_MEDIA_SESSION_SERVICE), true);
+        RemoteMediaController2 controller = createRemoteController2(
+                new SessionToken2(mContext, MOCK_MEDIA_SESSION_SERVICE), true);
 
         // Set current media item.
         final String mediaId = "testMediaId";
         Bitmap albumArt = BitmapFactory.decodeResource(mContext.getResources(),
                 android.support.mediacompat.service.R.drawable.big_buck_bunny);
         MediaMetadata2 metadata = new MediaMetadata2.Builder()
-                        .putText(MediaMetadata2.METADATA_KEY_MEDIA_ID, mediaId)
-                        .putText(MediaMetadata2.METADATA_KEY_DISPLAY_TITLE, "Test Song Name")
-                        .putText(MediaMetadata2.METADATA_KEY_ARTIST, "Test Artist Name")
-                        .putBitmap(MediaMetadata2.METADATA_KEY_ALBUM_ART, albumArt)
-                        .build();
-        mPlayer.mCurrentMediaItem = new MediaItem2.Builder(MediaItem2.FLAG_PLAYABLE)
+                .putText(MediaMetadata2.METADATA_KEY_MEDIA_ID, mediaId)
+                .putText(MediaMetadata2.METADATA_KEY_DISPLAY_TITLE, "Test Song Name")
+                .putText(MediaMetadata2.METADATA_KEY_ARTIST, "Test Artist Name")
+                .putBitmap(MediaMetadata2.METADATA_KEY_ALBUM_ART, albumArt)
+                .putLong(MediaMetadata2.METADATA_KEY_BROWSABLE, MediaMetadata2.BROWSABLE_TYPE_NONE)
+                .putLong(MediaMetadata2.METADATA_KEY_PLAYABLE, 1)
+                .build();
+        mPlayer.mCurrentMediaItem = new MediaItem2.Builder()
                         .setMetadata(metadata)
-                        .setMediaId(mediaId)
                         .build();
 
         // Notification should be shown. Clicking play/pause button will change the player state.
diff --git a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/MediaSessionService2Test.java b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/MediaSessionService2Test.java
index cd4b5d4..fd1c313 100644
--- a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/MediaSessionService2Test.java
+++ b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/MediaSessionService2Test.java
@@ -82,8 +82,8 @@
                     }
                 });
 
-        RemoteMediaController2 controller1 = new RemoteMediaController2(mContext, mToken, true);
-        RemoteMediaController2 controller2 = new RemoteMediaController2(mContext, mToken, true);
+        RemoteMediaController2 controller1 = createRemoteController2(mToken, true);
+        RemoteMediaController2 controller2 = createRemoteController2(mToken, true);
 
         assertNotEquals(controller1.getConnectedSessionToken(),
                 controller2.getConnectedSessionToken());
@@ -108,8 +108,8 @@
                     }
                 });
 
-        RemoteMediaController2 controller1 = new RemoteMediaController2(mContext, mToken, true);
-        RemoteMediaController2 controller2 = new RemoteMediaController2(mContext, mToken, true);
+        RemoteMediaController2 controller1 = createRemoteController2(mToken, true);
+        RemoteMediaController2 controller2 = createRemoteController2(mToken, true);
         controller1.close();
         controller2.close();
 
@@ -146,8 +146,8 @@
                     }
                 });
 
-        RemoteMediaController2 controller1 = new RemoteMediaController2(mContext, mToken, true);
-        RemoteMediaController2 controller2 = new RemoteMediaController2(mContext, mToken, true);
+        RemoteMediaController2 controller1 = createRemoteController2(mToken, true);
+        RemoteMediaController2 controller2 = createRemoteController2(mToken, true);
         controller1.close();
         controller2.close();
 
@@ -157,7 +157,7 @@
     @Test
     public void testGetSessions() throws InterruptedException {
         prepareLooper();
-        RemoteMediaController2 controller = new RemoteMediaController2(mContext, mToken, true);
+        RemoteMediaController2 controller = createRemoteController2(mToken, true);
         MediaSessionService2 service = TestServiceRegistry.getInstance().getServiceInstance();
         try (MediaSession2 session = new MediaSession2.Builder(mContext, new MockPlayer(0))
                 .setId("testGetSessions")
@@ -177,7 +177,7 @@
     @Test
     public void testAddSessions_removedWhenClose() throws InterruptedException {
         prepareLooper();
-        RemoteMediaController2 controller = new RemoteMediaController2(mContext, mToken, true);
+        RemoteMediaController2 controller = createRemoteController2(mToken, true);
         MediaSessionService2 service = TestServiceRegistry.getInstance().getServiceInstance();
         try (MediaSession2 session = new MediaSession2.Builder(mContext, new MockPlayer(0))
                 .setId("testAddSessions_removedWhenClose")
diff --git a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/SessionPlayerTest.java b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/SessionPlayerTest.java
index fcdd8e6b..fab65a6 100644
--- a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/SessionPlayerTest.java
+++ b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/tests/SessionPlayerTest.java
@@ -34,6 +34,7 @@
 import androidx.media2.MediaItem2;
 import androidx.media2.MediaMetadata2;
 import androidx.media2.MediaSession2;
+import androidx.media2.MediaSession2.ControllerInfo;
 import androidx.media2.SessionCommandGroup2;
 import androidx.media2.SessionPlayer2;
 import androidx.test.filters.LargeTest;
@@ -77,6 +78,12 @@
                         }
                         return null;
                     }
+
+                    @Override
+                    public MediaItem2 onCreateMediaItem(MediaSession2 session,
+                            ControllerInfo controller, String mediaId) {
+                        return MediaTestUtils.createMediaItem(mediaId);
+                    }
                 }).build();
 
         // Create a default MediaController2 in client app.
@@ -137,7 +144,7 @@
 
     @Test
     public void testPrepareByController() {
-        mController2.prefetch();
+        mController2.prepare();
         try {
             assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
         } catch (InterruptedException e) {
@@ -197,7 +204,7 @@
 
     @Test
     public void testSetPlaylistByController() throws InterruptedException {
-        final List<MediaItem2> list = MediaTestUtils.createPlaylist(2);
+        final List<String> list = MediaTestUtils.createMediaIds(2);
         mController2.setPlaylist(list, null /* metadata */);
         assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
 
@@ -207,8 +214,7 @@
         assertNotNull(mPlayer.mPlaylist);
         assertEquals(list.size(), mPlayer.mPlaylist.size());
         for (int i = 0; i < list.size(); i++) {
-            // MediaController2.setPlaylist does not ensure the equality of the items.
-            assertEquals(list.get(i).getMediaId(), mPlayer.mPlaylist.get(i).getMediaId());
+            assertEquals(list.get(i), mPlayer.mPlaylist.get(i).getMediaId());
         }
     }
 
@@ -265,15 +271,15 @@
     @Test
     public void testAddPlaylistItemByController() throws InterruptedException {
         final int testIndex = 12;
-        final MediaItem2 testMediaItem = MediaTestUtils.createMediaItemWithMetadata();
+        final String testMediaId = "testAddPlaylistItemByController";
 
-        mController2.addPlaylistItem(testIndex, testMediaItem);
+        mController2.addPlaylistItem(testIndex, testMediaId);
         assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
 
         assertTrue(mPlayer.mAddPlaylistItemCalled);
         assertEquals(testIndex, mPlayer.mIndex);
         // MediaController2.addPlaylistItem does not ensure the equality of the items.
-        assertEquals(testMediaItem.getMediaId(), mPlayer.mItem.getMediaId());
+        assertEquals(testMediaId, mPlayer.mItem.getMediaId());
     }
 
     @Test
@@ -290,7 +296,7 @@
         mPlayer.mPlaylist = MediaTestUtils.createPlaylist(2);
         MediaItem2 targetItem = mPlayer.mPlaylist.get(0);
 
-        mController2.removePlaylistItem(targetItem);
+        mController2.removePlaylistItem(0);
         assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
 
         assertTrue(mPlayer.mRemovePlaylistItemCalled);
@@ -311,14 +317,14 @@
     @Test
     public void testReplacePlaylistItemByController() throws InterruptedException {
         final int testIndex = 12;
-        final MediaItem2 testMediaItem = MediaTestUtils.createMediaItemWithMetadata();
+        final String testMediaId = "testReplacePlaylistItemByController";
 
-        mController2.replacePlaylistItem(testIndex, testMediaItem);
+        mController2.replacePlaylistItem(testIndex, testMediaId);
         assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
 
         assertTrue(mPlayer.mReplacePlaylistItemCalled);
         // MediaController2.replacePlaylistItem does not ensure the equality of the items.
-        assertEquals(testMediaItem.getMediaId(), mPlayer.mItem.getMediaId());
+        assertEquals(testMediaId, mPlayer.mItem.getMediaId());
     }
 
     @Test
@@ -360,12 +366,13 @@
 
     @Test
     public void testSkipToPlaylistItemByController() throws InterruptedException {
-        MediaItem2 targetItem = MediaTestUtils.createMediaItemWithMetadata();
-        mController2.skipToPlaylistItem(targetItem);
+        mPlayer.mPlaylist = MediaTestUtils.createPlaylist(3);
+
+        mController2.skipToPlaylistItem(2);
         assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
 
         assertTrue(mPlayer.mSkipToPlaylistItemCalled);
-        assertEquals(targetItem, mPlayer.mItem);
+        assertEquals(mPlayer.mPlaylist.get(2), mPlayer.mItem);
     }
 
     @Test
diff --git a/media/version-compat-tests/lib/src/main/aidl/android/support/mediacompat/testlib/IRemoteMediaController2.aidl b/media/version-compat-tests/lib/src/main/aidl/android/support/mediacompat/testlib/IRemoteMediaController2.aidl
index 67ebe77a..ab82b66 100644
--- a/media/version-compat-tests/lib/src/main/aidl/android/support/mediacompat/testlib/IRemoteMediaController2.aidl
+++ b/media/version-compat-tests/lib/src/main/aidl/android/support/mediacompat/testlib/IRemoteMediaController2.aidl
@@ -30,19 +30,19 @@
     ParcelImpl getConnectedSessionToken(String controllerId);
     void play(String controllerId);
     void pause(String controllerId);
-    void prefetch(String controllerId);
+    void prepare(String controllerId);
     void seekTo(String controllerId, long pos);
     void setPlaybackSpeed(String controllerId, float speed);
-    void setPlaylist(String controllerId, in List<Bundle> list, in Bundle metadata);
-    void createAndSetDummyPlaylist(String controllerId, int size, in Bundle metadata);
-    void setMediaItem(String controllerId, in Bundle item);
-    void updatePlaylistMetadata(String controllerId, in Bundle metadata);
-    void addPlaylistItem(String controllerId, int index, in Bundle item);
-    void removePlaylistItem(String controllerId, in Bundle item);
-    void replacePlaylistItem(String controllerId, int index, in Bundle item);
+    void setPlaylist(String controllerId, in List<String> list, in ParcelImpl metadata);
+    void createAndSetDummyPlaylist(String controllerId, int size, in ParcelImpl metadata);
+    void setMediaItem(String controllerId, in String mediaId);
+    void updatePlaylistMetadata(String controllerId, in ParcelImpl metadata);
+    void addPlaylistItem(String controllerId, int index, String mediaId);
+    void removePlaylistItem(String controllerId, int index);
+    void replacePlaylistItem(String controllerId, int index, String mediaId);
     void skipToPreviousItem(String controllerId);
     void skipToNextItem(String controllerId);
-    void skipToPlaylistItem(String controllerId, in Bundle item);
+    void skipToPlaylistItem(String controllerId, int index);
     void setShuffleMode(String controllerId, int shuffleMode);
     void setRepeatMode(String controllerId, int repeatMode);
     void setVolumeTo(String controllerId, int value, int flags);
@@ -50,12 +50,14 @@
     void sendCustomCommand(String controllerId, in Bundle command, in Bundle args);
     void fastForward(String controllerId);
     void rewind(String controllerId);
+    void skipForward(String controllerId);
+    void skipBackward(String controllerId);
     void playFromMediaId(String controllerId, String mediaId, in Bundle extras);
     void playFromSearch(String controllerId, String query, in Bundle extras);
     void playFromUri(String controllerId, in Uri uri, in Bundle extras);
-    void prefetchFromMediaId(String controllerId, String mediaId, in Bundle extras);
-    void prefetchFromSearch(String controllerId, String query, in Bundle extras);
-    void prefetchFromUri(String controllerId, in Uri uri, in Bundle extras);
+    void prepareFromMediaId(String controllerId, String mediaId, in Bundle extras);
+    void prepareFromSearch(String controllerId, String query, in Bundle extras);
+    void prepareFromUri(String controllerId, in Uri uri, in Bundle extras);
     void setRating(String controllerId, String mediaId, in ParcelImpl rating);
     void subscribeRoutesInfo(String controllerId);
     void unsubscribeRoutesInfo(String controllerId);
diff --git a/media/version-compat-tests/lib/src/main/aidl/android/support/mediacompat/testlib/IRemoteMediaSessionCompat.aidl b/media/version-compat-tests/lib/src/main/aidl/android/support/mediacompat/testlib/IRemoteMediaSessionCompat.aidl
index 829abaa..05e9c64 100644
--- a/media/version-compat-tests/lib/src/main/aidl/android/support/mediacompat/testlib/IRemoteMediaSessionCompat.aidl
+++ b/media/version-compat-tests/lib/src/main/aidl/android/support/mediacompat/testlib/IRemoteMediaSessionCompat.aidl
@@ -32,6 +32,8 @@
     Bundle getSessionToken(String sessionTag);
     void release(String sessionTag);
     void setPlaybackToLocal(String sessionTag, int stream);
+    void setPlaybackToRemote(String sessionTag, int volumeControl, int maxVolume,
+            int currentVolume);
     void setPlaybackState(String sessionTag, in Bundle stateBundle);
     void setMetadata(String sessionTag, in Bundle metadataBundle);
     void setQueue(String sessionTag, in Bundle queueBundle);
@@ -41,4 +43,5 @@
     void setSessionActivity(String sessionTag, in PendingIntent pi);
     void setFlags(String sessionTag, int flags);
     void setRatingType(String sessionTag, int type);
+    void sendSessionEvent(String sessionTag, String event, in Bundle extras);
 }
diff --git a/media2/api/1.0.0-alpha03.txt b/media2/api/1.0.0-alpha03.txt
index 8282a80..82d51ca 100644
--- a/media2/api/1.0.0-alpha03.txt
+++ b/media2/api/1.0.0-alpha03.txt
@@ -7,9 +7,8 @@
 
   public static final class CallbackMediaItem2.Builder {
     ctor public CallbackMediaItem2.Builder(androidx.media2.DataSourceCallback2);
-    method public androidx.media2.CallbackMediaItem2! build();
+    method public androidx.media2.CallbackMediaItem2 build();
     method public androidx.media2.CallbackMediaItem2.Builder! setEndPosition(long);
-    method public androidx.media2.CallbackMediaItem2.Builder! setMediaId(String!);
     method public androidx.media2.CallbackMediaItem2.Builder! setMetadata(androidx.media2.MediaMetadata2!);
     method public androidx.media2.CallbackMediaItem2.Builder! setStartPosition(long);
   }
@@ -17,7 +16,7 @@
   public abstract class DataSourceCallback2 implements java.io.Closeable {
     ctor public DataSourceCallback2();
     method public abstract long getSize() throws java.io.IOException;
-    method public abstract int readAt(long, byte[]!, int, int) throws java.io.IOException;
+    method public abstract int readAt(long, byte[], int, int) throws java.io.IOException;
   }
 
   public class FileMediaItem2 extends androidx.media2.MediaItem2 {
@@ -30,9 +29,8 @@
   public static final class FileMediaItem2.Builder {
     ctor public FileMediaItem2.Builder(java.io.FileDescriptor);
     ctor public FileMediaItem2.Builder(java.io.FileDescriptor, long, long);
-    method public androidx.media2.FileMediaItem2! build();
+    method public androidx.media2.FileMediaItem2 build();
     method public androidx.media2.FileMediaItem2.Builder! setEndPosition(long);
-    method public androidx.media2.FileMediaItem2.Builder! setMediaId(String!);
     method public androidx.media2.FileMediaItem2.Builder! setMetadata(androidx.media2.MediaMetadata2!);
     method public androidx.media2.FileMediaItem2.Builder! setStartPosition(long);
   }
@@ -46,24 +44,24 @@
 
   public class MediaBrowser2 extends androidx.media2.MediaController2 {
     ctor public MediaBrowser2(android.content.Context, androidx.media2.SessionToken2, java.util.concurrent.Executor, androidx.media2.MediaBrowser2.BrowserCallback);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaBrowser2.BrowserResult>! getChildren(String, @IntRange(from=0) int, @IntRange(from=1) int, androidx.media2.MediaLibraryService2.LibraryParams?);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaBrowser2.BrowserResult>! getItem(String);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaBrowser2.BrowserResult>! getLibraryRoot(androidx.media2.MediaLibraryService2.LibraryParams?);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaBrowser2.BrowserResult>! getSearchResult(String, @IntRange(from=0) int, @IntRange(from=1) int, androidx.media2.MediaLibraryService2.LibraryParams?);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaBrowser2.BrowserResult>! search(String, androidx.media2.MediaLibraryService2.LibraryParams?);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaBrowser2.BrowserResult>! subscribe(String, androidx.media2.MediaLibraryService2.LibraryParams?);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaBrowser2.BrowserResult>! unsubscribe(String);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaBrowser2.BrowserResult> getChildren(String, @IntRange(from=0) int, @IntRange(from=1) int, androidx.media2.MediaLibraryService2.LibraryParams?);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaBrowser2.BrowserResult> getItem(String);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaBrowser2.BrowserResult> getLibraryRoot(androidx.media2.MediaLibraryService2.LibraryParams?);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaBrowser2.BrowserResult> getSearchResult(String, @IntRange(from=0) int, @IntRange(from=1) int, androidx.media2.MediaLibraryService2.LibraryParams?);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaBrowser2.BrowserResult> search(String, androidx.media2.MediaLibraryService2.LibraryParams?);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaBrowser2.BrowserResult> subscribe(String, androidx.media2.MediaLibraryService2.LibraryParams?);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaBrowser2.BrowserResult> unsubscribe(String);
   }
 
   public static class MediaBrowser2.BrowserCallback extends androidx.media2.MediaController2.ControllerCallback {
     ctor public MediaBrowser2.BrowserCallback();
-    method public void onChildrenChanged(androidx.media2.MediaBrowser2, String, int, androidx.media2.MediaLibraryService2.LibraryParams?);
-    method public void onSearchResultChanged(androidx.media2.MediaBrowser2, String, int, androidx.media2.MediaLibraryService2.LibraryParams?);
+    method public void onChildrenChanged(androidx.media2.MediaBrowser2, String, @IntRange(from=0) int, androidx.media2.MediaLibraryService2.LibraryParams?);
+    method public void onSearchResultChanged(androidx.media2.MediaBrowser2, String, @IntRange(from=0) int, androidx.media2.MediaLibraryService2.LibraryParams?);
   }
 
   public static class MediaBrowser2.BrowserResult extends androidx.versionedparcelable.CustomVersionedParcelable {
     method public long getCompletionTime();
-    method public androidx.media2.MediaLibraryService2.LibraryParams! getLibraryParams();
+    method public androidx.media2.MediaLibraryService2.LibraryParams? getLibraryParams();
     method public androidx.media2.MediaItem2? getMediaItem();
     method public java.util.List<androidx.media2.MediaItem2>? getMediaItems();
     method public int getResultCode();
@@ -88,14 +86,14 @@
   public class MediaController2 implements java.lang.AutoCloseable {
     ctor public MediaController2(android.content.Context, androidx.media2.SessionToken2, java.util.concurrent.Executor, androidx.media2.MediaController2.ControllerCallback);
     ctor public MediaController2(android.content.Context, android.support.v4.media.session.MediaSessionCompat.Token, java.util.concurrent.Executor, androidx.media2.MediaController2.ControllerCallback);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! addPlaylistItem(int, androidx.media2.MediaItem2);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! adjustVolume(int, int);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> addPlaylistItem(@IntRange(from=0) int, String);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> adjustVolume(int, int);
     method public void close();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! fastForward();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> fastForward();
     method public long getBufferedPosition();
     method public int getBufferingState();
     method public androidx.media2.SessionToken2? getConnectedSessionToken();
-    method public androidx.media2.MediaItem2! getCurrentMediaItem();
+    method public androidx.media2.MediaItem2? getCurrentMediaItem();
     method public long getCurrentPosition();
     method public long getDuration();
     method public androidx.media2.MediaController2.PlaybackInfo? getPlaybackInfo();
@@ -107,25 +105,27 @@
     method public android.app.PendingIntent? getSessionActivity();
     method public int getShuffleMode();
     method public boolean isConnected();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! pause();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! play();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! prefetch();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! removePlaylistItem(androidx.media2.MediaItem2);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! replacePlaylistItem(int, androidx.media2.MediaItem2);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! rewind();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! seekTo(long);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! sendCustomCommand(androidx.media2.SessionCommand2, android.os.Bundle?);
-    method public void setMediaItem(androidx.media2.MediaItem2);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! setPlaybackSpeed(float);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! setPlaylist(java.util.List<androidx.media2.MediaItem2>, androidx.media2.MediaMetadata2?);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! setRating(String, androidx.media2.Rating2);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! setRepeatMode(int);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! setShuffleMode(int);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! setVolumeTo(int, int);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! skipToNextPlaylistItem();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! skipToPlaylistItem(androidx.media2.MediaItem2);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! skipToPreviousPlaylistItem();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! updatePlaylistMetadata(androidx.media2.MediaMetadata2?);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> pause();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> play();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> prepare();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> removePlaylistItem(@IntRange(from=0) int);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> replacePlaylistItem(@IntRange(from=0) int, String);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> rewind();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> seekTo(long);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> sendCustomCommand(androidx.media2.SessionCommand2, android.os.Bundle?);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> setMediaItem(String);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> setPlaybackSpeed(float);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> setPlaylist(java.util.List<java.lang.String>, androidx.media2.MediaMetadata2?);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> setRating(String, androidx.media2.Rating2);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> setRepeatMode(int);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> setShuffleMode(int);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> setVolumeTo(int, int);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> skipBackward();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> skipForward();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> skipToNextPlaylistItem();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> skipToPlaylistItem(@IntRange(from=0) int);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> skipToPreviousPlaylistItem();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> updatePlaylistMetadata(androidx.media2.MediaMetadata2?);
   }
 
   public abstract static class MediaController2.ControllerCallback {
@@ -140,7 +140,7 @@
     method public void onPlaybackInfoChanged(androidx.media2.MediaController2, androidx.media2.MediaController2.PlaybackInfo);
     method public void onPlaybackSpeedChanged(androidx.media2.MediaController2, float);
     method public void onPlayerStateChanged(androidx.media2.MediaController2, int);
-    method public void onPlaylistChanged(androidx.media2.MediaController2, java.util.List<androidx.media2.MediaItem2>, androidx.media2.MediaMetadata2?);
+    method public void onPlaylistChanged(androidx.media2.MediaController2, java.util.List<androidx.media2.MediaItem2>?, androidx.media2.MediaMetadata2?);
     method public void onPlaylistMetadataChanged(androidx.media2.MediaController2, androidx.media2.MediaMetadata2?);
     method public void onRepeatModeChanged(androidx.media2.MediaController2, int);
     method public void onSeekCompleted(androidx.media2.MediaController2, long);
@@ -173,7 +173,7 @@
   }
 
   public static final class MediaController2.PlaybackInfo implements androidx.versionedparcelable.VersionedParcelable {
-    method public androidx.media.AudioAttributesCompat! getAudioAttributes();
+    method public androidx.media.AudioAttributesCompat? getAudioAttributes();
     method public int getControlType();
     method public int getCurrentVolume();
     method public int getMaxVolume();
@@ -184,23 +184,16 @@
 
   public class MediaItem2 implements androidx.versionedparcelable.VersionedParcelable {
     method public long getEndPosition();
-    method public int getFlags();
-    method public String? getMediaId();
     method public androidx.media2.MediaMetadata2? getMetadata();
     method public long getStartPosition();
-    method public boolean isBrowsable();
-    method public boolean isPlayable();
     method public void setMetadata(androidx.media2.MediaMetadata2?);
-    field public static final int FLAG_BROWSABLE = 1; // 0x1
-    field public static final int FLAG_PLAYABLE = 2; // 0x2
     field public static final long POSITION_UNKNOWN = 576460752303423487L; // 0x7ffffffffffffffL
   }
 
   public static class MediaItem2.Builder {
-    ctor public MediaItem2.Builder(int);
+    ctor public MediaItem2.Builder();
     method public androidx.media2.MediaItem2! build();
     method public androidx.media2.MediaItem2.BuilderBase! setEndPosition(long);
-    method public androidx.media2.MediaItem2.BuilderBase! setMediaId(String!);
     method public androidx.media2.MediaItem2.BuilderBase! setMetadata(androidx.media2.MediaMetadata2!);
     method public androidx.media2.MediaItem2.BuilderBase! setStartPosition(long);
   }
@@ -220,11 +213,11 @@
 
   public static final class MediaLibraryService2.LibraryParams.Builder {
     ctor public MediaLibraryService2.LibraryParams.Builder();
-    method public androidx.media2.MediaLibraryService2.LibraryParams! build();
-    method public androidx.media2.MediaLibraryService2.LibraryParams.Builder! setExtras(android.os.Bundle?);
-    method public androidx.media2.MediaLibraryService2.LibraryParams.Builder! setOffline(boolean);
-    method public androidx.media2.MediaLibraryService2.LibraryParams.Builder! setRecent(boolean);
-    method public androidx.media2.MediaLibraryService2.LibraryParams.Builder! setSuggested(boolean);
+    method public androidx.media2.MediaLibraryService2.LibraryParams build();
+    method public androidx.media2.MediaLibraryService2.LibraryParams.Builder setExtras(android.os.Bundle?);
+    method public androidx.media2.MediaLibraryService2.LibraryParams.Builder setOffline(boolean);
+    method public androidx.media2.MediaLibraryService2.LibraryParams.Builder setRecent(boolean);
+    method public androidx.media2.MediaLibraryService2.LibraryParams.Builder setSuggested(boolean);
   }
 
   public static class MediaLibraryService2.LibraryResult extends androidx.versionedparcelable.CustomVersionedParcelable {
@@ -250,9 +243,9 @@
   }
 
   public static final class MediaLibraryService2.MediaLibrarySession extends androidx.media2.MediaSession2 {
-    method public void notifyChildrenChanged(androidx.media2.MediaSession2.ControllerInfo, String, int, androidx.media2.MediaLibraryService2.LibraryParams?);
+    method public void notifyChildrenChanged(androidx.media2.MediaSession2.ControllerInfo, String, @IntRange(from=0) int, androidx.media2.MediaLibraryService2.LibraryParams?);
     method public void notifyChildrenChanged(String, int, androidx.media2.MediaLibraryService2.LibraryParams?);
-    method public void notifySearchResultChanged(androidx.media2.MediaSession2.ControllerInfo, String, int, androidx.media2.MediaLibraryService2.LibraryParams?);
+    method public void notifySearchResultChanged(androidx.media2.MediaSession2.ControllerInfo, String, @IntRange(from=0) int, androidx.media2.MediaLibraryService2.LibraryParams?);
   }
 
   public static final class MediaLibraryService2.MediaLibrarySession.Builder {
@@ -264,10 +257,10 @@
 
   public static class MediaLibraryService2.MediaLibrarySession.MediaLibrarySessionCallback extends androidx.media2.MediaSession2.SessionCallback {
     ctor public MediaLibraryService2.MediaLibrarySession.MediaLibrarySessionCallback();
-    method public androidx.media2.MediaLibraryService2.LibraryResult onGetChildren(androidx.media2.MediaLibraryService2.MediaLibrarySession, androidx.media2.MediaSession2.ControllerInfo, String, int, int, androidx.media2.MediaLibraryService2.LibraryParams?);
+    method public androidx.media2.MediaLibraryService2.LibraryResult onGetChildren(androidx.media2.MediaLibraryService2.MediaLibrarySession, androidx.media2.MediaSession2.ControllerInfo, String, @IntRange(from=0) int, @IntRange(from=1) int, androidx.media2.MediaLibraryService2.LibraryParams?);
     method public androidx.media2.MediaLibraryService2.LibraryResult onGetItem(androidx.media2.MediaLibraryService2.MediaLibrarySession, androidx.media2.MediaSession2.ControllerInfo, String);
     method public androidx.media2.MediaLibraryService2.LibraryResult onGetLibraryRoot(androidx.media2.MediaLibraryService2.MediaLibrarySession, androidx.media2.MediaSession2.ControllerInfo, androidx.media2.MediaLibraryService2.LibraryParams?);
-    method public androidx.media2.MediaLibraryService2.LibraryResult onGetSearchResult(androidx.media2.MediaLibraryService2.MediaLibrarySession, androidx.media2.MediaSession2.ControllerInfo, String, int, int, androidx.media2.MediaLibraryService2.LibraryParams?);
+    method public androidx.media2.MediaLibraryService2.LibraryResult onGetSearchResult(androidx.media2.MediaLibraryService2.MediaLibrarySession, androidx.media2.MediaSession2.ControllerInfo, String, @IntRange(from=0) int, @IntRange(from=1) int, androidx.media2.MediaLibraryService2.LibraryParams?);
     method public int onSearch(androidx.media2.MediaLibraryService2.MediaLibrarySession, androidx.media2.MediaSession2.ControllerInfo, String, androidx.media2.MediaLibraryService2.LibraryParams?);
     method public int onSubscribe(androidx.media2.MediaLibraryService2.MediaLibrarySession, androidx.media2.MediaSession2.ControllerInfo, String, androidx.media2.MediaLibraryService2.LibraryParams?);
     method public int onUnsubscribe(androidx.media2.MediaLibraryService2.MediaLibrarySession, androidx.media2.MediaSession2.ControllerInfo, String);
@@ -287,13 +280,14 @@
     method public java.util.Set<java.lang.String> keySet();
     method public int size();
     method public android.os.Bundle toBundle();
-    field public static final long BT_FOLDER_TYPE_ALBUMS = 2L; // 0x2L
-    field public static final long BT_FOLDER_TYPE_ARTISTS = 3L; // 0x3L
-    field public static final long BT_FOLDER_TYPE_GENRES = 4L; // 0x4L
-    field public static final long BT_FOLDER_TYPE_MIXED = 0L; // 0x0L
-    field public static final long BT_FOLDER_TYPE_PLAYLISTS = 5L; // 0x5L
-    field public static final long BT_FOLDER_TYPE_TITLES = 1L; // 0x1L
-    field public static final long BT_FOLDER_TYPE_YEARS = 6L; // 0x6L
+    field public static final long BROWSABLE_TYPE_ALBUMS = 2L; // 0x2L
+    field public static final long BROWSABLE_TYPE_ARTISTS = 3L; // 0x3L
+    field public static final long BROWSABLE_TYPE_GENRES = 4L; // 0x4L
+    field public static final long BROWSABLE_TYPE_MIXED = 0L; // 0x0L
+    field public static final long BROWSABLE_TYPE_NONE = -1L; // 0xffffffffffffffffL
+    field public static final long BROWSABLE_TYPE_PLAYLISTS = 5L; // 0x5L
+    field public static final long BROWSABLE_TYPE_TITLES = 1L; // 0x1L
+    field public static final long BROWSABLE_TYPE_YEARS = 6L; // 0x6L
     field public static final String METADATA_KEY_ADVERTISEMENT = "android.media.metadata.ADVERTISEMENT";
     field public static final String METADATA_KEY_ALBUM = "android.media.metadata.ALBUM";
     field public static final String METADATA_KEY_ALBUM_ART = "android.media.metadata.ALBUM_ART";
@@ -303,7 +297,7 @@
     field public static final String METADATA_KEY_ARTIST = "android.media.metadata.ARTIST";
     field public static final String METADATA_KEY_ART_URI = "android.media.metadata.ART_URI";
     field public static final String METADATA_KEY_AUTHOR = "android.media.metadata.AUTHOR";
-    field public static final String METADATA_KEY_BT_FOLDER_TYPE = "android.media.metadata.BT_FOLDER_TYPE";
+    field public static final String METADATA_KEY_BROWSABLE = "android.media.metadata.BT_FOLDER_TYPE";
     field public static final String METADATA_KEY_COMPILATION = "android.media.metadata.COMPILATION";
     field public static final String METADATA_KEY_COMPOSER = "android.media.metadata.COMPOSER";
     field public static final String METADATA_KEY_DATE = "android.media.metadata.DATE";
@@ -320,6 +314,7 @@
     field public static final String METADATA_KEY_MEDIA_ID = "android.media.metadata.MEDIA_ID";
     field public static final String METADATA_KEY_MEDIA_URI = "android.media.metadata.MEDIA_URI";
     field public static final String METADATA_KEY_NUM_TRACKS = "android.media.metadata.NUM_TRACKS";
+    field public static final String METADATA_KEY_PLAYABLE = "android.media.metadata.playable";
     field public static final String METADATA_KEY_RATING = "android.media.metadata.RATING";
     field public static final String METADATA_KEY_TITLE = "android.media.metadata.TITLE";
     field public static final String METADATA_KEY_TRACK_NUMBER = "android.media.metadata.TRACK_NUMBER";
@@ -341,7 +336,98 @@
     method public androidx.media2.MediaMetadata2.Builder putRating(String, androidx.media2.Rating2?);
     method public androidx.media2.MediaMetadata2.Builder putString(String, String?);
     method public androidx.media2.MediaMetadata2.Builder putText(String, CharSequence?);
-    method public androidx.media2.MediaMetadata2.Builder! setExtras(android.os.Bundle?);
+    method public androidx.media2.MediaMetadata2.Builder setExtras(android.os.Bundle?);
+  }
+
+  public class MediaPlayer extends androidx.media2.SessionPlayer2 {
+    ctor public MediaPlayer(android.content.Context);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> addPlaylistItem(int, androidx.media2.MediaItem2);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> attachAuxEffect(int);
+    method public void close() throws java.lang.Exception;
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> deselectTrack(int);
+    method public androidx.media.AudioAttributesCompat? getAudioAttributes();
+    method public int getAudioSessionId();
+    method public long getBufferedPosition();
+    method public int getBufferingState();
+    method public androidx.media2.MediaItem2? getCurrentMediaItem();
+    method public long getCurrentPosition();
+    method public long getDuration();
+    method public float getMaxPlayerVolume();
+    method public androidx.media2.PlaybackParams2 getPlaybackParams();
+    method public float getPlaybackSpeed();
+    method public int getPlayerState();
+    method public float getPlayerVolume();
+    method public java.util.List<androidx.media2.MediaItem2>? getPlaylist();
+    method public androidx.media2.MediaMetadata2? getPlaylistMetadata();
+    method public int getRepeatMode();
+    method public int getSelectedTrack(int);
+    method public int getShuffleMode();
+    method public androidx.media2.MediaTimestamp2? getTimestamp();
+    method public java.util.List<androidx.media2.MediaPlayer.TrackInfo> getTrackInfo();
+    method public int getVideoHeight();
+    method public int getVideoWidth();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> pause();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> play();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> prepare();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> removePlaylistItem(androidx.media2.MediaItem2);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> replacePlaylistItem(int, androidx.media2.MediaItem2);
+    method public void reset();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> seekTo(long);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> seekTo(long, int);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> selectTrack(int);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> setAudioAttributes(androidx.media.AudioAttributesCompat);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> setAudioSessionId(int);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> setAuxEffectSendLevel(float);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> setMediaItem(androidx.media2.MediaItem2);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> setPlaybackParams(androidx.media2.PlaybackParams2);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> setPlaybackSpeed(float);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> setPlayerVolume(float);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> setPlaylist(java.util.List<androidx.media2.MediaItem2>, androidx.media2.MediaMetadata2?);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> setRepeatMode(int);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> setShuffleMode(int);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> setSurface(android.view.Surface?);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> skipToNextPlaylistItem();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> skipToPlaylistItem(androidx.media2.MediaItem2);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> skipToPreviousPlaylistItem();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> updatePlaylistMetadata(androidx.media2.MediaMetadata2?);
+    field public static final int MEDIA_INFO_AUDIO_NOT_PLAYING = 804; // 0x324
+    field public static final int MEDIA_INFO_BAD_INTERLEAVING = 800; // 0x320
+    field public static final int MEDIA_INFO_BUFFERING_UPDATE = 704; // 0x2c0
+    field public static final int MEDIA_INFO_NOT_SEEKABLE = 801; // 0x321
+    field public static final int MEDIA_INFO_PREPARED = 100; // 0x64
+    field public static final int MEDIA_INFO_VIDEO_NOT_PLAYING = 805; // 0x325
+    field public static final int MEDIA_INFO_VIDEO_RENDERING_START = 3; // 0x3
+    field public static final int MEDIA_INFO_VIDEO_TRACK_LAGGING = 700; // 0x2bc
+    field public static final int PLAYER_ERROR_IO = -1004; // 0xfffffc14
+    field public static final int PLAYER_ERROR_MALFORMED = -1007; // 0xfffffc11
+    field public static final int PLAYER_ERROR_TIMED_OUT = -110; // 0xffffff92
+    field public static final int PLAYER_ERROR_UNKNOWN = 1; // 0x1
+    field public static final int PLAYER_ERROR_UNSUPPORTED = -1010; // 0xfffffc0e
+    field public static final int SEEK_CLOSEST = 3; // 0x3
+    field public static final int SEEK_CLOSEST_SYNC = 2; // 0x2
+    field public static final int SEEK_NEXT_SYNC = 1; // 0x1
+    field public static final int SEEK_PREVIOUS_SYNC = 0; // 0x0
+  }
+
+  public abstract static class MediaPlayer.PlayerCallback extends androidx.media2.SessionPlayer2.PlayerCallback {
+    ctor public MediaPlayer.PlayerCallback();
+    method public void onError(androidx.media2.MediaPlayer, androidx.media2.MediaItem2, int, int);
+    method public void onInfo(androidx.media2.MediaPlayer, androidx.media2.MediaItem2, int, int);
+    method public void onMediaTimeDiscontinuity(androidx.media2.MediaPlayer, androidx.media2.MediaItem2, androidx.media2.MediaTimestamp2);
+    method public void onSubtitleData(androidx.media2.MediaPlayer, androidx.media2.MediaItem2, androidx.media2.SubtitleData2);
+    method public void onTimedMetaDataAvailable(androidx.media2.MediaPlayer, androidx.media2.MediaItem2, androidx.media2.TimedMetaData2);
+    method public void onVideoSizeChanged(androidx.media2.MediaPlayer, androidx.media2.MediaItem2, int, int);
+  }
+
+  public static final class MediaPlayer.TrackInfo {
+    method public android.media.MediaFormat? getFormat();
+    method public String getLanguage();
+    method public int getTrackType();
+    field public static final int MEDIA_TRACK_TYPE_AUDIO = 2; // 0x2
+    field public static final int MEDIA_TRACK_TYPE_METADATA = 5; // 0x5
+    field public static final int MEDIA_TRACK_TYPE_SUBTITLE = 4; // 0x4
+    field public static final int MEDIA_TRACK_TYPE_UNKNOWN = 0; // 0x0
+    field public static final int MEDIA_TRACK_TYPE_VIDEO = 1; // 0x1
   }
 
   public class MediaSession2 implements java.lang.AutoCloseable {
@@ -392,12 +478,14 @@
     ctor public MediaSession2.SessionCallback();
     method public int onCommandRequest(androidx.media2.MediaSession2, androidx.media2.MediaSession2.ControllerInfo, androidx.media2.SessionCommand2);
     method public androidx.media2.SessionCommandGroup2? onConnect(androidx.media2.MediaSession2, androidx.media2.MediaSession2.ControllerInfo);
-    method public androidx.media2.MediaItem2? onCreateMediaItem(androidx.media2.MediaSession2, androidx.media2.MediaSession2.ControllerInfo, androidx.media2.MediaItem2);
+    method public androidx.media2.MediaItem2? onCreateMediaItem(androidx.media2.MediaSession2, androidx.media2.MediaSession2.ControllerInfo, String);
     method public androidx.media2.MediaSession2.SessionResult onCustomCommand(androidx.media2.MediaSession2, androidx.media2.MediaSession2.ControllerInfo, androidx.media2.SessionCommand2, android.os.Bundle?);
     method public void onDisconnected(androidx.media2.MediaSession2, androidx.media2.MediaSession2.ControllerInfo);
     method public int onFastForward(androidx.media2.MediaSession2, androidx.media2.MediaSession2.ControllerInfo);
     method public int onRewind(androidx.media2.MediaSession2, androidx.media2.MediaSession2.ControllerInfo);
     method public int onSetRating(androidx.media2.MediaSession2, androidx.media2.MediaSession2.ControllerInfo, String, androidx.media2.Rating2);
+    method public int onSkipBackward(androidx.media2.MediaSession2, androidx.media2.MediaSession2.ControllerInfo);
+    method public int onSkipForward(androidx.media2.MediaSession2, androidx.media2.MediaSession2.ControllerInfo);
   }
 
   public static class MediaSession2.SessionResult implements androidx.versionedparcelable.VersionedParcelable {
@@ -432,7 +520,7 @@
     ctor public MediaSessionService2();
     method public final void addSession(androidx.media2.MediaSession2);
     method public final java.util.List<androidx.media2.MediaSession2> getSessions();
-    method @CallSuper public android.os.IBinder? onBind(android.content.Intent!);
+    method @CallSuper public android.os.IBinder? onBind(android.content.Intent);
     method public abstract androidx.media2.MediaSession2 onGetSession();
     method public androidx.media2.MediaSessionService2.MediaNotification? onUpdateNotification(androidx.media2.MediaSession2);
     method public final void removeSession(androidx.media2.MediaSession2);
@@ -449,7 +537,7 @@
     method public long getAnchorMediaTimeUs();
     method public long getAnchorSystemNanoTime();
     method public float getMediaClockRate();
-    field public static final androidx.media2.MediaTimestamp2! TIMESTAMP_UNKNOWN;
+    field public static final androidx.media2.MediaTimestamp2 TIMESTAMP_UNKNOWN;
   }
 
   public final class PercentageRating2 implements androidx.media2.Rating2 {
@@ -460,9 +548,9 @@
   }
 
   public final class PlaybackParams2 {
-    method public Integer! getAudioFallbackMode();
-    method public Float! getPitch();
-    method public Float! getSpeed();
+    method public Integer? getAudioFallbackMode();
+    method public Float? getPitch();
+    method public Float? getSpeed();
     field public static final int AUDIO_FALLBACK_MODE_DEFAULT = 0; // 0x0
     field public static final int AUDIO_FALLBACK_MODE_FAIL = 2; // 0x2
     field public static final int AUDIO_FALLBACK_MODE_MUTE = 1; // 0x1
@@ -470,10 +558,10 @@
 
   public static final class PlaybackParams2.Builder {
     ctor public PlaybackParams2.Builder();
-    method public androidx.media2.PlaybackParams2! build();
-    method public androidx.media2.PlaybackParams2.Builder! setAudioFallbackMode(int);
-    method public androidx.media2.PlaybackParams2.Builder! setPitch(float);
-    method public androidx.media2.PlaybackParams2.Builder! setSpeed(float);
+    method public androidx.media2.PlaybackParams2 build();
+    method public androidx.media2.PlaybackParams2.Builder setAudioFallbackMode(int);
+    method public androidx.media2.PlaybackParams2.Builder setPitch(float);
+    method public androidx.media2.PlaybackParams2.Builder setSpeed(float);
   }
 
   public interface Rating2 extends androidx.versionedparcelable.VersionedParcelable {
@@ -500,7 +588,7 @@
     field public static final int COMMAND_CODE_PLAYER_GET_PLAYLIST_METADATA = 10012; // 0x271c
     field public static final int COMMAND_CODE_PLAYER_PAUSE = 10001; // 0x2711
     field public static final int COMMAND_CODE_PLAYER_PLAY = 10000; // 0x2710
-    field public static final int COMMAND_CODE_PLAYER_PREFETCH = 10002; // 0x2712
+    field public static final int COMMAND_CODE_PLAYER_PREPARE = 10002; // 0x2712
     field public static final int COMMAND_CODE_PLAYER_REMOVE_PLAYLIST_ITEM = 10014; // 0x271e
     field public static final int COMMAND_CODE_PLAYER_REPLACE_PLAYLIST_ITEM = 10015; // 0x271f
     field public static final int COMMAND_CODE_PLAYER_SEEK_TO = 10003; // 0x2713
@@ -515,7 +603,9 @@
     field public static final int COMMAND_CODE_PLAYER_UPDATE_LIST_METADATA = 10017; // 0x2721
     field public static final int COMMAND_CODE_SESSION_FAST_FORWARD = 40000; // 0x9c40
     field public static final int COMMAND_CODE_SESSION_REWIND = 40001; // 0x9c41
-    field public static final int COMMAND_CODE_SESSION_SET_RATING = 40008; // 0x9c48
+    field public static final int COMMAND_CODE_SESSION_SET_RATING = 40010; // 0x9c4a
+    field public static final int COMMAND_CODE_SESSION_SKIP_BACKWARD = 40003; // 0x9c43
+    field public static final int COMMAND_CODE_SESSION_SKIP_FORWARD = 40002; // 0x9c42
     field public static final int COMMAND_CODE_VOLUME_ADJUST_VOLUME = 30001; // 0x7531
     field public static final int COMMAND_CODE_VOLUME_SET_VOLUME = 30000; // 0x7530
     field public static final int COMMAND_VERSION_1 = 1; // 0x1
@@ -531,9 +621,9 @@
 
   public static final class SessionCommandGroup2.Builder {
     ctor public SessionCommandGroup2.Builder();
-    ctor public SessionCommandGroup2.Builder(androidx.media2.SessionCommandGroup2!);
+    ctor public SessionCommandGroup2.Builder(androidx.media2.SessionCommandGroup2);
     method public androidx.media2.SessionCommandGroup2.Builder addAllPredefinedCommands(int);
-    method public androidx.media2.SessionCommandGroup2.Builder addCommand(androidx.media2.SessionCommand2!);
+    method public androidx.media2.SessionCommandGroup2.Builder addCommand(androidx.media2.SessionCommand2);
     method public androidx.media2.SessionCommandGroup2.Builder addCommand(int);
     method public androidx.media2.SessionCommandGroup2 build();
     method public androidx.media2.SessionCommandGroup2.Builder removeCommand(androidx.media2.SessionCommand2);
@@ -600,7 +690,7 @@
     method public void onPlaybackCompleted(androidx.media2.SessionPlayer2);
     method public void onPlaybackSpeedChanged(androidx.media2.SessionPlayer2, float);
     method public void onPlayerStateChanged(androidx.media2.SessionPlayer2, int);
-    method public void onPlaylistChanged(androidx.media2.SessionPlayer2, java.util.List<androidx.media2.MediaItem2>!, androidx.media2.MediaMetadata2?);
+    method public void onPlaylistChanged(androidx.media2.SessionPlayer2, java.util.List<androidx.media2.MediaItem2>?, androidx.media2.MediaMetadata2?);
     method public void onPlaylistMetadataChanged(androidx.media2.SessionPlayer2, androidx.media2.MediaMetadata2?);
     method public void onRepeatModeChanged(androidx.media2.SessionPlayer2, int);
     method public void onSeekCompleted(androidx.media2.SessionPlayer2, long);
@@ -673,143 +763,11 @@
   public static final class UriMediaItem2.Builder {
     ctor public UriMediaItem2.Builder(android.content.Context, android.net.Uri);
     ctor public UriMediaItem2.Builder(android.content.Context, android.net.Uri, java.util.Map<java.lang.String,java.lang.String>?, java.util.List<java.net.HttpCookie>?);
-    method public androidx.media2.UriMediaItem2! build();
+    method public androidx.media2.UriMediaItem2 build();
     method public androidx.media2.UriMediaItem2.Builder! setEndPosition(long);
-    method public androidx.media2.UriMediaItem2.Builder! setMediaId(String!);
     method public androidx.media2.UriMediaItem2.Builder! setMetadata(androidx.media2.MediaMetadata2!);
     method public androidx.media2.UriMediaItem2.Builder! setStartPosition(long);
   }
 
-  public class XMediaPlayer extends androidx.media2.SessionPlayer2 {
-    ctor public XMediaPlayer(android.content.Context!);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! addPlaylistItem(int, androidx.media2.MediaItem2!);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! attachAuxEffect(int);
-    method public void close() throws java.lang.Exception;
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! deselectTrack(int);
-    method public androidx.media.AudioAttributesCompat! getAudioAttributes();
-    method public int getAudioSessionId();
-    method public long getBufferedPosition();
-    method public int getBufferingState();
-    method public androidx.media2.MediaItem2! getCurrentMediaItem();
-    method public long getCurrentPosition();
-    method public androidx.media2.XMediaPlayer.DrmInfo! getDrmInfo();
-    method public android.media.MediaDrm.KeyRequest getDrmKeyRequest(byte[]?, byte[]?, String?, int, java.util.Map<java.lang.String,java.lang.String>?) throws androidx.media2.XMediaPlayer.NoDrmSchemeException;
-    method public String getDrmPropertyString(String) throws androidx.media2.XMediaPlayer.NoDrmSchemeException;
-    method public long getDuration();
-    method public float getMaxPlayerVolume();
-    method public androidx.media2.PlaybackParams2! getPlaybackParams();
-    method public float getPlaybackSpeed();
-    method public int getPlayerState();
-    method public float getPlayerVolume();
-    method public java.util.List<androidx.media2.MediaItem2>! getPlaylist();
-    method public androidx.media2.MediaMetadata2! getPlaylistMetadata();
-    method public int getRepeatMode();
-    method public int getSelectedTrack(int);
-    method public int getShuffleMode();
-    method public androidx.media2.MediaTimestamp2? getTimestamp();
-    method public java.util.List<androidx.media2.XMediaPlayer.TrackInfo>! getTrackInfo();
-    method public int getVideoHeight();
-    method public int getVideoWidth();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! pause();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! play();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! prepare();
-    method public androidx.concurrent.futures.ResolvableFuture<androidx.media2.XMediaPlayer.DrmResult>! prepareDrm(java.util.UUID);
-    method public byte[]! provideDrmKeyResponse(byte[]?, byte[]) throws android.media.DeniedByServerException, androidx.media2.XMediaPlayer.NoDrmSchemeException;
-    method public void releaseDrm() throws androidx.media2.XMediaPlayer.NoDrmSchemeException;
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! removePlaylistItem(androidx.media2.MediaItem2!);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! replacePlaylistItem(int, androidx.media2.MediaItem2!);
-    method public void reset();
-    method public void restoreDrmKeys(byte[]) throws androidx.media2.XMediaPlayer.NoDrmSchemeException;
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! seekTo(long);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! seekTo(long, int);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! selectTrack(int);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! setAudioAttributes(androidx.media.AudioAttributesCompat!);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! setAudioSessionId(int);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! setAuxEffectSendLevel(float);
-    method public void setDrmPropertyString(String, String) throws androidx.media2.XMediaPlayer.NoDrmSchemeException;
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! setMediaItem(androidx.media2.MediaItem2);
-    method public void setOnDrmConfigHelper(androidx.media2.XMediaPlayer.OnDrmConfigHelper!);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! setPlaybackParams(androidx.media2.PlaybackParams2);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! setPlaybackSpeed(float);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! setPlayerVolume(float);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! setPlaylist(java.util.List<androidx.media2.MediaItem2>, androidx.media2.MediaMetadata2?);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! setRepeatMode(int);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! setShuffleMode(int);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! setSurface(android.view.Surface!);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! skipToNextPlaylistItem();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! skipToPlaylistItem(androidx.media2.MediaItem2!);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! skipToPreviousPlaylistItem();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! updatePlaylistMetadata(androidx.media2.MediaMetadata2!);
-    field public static final int MEDIA_INFO_AUDIO_NOT_PLAYING = 804; // 0x324
-    field public static final int MEDIA_INFO_BAD_INTERLEAVING = 800; // 0x320
-    field public static final int MEDIA_INFO_BUFFERING_UPDATE = 704; // 0x2c0
-    field public static final int MEDIA_INFO_NOT_SEEKABLE = 801; // 0x321
-    field public static final int MEDIA_INFO_PREFETCHED = 100; // 0x64
-    field public static final int MEDIA_INFO_VIDEO_NOT_PLAYING = 805; // 0x325
-    field public static final int MEDIA_INFO_VIDEO_RENDERING_START = 3; // 0x3
-    field public static final int MEDIA_INFO_VIDEO_TRACK_LAGGING = 700; // 0x2bc
-    field public static final int PLAYER_ERROR_IO = -1004; // 0xfffffc14
-    field public static final int PLAYER_ERROR_MALFORMED = -1007; // 0xfffffc11
-    field public static final int PLAYER_ERROR_TIMED_OUT = -110; // 0xffffff92
-    field public static final int PLAYER_ERROR_UNKNOWN = 1; // 0x1
-    field public static final int PLAYER_ERROR_UNSUPPORTED = -1010; // 0xfffffc0e
-    field public static final int SEEK_CLOSEST = 3; // 0x3
-    field public static final int SEEK_CLOSEST_SYNC = 2; // 0x2
-    field public static final int SEEK_NEXT_SYNC = 1; // 0x1
-    field public static final int SEEK_PREVIOUS_SYNC = 0; // 0x0
-  }
-
-  public static final class XMediaPlayer.DrmInfo {
-    method public java.util.Map<java.util.UUID,byte[]>! getPssh();
-    method public java.util.List<java.util.UUID>! getSupportedSchemes();
-  }
-
-  public static class XMediaPlayer.DrmResult extends androidx.media2.SessionPlayer2.PlayerResult {
-    ctor public XMediaPlayer.DrmResult(int, androidx.media2.MediaItem2?);
-    field public static final int RESULT_CODE_BAD_VALUE = -3; // 0xfffffffd
-    field public static final int RESULT_CODE_INVALID_STATE = -2; // 0xfffffffe
-    field public static final int RESULT_CODE_IO_ERROR = -5; // 0xfffffffb
-    field public static final int RESULT_CODE_NOT_SUPPORTED = -6; // 0xfffffffa
-    field public static final int RESULT_CODE_PERMISSION_DENIED = -4; // 0xfffffffc
-    field public static final int RESULT_CODE_PREPARATION_ERROR = -1003; // 0xfffffc15
-    field public static final int RESULT_CODE_PROVISIONING_NETWORK_ERROR = -1001; // 0xfffffc17
-    field public static final int RESULT_CODE_PROVISIONING_SERVER_ERROR = -1002; // 0xfffffc16
-    field public static final int RESULT_CODE_RESOURCE_BUSY = -1005; // 0xfffffc13
-    field public static final int RESULT_CODE_SKIPPED = 1; // 0x1
-    field public static final int RESULT_CODE_SUCCESS = 0; // 0x0
-    field public static final int RESULT_CODE_UNKNOWN_ERROR = -1; // 0xffffffff
-    field public static final int RESULT_CODE_UNSUPPORTED_SCHEME = -1004; // 0xfffffc14
-  }
-
-  public static class XMediaPlayer.NoDrmSchemeException extends android.media.MediaDrmException {
-    ctor public XMediaPlayer.NoDrmSchemeException(String!);
-  }
-
-  public static interface XMediaPlayer.OnDrmConfigHelper {
-    method public void onDrmConfig(androidx.media2.XMediaPlayer!, androidx.media2.MediaItem2!);
-  }
-
-  public abstract static class XMediaPlayer.PlayerCallback extends androidx.media2.SessionPlayer2.PlayerCallback {
-    ctor public XMediaPlayer.PlayerCallback();
-    method public void onDrmInfo(androidx.media2.XMediaPlayer!, androidx.media2.MediaItem2!, androidx.media2.XMediaPlayer.DrmInfo!);
-    method public void onError(androidx.media2.XMediaPlayer!, androidx.media2.MediaItem2!, int, int);
-    method public void onInfo(androidx.media2.XMediaPlayer!, androidx.media2.MediaItem2!, int, int);
-    method public void onMediaTimeDiscontinuity(androidx.media2.XMediaPlayer!, androidx.media2.MediaItem2!, androidx.media2.MediaTimestamp2!);
-    method public void onSubtitleData(androidx.media2.XMediaPlayer!, androidx.media2.MediaItem2!, androidx.media2.SubtitleData2);
-    method public void onTimedMetaDataAvailable(androidx.media2.XMediaPlayer!, androidx.media2.MediaItem2!, androidx.media2.TimedMetaData2!);
-    method public void onVideoSizeChanged(androidx.media2.XMediaPlayer!, androidx.media2.MediaItem2!, int, int);
-  }
-
-  public static final class XMediaPlayer.TrackInfo {
-    method public android.media.MediaFormat! getFormat();
-    method public String! getLanguage();
-    method public int getTrackType();
-    field public static final int MEDIA_TRACK_TYPE_AUDIO = 2; // 0x2
-    field public static final int MEDIA_TRACK_TYPE_METADATA = 5; // 0x5
-    field public static final int MEDIA_TRACK_TYPE_SUBTITLE = 4; // 0x4
-    field public static final int MEDIA_TRACK_TYPE_UNKNOWN = 0; // 0x0
-    field public static final int MEDIA_TRACK_TYPE_VIDEO = 1; // 0x1
-  }
-
 }
 
diff --git a/media2/api/current.txt b/media2/api/current.txt
index 8282a80..82d51ca 100644
--- a/media2/api/current.txt
+++ b/media2/api/current.txt
@@ -7,9 +7,8 @@
 
   public static final class CallbackMediaItem2.Builder {
     ctor public CallbackMediaItem2.Builder(androidx.media2.DataSourceCallback2);
-    method public androidx.media2.CallbackMediaItem2! build();
+    method public androidx.media2.CallbackMediaItem2 build();
     method public androidx.media2.CallbackMediaItem2.Builder! setEndPosition(long);
-    method public androidx.media2.CallbackMediaItem2.Builder! setMediaId(String!);
     method public androidx.media2.CallbackMediaItem2.Builder! setMetadata(androidx.media2.MediaMetadata2!);
     method public androidx.media2.CallbackMediaItem2.Builder! setStartPosition(long);
   }
@@ -17,7 +16,7 @@
   public abstract class DataSourceCallback2 implements java.io.Closeable {
     ctor public DataSourceCallback2();
     method public abstract long getSize() throws java.io.IOException;
-    method public abstract int readAt(long, byte[]!, int, int) throws java.io.IOException;
+    method public abstract int readAt(long, byte[], int, int) throws java.io.IOException;
   }
 
   public class FileMediaItem2 extends androidx.media2.MediaItem2 {
@@ -30,9 +29,8 @@
   public static final class FileMediaItem2.Builder {
     ctor public FileMediaItem2.Builder(java.io.FileDescriptor);
     ctor public FileMediaItem2.Builder(java.io.FileDescriptor, long, long);
-    method public androidx.media2.FileMediaItem2! build();
+    method public androidx.media2.FileMediaItem2 build();
     method public androidx.media2.FileMediaItem2.Builder! setEndPosition(long);
-    method public androidx.media2.FileMediaItem2.Builder! setMediaId(String!);
     method public androidx.media2.FileMediaItem2.Builder! setMetadata(androidx.media2.MediaMetadata2!);
     method public androidx.media2.FileMediaItem2.Builder! setStartPosition(long);
   }
@@ -46,24 +44,24 @@
 
   public class MediaBrowser2 extends androidx.media2.MediaController2 {
     ctor public MediaBrowser2(android.content.Context, androidx.media2.SessionToken2, java.util.concurrent.Executor, androidx.media2.MediaBrowser2.BrowserCallback);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaBrowser2.BrowserResult>! getChildren(String, @IntRange(from=0) int, @IntRange(from=1) int, androidx.media2.MediaLibraryService2.LibraryParams?);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaBrowser2.BrowserResult>! getItem(String);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaBrowser2.BrowserResult>! getLibraryRoot(androidx.media2.MediaLibraryService2.LibraryParams?);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaBrowser2.BrowserResult>! getSearchResult(String, @IntRange(from=0) int, @IntRange(from=1) int, androidx.media2.MediaLibraryService2.LibraryParams?);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaBrowser2.BrowserResult>! search(String, androidx.media2.MediaLibraryService2.LibraryParams?);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaBrowser2.BrowserResult>! subscribe(String, androidx.media2.MediaLibraryService2.LibraryParams?);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaBrowser2.BrowserResult>! unsubscribe(String);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaBrowser2.BrowserResult> getChildren(String, @IntRange(from=0) int, @IntRange(from=1) int, androidx.media2.MediaLibraryService2.LibraryParams?);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaBrowser2.BrowserResult> getItem(String);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaBrowser2.BrowserResult> getLibraryRoot(androidx.media2.MediaLibraryService2.LibraryParams?);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaBrowser2.BrowserResult> getSearchResult(String, @IntRange(from=0) int, @IntRange(from=1) int, androidx.media2.MediaLibraryService2.LibraryParams?);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaBrowser2.BrowserResult> search(String, androidx.media2.MediaLibraryService2.LibraryParams?);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaBrowser2.BrowserResult> subscribe(String, androidx.media2.MediaLibraryService2.LibraryParams?);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaBrowser2.BrowserResult> unsubscribe(String);
   }
 
   public static class MediaBrowser2.BrowserCallback extends androidx.media2.MediaController2.ControllerCallback {
     ctor public MediaBrowser2.BrowserCallback();
-    method public void onChildrenChanged(androidx.media2.MediaBrowser2, String, int, androidx.media2.MediaLibraryService2.LibraryParams?);
-    method public void onSearchResultChanged(androidx.media2.MediaBrowser2, String, int, androidx.media2.MediaLibraryService2.LibraryParams?);
+    method public void onChildrenChanged(androidx.media2.MediaBrowser2, String, @IntRange(from=0) int, androidx.media2.MediaLibraryService2.LibraryParams?);
+    method public void onSearchResultChanged(androidx.media2.MediaBrowser2, String, @IntRange(from=0) int, androidx.media2.MediaLibraryService2.LibraryParams?);
   }
 
   public static class MediaBrowser2.BrowserResult extends androidx.versionedparcelable.CustomVersionedParcelable {
     method public long getCompletionTime();
-    method public androidx.media2.MediaLibraryService2.LibraryParams! getLibraryParams();
+    method public androidx.media2.MediaLibraryService2.LibraryParams? getLibraryParams();
     method public androidx.media2.MediaItem2? getMediaItem();
     method public java.util.List<androidx.media2.MediaItem2>? getMediaItems();
     method public int getResultCode();
@@ -88,14 +86,14 @@
   public class MediaController2 implements java.lang.AutoCloseable {
     ctor public MediaController2(android.content.Context, androidx.media2.SessionToken2, java.util.concurrent.Executor, androidx.media2.MediaController2.ControllerCallback);
     ctor public MediaController2(android.content.Context, android.support.v4.media.session.MediaSessionCompat.Token, java.util.concurrent.Executor, androidx.media2.MediaController2.ControllerCallback);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! addPlaylistItem(int, androidx.media2.MediaItem2);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! adjustVolume(int, int);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> addPlaylistItem(@IntRange(from=0) int, String);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> adjustVolume(int, int);
     method public void close();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! fastForward();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> fastForward();
     method public long getBufferedPosition();
     method public int getBufferingState();
     method public androidx.media2.SessionToken2? getConnectedSessionToken();
-    method public androidx.media2.MediaItem2! getCurrentMediaItem();
+    method public androidx.media2.MediaItem2? getCurrentMediaItem();
     method public long getCurrentPosition();
     method public long getDuration();
     method public androidx.media2.MediaController2.PlaybackInfo? getPlaybackInfo();
@@ -107,25 +105,27 @@
     method public android.app.PendingIntent? getSessionActivity();
     method public int getShuffleMode();
     method public boolean isConnected();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! pause();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! play();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! prefetch();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! removePlaylistItem(androidx.media2.MediaItem2);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! replacePlaylistItem(int, androidx.media2.MediaItem2);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! rewind();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! seekTo(long);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! sendCustomCommand(androidx.media2.SessionCommand2, android.os.Bundle?);
-    method public void setMediaItem(androidx.media2.MediaItem2);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! setPlaybackSpeed(float);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! setPlaylist(java.util.List<androidx.media2.MediaItem2>, androidx.media2.MediaMetadata2?);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! setRating(String, androidx.media2.Rating2);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! setRepeatMode(int);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! setShuffleMode(int);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! setVolumeTo(int, int);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! skipToNextPlaylistItem();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! skipToPlaylistItem(androidx.media2.MediaItem2);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! skipToPreviousPlaylistItem();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult>! updatePlaylistMetadata(androidx.media2.MediaMetadata2?);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> pause();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> play();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> prepare();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> removePlaylistItem(@IntRange(from=0) int);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> replacePlaylistItem(@IntRange(from=0) int, String);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> rewind();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> seekTo(long);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> sendCustomCommand(androidx.media2.SessionCommand2, android.os.Bundle?);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> setMediaItem(String);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> setPlaybackSpeed(float);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> setPlaylist(java.util.List<java.lang.String>, androidx.media2.MediaMetadata2?);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> setRating(String, androidx.media2.Rating2);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> setRepeatMode(int);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> setShuffleMode(int);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> setVolumeTo(int, int);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> skipBackward();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> skipForward();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> skipToNextPlaylistItem();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> skipToPlaylistItem(@IntRange(from=0) int);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> skipToPreviousPlaylistItem();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaController2.ControllerResult> updatePlaylistMetadata(androidx.media2.MediaMetadata2?);
   }
 
   public abstract static class MediaController2.ControllerCallback {
@@ -140,7 +140,7 @@
     method public void onPlaybackInfoChanged(androidx.media2.MediaController2, androidx.media2.MediaController2.PlaybackInfo);
     method public void onPlaybackSpeedChanged(androidx.media2.MediaController2, float);
     method public void onPlayerStateChanged(androidx.media2.MediaController2, int);
-    method public void onPlaylistChanged(androidx.media2.MediaController2, java.util.List<androidx.media2.MediaItem2>, androidx.media2.MediaMetadata2?);
+    method public void onPlaylistChanged(androidx.media2.MediaController2, java.util.List<androidx.media2.MediaItem2>?, androidx.media2.MediaMetadata2?);
     method public void onPlaylistMetadataChanged(androidx.media2.MediaController2, androidx.media2.MediaMetadata2?);
     method public void onRepeatModeChanged(androidx.media2.MediaController2, int);
     method public void onSeekCompleted(androidx.media2.MediaController2, long);
@@ -173,7 +173,7 @@
   }
 
   public static final class MediaController2.PlaybackInfo implements androidx.versionedparcelable.VersionedParcelable {
-    method public androidx.media.AudioAttributesCompat! getAudioAttributes();
+    method public androidx.media.AudioAttributesCompat? getAudioAttributes();
     method public int getControlType();
     method public int getCurrentVolume();
     method public int getMaxVolume();
@@ -184,23 +184,16 @@
 
   public class MediaItem2 implements androidx.versionedparcelable.VersionedParcelable {
     method public long getEndPosition();
-    method public int getFlags();
-    method public String? getMediaId();
     method public androidx.media2.MediaMetadata2? getMetadata();
     method public long getStartPosition();
-    method public boolean isBrowsable();
-    method public boolean isPlayable();
     method public void setMetadata(androidx.media2.MediaMetadata2?);
-    field public static final int FLAG_BROWSABLE = 1; // 0x1
-    field public static final int FLAG_PLAYABLE = 2; // 0x2
     field public static final long POSITION_UNKNOWN = 576460752303423487L; // 0x7ffffffffffffffL
   }
 
   public static class MediaItem2.Builder {
-    ctor public MediaItem2.Builder(int);
+    ctor public MediaItem2.Builder();
     method public androidx.media2.MediaItem2! build();
     method public androidx.media2.MediaItem2.BuilderBase! setEndPosition(long);
-    method public androidx.media2.MediaItem2.BuilderBase! setMediaId(String!);
     method public androidx.media2.MediaItem2.BuilderBase! setMetadata(androidx.media2.MediaMetadata2!);
     method public androidx.media2.MediaItem2.BuilderBase! setStartPosition(long);
   }
@@ -220,11 +213,11 @@
 
   public static final class MediaLibraryService2.LibraryParams.Builder {
     ctor public MediaLibraryService2.LibraryParams.Builder();
-    method public androidx.media2.MediaLibraryService2.LibraryParams! build();
-    method public androidx.media2.MediaLibraryService2.LibraryParams.Builder! setExtras(android.os.Bundle?);
-    method public androidx.media2.MediaLibraryService2.LibraryParams.Builder! setOffline(boolean);
-    method public androidx.media2.MediaLibraryService2.LibraryParams.Builder! setRecent(boolean);
-    method public androidx.media2.MediaLibraryService2.LibraryParams.Builder! setSuggested(boolean);
+    method public androidx.media2.MediaLibraryService2.LibraryParams build();
+    method public androidx.media2.MediaLibraryService2.LibraryParams.Builder setExtras(android.os.Bundle?);
+    method public androidx.media2.MediaLibraryService2.LibraryParams.Builder setOffline(boolean);
+    method public androidx.media2.MediaLibraryService2.LibraryParams.Builder setRecent(boolean);
+    method public androidx.media2.MediaLibraryService2.LibraryParams.Builder setSuggested(boolean);
   }
 
   public static class MediaLibraryService2.LibraryResult extends androidx.versionedparcelable.CustomVersionedParcelable {
@@ -250,9 +243,9 @@
   }
 
   public static final class MediaLibraryService2.MediaLibrarySession extends androidx.media2.MediaSession2 {
-    method public void notifyChildrenChanged(androidx.media2.MediaSession2.ControllerInfo, String, int, androidx.media2.MediaLibraryService2.LibraryParams?);
+    method public void notifyChildrenChanged(androidx.media2.MediaSession2.ControllerInfo, String, @IntRange(from=0) int, androidx.media2.MediaLibraryService2.LibraryParams?);
     method public void notifyChildrenChanged(String, int, androidx.media2.MediaLibraryService2.LibraryParams?);
-    method public void notifySearchResultChanged(androidx.media2.MediaSession2.ControllerInfo, String, int, androidx.media2.MediaLibraryService2.LibraryParams?);
+    method public void notifySearchResultChanged(androidx.media2.MediaSession2.ControllerInfo, String, @IntRange(from=0) int, androidx.media2.MediaLibraryService2.LibraryParams?);
   }
 
   public static final class MediaLibraryService2.MediaLibrarySession.Builder {
@@ -264,10 +257,10 @@
 
   public static class MediaLibraryService2.MediaLibrarySession.MediaLibrarySessionCallback extends androidx.media2.MediaSession2.SessionCallback {
     ctor public MediaLibraryService2.MediaLibrarySession.MediaLibrarySessionCallback();
-    method public androidx.media2.MediaLibraryService2.LibraryResult onGetChildren(androidx.media2.MediaLibraryService2.MediaLibrarySession, androidx.media2.MediaSession2.ControllerInfo, String, int, int, androidx.media2.MediaLibraryService2.LibraryParams?);
+    method public androidx.media2.MediaLibraryService2.LibraryResult onGetChildren(androidx.media2.MediaLibraryService2.MediaLibrarySession, androidx.media2.MediaSession2.ControllerInfo, String, @IntRange(from=0) int, @IntRange(from=1) int, androidx.media2.MediaLibraryService2.LibraryParams?);
     method public androidx.media2.MediaLibraryService2.LibraryResult onGetItem(androidx.media2.MediaLibraryService2.MediaLibrarySession, androidx.media2.MediaSession2.ControllerInfo, String);
     method public androidx.media2.MediaLibraryService2.LibraryResult onGetLibraryRoot(androidx.media2.MediaLibraryService2.MediaLibrarySession, androidx.media2.MediaSession2.ControllerInfo, androidx.media2.MediaLibraryService2.LibraryParams?);
-    method public androidx.media2.MediaLibraryService2.LibraryResult onGetSearchResult(androidx.media2.MediaLibraryService2.MediaLibrarySession, androidx.media2.MediaSession2.ControllerInfo, String, int, int, androidx.media2.MediaLibraryService2.LibraryParams?);
+    method public androidx.media2.MediaLibraryService2.LibraryResult onGetSearchResult(androidx.media2.MediaLibraryService2.MediaLibrarySession, androidx.media2.MediaSession2.ControllerInfo, String, @IntRange(from=0) int, @IntRange(from=1) int, androidx.media2.MediaLibraryService2.LibraryParams?);
     method public int onSearch(androidx.media2.MediaLibraryService2.MediaLibrarySession, androidx.media2.MediaSession2.ControllerInfo, String, androidx.media2.MediaLibraryService2.LibraryParams?);
     method public int onSubscribe(androidx.media2.MediaLibraryService2.MediaLibrarySession, androidx.media2.MediaSession2.ControllerInfo, String, androidx.media2.MediaLibraryService2.LibraryParams?);
     method public int onUnsubscribe(androidx.media2.MediaLibraryService2.MediaLibrarySession, androidx.media2.MediaSession2.ControllerInfo, String);
@@ -287,13 +280,14 @@
     method public java.util.Set<java.lang.String> keySet();
     method public int size();
     method public android.os.Bundle toBundle();
-    field public static final long BT_FOLDER_TYPE_ALBUMS = 2L; // 0x2L
-    field public static final long BT_FOLDER_TYPE_ARTISTS = 3L; // 0x3L
-    field public static final long BT_FOLDER_TYPE_GENRES = 4L; // 0x4L
-    field public static final long BT_FOLDER_TYPE_MIXED = 0L; // 0x0L
-    field public static final long BT_FOLDER_TYPE_PLAYLISTS = 5L; // 0x5L
-    field public static final long BT_FOLDER_TYPE_TITLES = 1L; // 0x1L
-    field public static final long BT_FOLDER_TYPE_YEARS = 6L; // 0x6L
+    field public static final long BROWSABLE_TYPE_ALBUMS = 2L; // 0x2L
+    field public static final long BROWSABLE_TYPE_ARTISTS = 3L; // 0x3L
+    field public static final long BROWSABLE_TYPE_GENRES = 4L; // 0x4L
+    field public static final long BROWSABLE_TYPE_MIXED = 0L; // 0x0L
+    field public static final long BROWSABLE_TYPE_NONE = -1L; // 0xffffffffffffffffL
+    field public static final long BROWSABLE_TYPE_PLAYLISTS = 5L; // 0x5L
+    field public static final long BROWSABLE_TYPE_TITLES = 1L; // 0x1L
+    field public static final long BROWSABLE_TYPE_YEARS = 6L; // 0x6L
     field public static final String METADATA_KEY_ADVERTISEMENT = "android.media.metadata.ADVERTISEMENT";
     field public static final String METADATA_KEY_ALBUM = "android.media.metadata.ALBUM";
     field public static final String METADATA_KEY_ALBUM_ART = "android.media.metadata.ALBUM_ART";
@@ -303,7 +297,7 @@
     field public static final String METADATA_KEY_ARTIST = "android.media.metadata.ARTIST";
     field public static final String METADATA_KEY_ART_URI = "android.media.metadata.ART_URI";
     field public static final String METADATA_KEY_AUTHOR = "android.media.metadata.AUTHOR";
-    field public static final String METADATA_KEY_BT_FOLDER_TYPE = "android.media.metadata.BT_FOLDER_TYPE";
+    field public static final String METADATA_KEY_BROWSABLE = "android.media.metadata.BT_FOLDER_TYPE";
     field public static final String METADATA_KEY_COMPILATION = "android.media.metadata.COMPILATION";
     field public static final String METADATA_KEY_COMPOSER = "android.media.metadata.COMPOSER";
     field public static final String METADATA_KEY_DATE = "android.media.metadata.DATE";
@@ -320,6 +314,7 @@
     field public static final String METADATA_KEY_MEDIA_ID = "android.media.metadata.MEDIA_ID";
     field public static final String METADATA_KEY_MEDIA_URI = "android.media.metadata.MEDIA_URI";
     field public static final String METADATA_KEY_NUM_TRACKS = "android.media.metadata.NUM_TRACKS";
+    field public static final String METADATA_KEY_PLAYABLE = "android.media.metadata.playable";
     field public static final String METADATA_KEY_RATING = "android.media.metadata.RATING";
     field public static final String METADATA_KEY_TITLE = "android.media.metadata.TITLE";
     field public static final String METADATA_KEY_TRACK_NUMBER = "android.media.metadata.TRACK_NUMBER";
@@ -341,7 +336,98 @@
     method public androidx.media2.MediaMetadata2.Builder putRating(String, androidx.media2.Rating2?);
     method public androidx.media2.MediaMetadata2.Builder putString(String, String?);
     method public androidx.media2.MediaMetadata2.Builder putText(String, CharSequence?);
-    method public androidx.media2.MediaMetadata2.Builder! setExtras(android.os.Bundle?);
+    method public androidx.media2.MediaMetadata2.Builder setExtras(android.os.Bundle?);
+  }
+
+  public class MediaPlayer extends androidx.media2.SessionPlayer2 {
+    ctor public MediaPlayer(android.content.Context);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> addPlaylistItem(int, androidx.media2.MediaItem2);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> attachAuxEffect(int);
+    method public void close() throws java.lang.Exception;
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> deselectTrack(int);
+    method public androidx.media.AudioAttributesCompat? getAudioAttributes();
+    method public int getAudioSessionId();
+    method public long getBufferedPosition();
+    method public int getBufferingState();
+    method public androidx.media2.MediaItem2? getCurrentMediaItem();
+    method public long getCurrentPosition();
+    method public long getDuration();
+    method public float getMaxPlayerVolume();
+    method public androidx.media2.PlaybackParams2 getPlaybackParams();
+    method public float getPlaybackSpeed();
+    method public int getPlayerState();
+    method public float getPlayerVolume();
+    method public java.util.List<androidx.media2.MediaItem2>? getPlaylist();
+    method public androidx.media2.MediaMetadata2? getPlaylistMetadata();
+    method public int getRepeatMode();
+    method public int getSelectedTrack(int);
+    method public int getShuffleMode();
+    method public androidx.media2.MediaTimestamp2? getTimestamp();
+    method public java.util.List<androidx.media2.MediaPlayer.TrackInfo> getTrackInfo();
+    method public int getVideoHeight();
+    method public int getVideoWidth();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> pause();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> play();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> prepare();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> removePlaylistItem(androidx.media2.MediaItem2);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> replacePlaylistItem(int, androidx.media2.MediaItem2);
+    method public void reset();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> seekTo(long);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> seekTo(long, int);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> selectTrack(int);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> setAudioAttributes(androidx.media.AudioAttributesCompat);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> setAudioSessionId(int);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> setAuxEffectSendLevel(float);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> setMediaItem(androidx.media2.MediaItem2);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> setPlaybackParams(androidx.media2.PlaybackParams2);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> setPlaybackSpeed(float);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> setPlayerVolume(float);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> setPlaylist(java.util.List<androidx.media2.MediaItem2>, androidx.media2.MediaMetadata2?);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> setRepeatMode(int);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> setShuffleMode(int);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> setSurface(android.view.Surface?);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> skipToNextPlaylistItem();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> skipToPlaylistItem(androidx.media2.MediaItem2);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> skipToPreviousPlaylistItem();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult> updatePlaylistMetadata(androidx.media2.MediaMetadata2?);
+    field public static final int MEDIA_INFO_AUDIO_NOT_PLAYING = 804; // 0x324
+    field public static final int MEDIA_INFO_BAD_INTERLEAVING = 800; // 0x320
+    field public static final int MEDIA_INFO_BUFFERING_UPDATE = 704; // 0x2c0
+    field public static final int MEDIA_INFO_NOT_SEEKABLE = 801; // 0x321
+    field public static final int MEDIA_INFO_PREPARED = 100; // 0x64
+    field public static final int MEDIA_INFO_VIDEO_NOT_PLAYING = 805; // 0x325
+    field public static final int MEDIA_INFO_VIDEO_RENDERING_START = 3; // 0x3
+    field public static final int MEDIA_INFO_VIDEO_TRACK_LAGGING = 700; // 0x2bc
+    field public static final int PLAYER_ERROR_IO = -1004; // 0xfffffc14
+    field public static final int PLAYER_ERROR_MALFORMED = -1007; // 0xfffffc11
+    field public static final int PLAYER_ERROR_TIMED_OUT = -110; // 0xffffff92
+    field public static final int PLAYER_ERROR_UNKNOWN = 1; // 0x1
+    field public static final int PLAYER_ERROR_UNSUPPORTED = -1010; // 0xfffffc0e
+    field public static final int SEEK_CLOSEST = 3; // 0x3
+    field public static final int SEEK_CLOSEST_SYNC = 2; // 0x2
+    field public static final int SEEK_NEXT_SYNC = 1; // 0x1
+    field public static final int SEEK_PREVIOUS_SYNC = 0; // 0x0
+  }
+
+  public abstract static class MediaPlayer.PlayerCallback extends androidx.media2.SessionPlayer2.PlayerCallback {
+    ctor public MediaPlayer.PlayerCallback();
+    method public void onError(androidx.media2.MediaPlayer, androidx.media2.MediaItem2, int, int);
+    method public void onInfo(androidx.media2.MediaPlayer, androidx.media2.MediaItem2, int, int);
+    method public void onMediaTimeDiscontinuity(androidx.media2.MediaPlayer, androidx.media2.MediaItem2, androidx.media2.MediaTimestamp2);
+    method public void onSubtitleData(androidx.media2.MediaPlayer, androidx.media2.MediaItem2, androidx.media2.SubtitleData2);
+    method public void onTimedMetaDataAvailable(androidx.media2.MediaPlayer, androidx.media2.MediaItem2, androidx.media2.TimedMetaData2);
+    method public void onVideoSizeChanged(androidx.media2.MediaPlayer, androidx.media2.MediaItem2, int, int);
+  }
+
+  public static final class MediaPlayer.TrackInfo {
+    method public android.media.MediaFormat? getFormat();
+    method public String getLanguage();
+    method public int getTrackType();
+    field public static final int MEDIA_TRACK_TYPE_AUDIO = 2; // 0x2
+    field public static final int MEDIA_TRACK_TYPE_METADATA = 5; // 0x5
+    field public static final int MEDIA_TRACK_TYPE_SUBTITLE = 4; // 0x4
+    field public static final int MEDIA_TRACK_TYPE_UNKNOWN = 0; // 0x0
+    field public static final int MEDIA_TRACK_TYPE_VIDEO = 1; // 0x1
   }
 
   public class MediaSession2 implements java.lang.AutoCloseable {
@@ -392,12 +478,14 @@
     ctor public MediaSession2.SessionCallback();
     method public int onCommandRequest(androidx.media2.MediaSession2, androidx.media2.MediaSession2.ControllerInfo, androidx.media2.SessionCommand2);
     method public androidx.media2.SessionCommandGroup2? onConnect(androidx.media2.MediaSession2, androidx.media2.MediaSession2.ControllerInfo);
-    method public androidx.media2.MediaItem2? onCreateMediaItem(androidx.media2.MediaSession2, androidx.media2.MediaSession2.ControllerInfo, androidx.media2.MediaItem2);
+    method public androidx.media2.MediaItem2? onCreateMediaItem(androidx.media2.MediaSession2, androidx.media2.MediaSession2.ControllerInfo, String);
     method public androidx.media2.MediaSession2.SessionResult onCustomCommand(androidx.media2.MediaSession2, androidx.media2.MediaSession2.ControllerInfo, androidx.media2.SessionCommand2, android.os.Bundle?);
     method public void onDisconnected(androidx.media2.MediaSession2, androidx.media2.MediaSession2.ControllerInfo);
     method public int onFastForward(androidx.media2.MediaSession2, androidx.media2.MediaSession2.ControllerInfo);
     method public int onRewind(androidx.media2.MediaSession2, androidx.media2.MediaSession2.ControllerInfo);
     method public int onSetRating(androidx.media2.MediaSession2, androidx.media2.MediaSession2.ControllerInfo, String, androidx.media2.Rating2);
+    method public int onSkipBackward(androidx.media2.MediaSession2, androidx.media2.MediaSession2.ControllerInfo);
+    method public int onSkipForward(androidx.media2.MediaSession2, androidx.media2.MediaSession2.ControllerInfo);
   }
 
   public static class MediaSession2.SessionResult implements androidx.versionedparcelable.VersionedParcelable {
@@ -432,7 +520,7 @@
     ctor public MediaSessionService2();
     method public final void addSession(androidx.media2.MediaSession2);
     method public final java.util.List<androidx.media2.MediaSession2> getSessions();
-    method @CallSuper public android.os.IBinder? onBind(android.content.Intent!);
+    method @CallSuper public android.os.IBinder? onBind(android.content.Intent);
     method public abstract androidx.media2.MediaSession2 onGetSession();
     method public androidx.media2.MediaSessionService2.MediaNotification? onUpdateNotification(androidx.media2.MediaSession2);
     method public final void removeSession(androidx.media2.MediaSession2);
@@ -449,7 +537,7 @@
     method public long getAnchorMediaTimeUs();
     method public long getAnchorSystemNanoTime();
     method public float getMediaClockRate();
-    field public static final androidx.media2.MediaTimestamp2! TIMESTAMP_UNKNOWN;
+    field public static final androidx.media2.MediaTimestamp2 TIMESTAMP_UNKNOWN;
   }
 
   public final class PercentageRating2 implements androidx.media2.Rating2 {
@@ -460,9 +548,9 @@
   }
 
   public final class PlaybackParams2 {
-    method public Integer! getAudioFallbackMode();
-    method public Float! getPitch();
-    method public Float! getSpeed();
+    method public Integer? getAudioFallbackMode();
+    method public Float? getPitch();
+    method public Float? getSpeed();
     field public static final int AUDIO_FALLBACK_MODE_DEFAULT = 0; // 0x0
     field public static final int AUDIO_FALLBACK_MODE_FAIL = 2; // 0x2
     field public static final int AUDIO_FALLBACK_MODE_MUTE = 1; // 0x1
@@ -470,10 +558,10 @@
 
   public static final class PlaybackParams2.Builder {
     ctor public PlaybackParams2.Builder();
-    method public androidx.media2.PlaybackParams2! build();
-    method public androidx.media2.PlaybackParams2.Builder! setAudioFallbackMode(int);
-    method public androidx.media2.PlaybackParams2.Builder! setPitch(float);
-    method public androidx.media2.PlaybackParams2.Builder! setSpeed(float);
+    method public androidx.media2.PlaybackParams2 build();
+    method public androidx.media2.PlaybackParams2.Builder setAudioFallbackMode(int);
+    method public androidx.media2.PlaybackParams2.Builder setPitch(float);
+    method public androidx.media2.PlaybackParams2.Builder setSpeed(float);
   }
 
   public interface Rating2 extends androidx.versionedparcelable.VersionedParcelable {
@@ -500,7 +588,7 @@
     field public static final int COMMAND_CODE_PLAYER_GET_PLAYLIST_METADATA = 10012; // 0x271c
     field public static final int COMMAND_CODE_PLAYER_PAUSE = 10001; // 0x2711
     field public static final int COMMAND_CODE_PLAYER_PLAY = 10000; // 0x2710
-    field public static final int COMMAND_CODE_PLAYER_PREFETCH = 10002; // 0x2712
+    field public static final int COMMAND_CODE_PLAYER_PREPARE = 10002; // 0x2712
     field public static final int COMMAND_CODE_PLAYER_REMOVE_PLAYLIST_ITEM = 10014; // 0x271e
     field public static final int COMMAND_CODE_PLAYER_REPLACE_PLAYLIST_ITEM = 10015; // 0x271f
     field public static final int COMMAND_CODE_PLAYER_SEEK_TO = 10003; // 0x2713
@@ -515,7 +603,9 @@
     field public static final int COMMAND_CODE_PLAYER_UPDATE_LIST_METADATA = 10017; // 0x2721
     field public static final int COMMAND_CODE_SESSION_FAST_FORWARD = 40000; // 0x9c40
     field public static final int COMMAND_CODE_SESSION_REWIND = 40001; // 0x9c41
-    field public static final int COMMAND_CODE_SESSION_SET_RATING = 40008; // 0x9c48
+    field public static final int COMMAND_CODE_SESSION_SET_RATING = 40010; // 0x9c4a
+    field public static final int COMMAND_CODE_SESSION_SKIP_BACKWARD = 40003; // 0x9c43
+    field public static final int COMMAND_CODE_SESSION_SKIP_FORWARD = 40002; // 0x9c42
     field public static final int COMMAND_CODE_VOLUME_ADJUST_VOLUME = 30001; // 0x7531
     field public static final int COMMAND_CODE_VOLUME_SET_VOLUME = 30000; // 0x7530
     field public static final int COMMAND_VERSION_1 = 1; // 0x1
@@ -531,9 +621,9 @@
 
   public static final class SessionCommandGroup2.Builder {
     ctor public SessionCommandGroup2.Builder();
-    ctor public SessionCommandGroup2.Builder(androidx.media2.SessionCommandGroup2!);
+    ctor public SessionCommandGroup2.Builder(androidx.media2.SessionCommandGroup2);
     method public androidx.media2.SessionCommandGroup2.Builder addAllPredefinedCommands(int);
-    method public androidx.media2.SessionCommandGroup2.Builder addCommand(androidx.media2.SessionCommand2!);
+    method public androidx.media2.SessionCommandGroup2.Builder addCommand(androidx.media2.SessionCommand2);
     method public androidx.media2.SessionCommandGroup2.Builder addCommand(int);
     method public androidx.media2.SessionCommandGroup2 build();
     method public androidx.media2.SessionCommandGroup2.Builder removeCommand(androidx.media2.SessionCommand2);
@@ -600,7 +690,7 @@
     method public void onPlaybackCompleted(androidx.media2.SessionPlayer2);
     method public void onPlaybackSpeedChanged(androidx.media2.SessionPlayer2, float);
     method public void onPlayerStateChanged(androidx.media2.SessionPlayer2, int);
-    method public void onPlaylistChanged(androidx.media2.SessionPlayer2, java.util.List<androidx.media2.MediaItem2>!, androidx.media2.MediaMetadata2?);
+    method public void onPlaylistChanged(androidx.media2.SessionPlayer2, java.util.List<androidx.media2.MediaItem2>?, androidx.media2.MediaMetadata2?);
     method public void onPlaylistMetadataChanged(androidx.media2.SessionPlayer2, androidx.media2.MediaMetadata2?);
     method public void onRepeatModeChanged(androidx.media2.SessionPlayer2, int);
     method public void onSeekCompleted(androidx.media2.SessionPlayer2, long);
@@ -673,143 +763,11 @@
   public static final class UriMediaItem2.Builder {
     ctor public UriMediaItem2.Builder(android.content.Context, android.net.Uri);
     ctor public UriMediaItem2.Builder(android.content.Context, android.net.Uri, java.util.Map<java.lang.String,java.lang.String>?, java.util.List<java.net.HttpCookie>?);
-    method public androidx.media2.UriMediaItem2! build();
+    method public androidx.media2.UriMediaItem2 build();
     method public androidx.media2.UriMediaItem2.Builder! setEndPosition(long);
-    method public androidx.media2.UriMediaItem2.Builder! setMediaId(String!);
     method public androidx.media2.UriMediaItem2.Builder! setMetadata(androidx.media2.MediaMetadata2!);
     method public androidx.media2.UriMediaItem2.Builder! setStartPosition(long);
   }
 
-  public class XMediaPlayer extends androidx.media2.SessionPlayer2 {
-    ctor public XMediaPlayer(android.content.Context!);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! addPlaylistItem(int, androidx.media2.MediaItem2!);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! attachAuxEffect(int);
-    method public void close() throws java.lang.Exception;
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! deselectTrack(int);
-    method public androidx.media.AudioAttributesCompat! getAudioAttributes();
-    method public int getAudioSessionId();
-    method public long getBufferedPosition();
-    method public int getBufferingState();
-    method public androidx.media2.MediaItem2! getCurrentMediaItem();
-    method public long getCurrentPosition();
-    method public androidx.media2.XMediaPlayer.DrmInfo! getDrmInfo();
-    method public android.media.MediaDrm.KeyRequest getDrmKeyRequest(byte[]?, byte[]?, String?, int, java.util.Map<java.lang.String,java.lang.String>?) throws androidx.media2.XMediaPlayer.NoDrmSchemeException;
-    method public String getDrmPropertyString(String) throws androidx.media2.XMediaPlayer.NoDrmSchemeException;
-    method public long getDuration();
-    method public float getMaxPlayerVolume();
-    method public androidx.media2.PlaybackParams2! getPlaybackParams();
-    method public float getPlaybackSpeed();
-    method public int getPlayerState();
-    method public float getPlayerVolume();
-    method public java.util.List<androidx.media2.MediaItem2>! getPlaylist();
-    method public androidx.media2.MediaMetadata2! getPlaylistMetadata();
-    method public int getRepeatMode();
-    method public int getSelectedTrack(int);
-    method public int getShuffleMode();
-    method public androidx.media2.MediaTimestamp2? getTimestamp();
-    method public java.util.List<androidx.media2.XMediaPlayer.TrackInfo>! getTrackInfo();
-    method public int getVideoHeight();
-    method public int getVideoWidth();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! pause();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! play();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! prepare();
-    method public androidx.concurrent.futures.ResolvableFuture<androidx.media2.XMediaPlayer.DrmResult>! prepareDrm(java.util.UUID);
-    method public byte[]! provideDrmKeyResponse(byte[]?, byte[]) throws android.media.DeniedByServerException, androidx.media2.XMediaPlayer.NoDrmSchemeException;
-    method public void releaseDrm() throws androidx.media2.XMediaPlayer.NoDrmSchemeException;
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! removePlaylistItem(androidx.media2.MediaItem2!);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! replacePlaylistItem(int, androidx.media2.MediaItem2!);
-    method public void reset();
-    method public void restoreDrmKeys(byte[]) throws androidx.media2.XMediaPlayer.NoDrmSchemeException;
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! seekTo(long);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! seekTo(long, int);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! selectTrack(int);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! setAudioAttributes(androidx.media.AudioAttributesCompat!);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! setAudioSessionId(int);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! setAuxEffectSendLevel(float);
-    method public void setDrmPropertyString(String, String) throws androidx.media2.XMediaPlayer.NoDrmSchemeException;
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! setMediaItem(androidx.media2.MediaItem2);
-    method public void setOnDrmConfigHelper(androidx.media2.XMediaPlayer.OnDrmConfigHelper!);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! setPlaybackParams(androidx.media2.PlaybackParams2);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! setPlaybackSpeed(float);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! setPlayerVolume(float);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! setPlaylist(java.util.List<androidx.media2.MediaItem2>, androidx.media2.MediaMetadata2?);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! setRepeatMode(int);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! setShuffleMode(int);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! setSurface(android.view.Surface!);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! skipToNextPlaylistItem();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! skipToPlaylistItem(androidx.media2.MediaItem2!);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! skipToPreviousPlaylistItem();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer2.PlayerResult>! updatePlaylistMetadata(androidx.media2.MediaMetadata2!);
-    field public static final int MEDIA_INFO_AUDIO_NOT_PLAYING = 804; // 0x324
-    field public static final int MEDIA_INFO_BAD_INTERLEAVING = 800; // 0x320
-    field public static final int MEDIA_INFO_BUFFERING_UPDATE = 704; // 0x2c0
-    field public static final int MEDIA_INFO_NOT_SEEKABLE = 801; // 0x321
-    field public static final int MEDIA_INFO_PREFETCHED = 100; // 0x64
-    field public static final int MEDIA_INFO_VIDEO_NOT_PLAYING = 805; // 0x325
-    field public static final int MEDIA_INFO_VIDEO_RENDERING_START = 3; // 0x3
-    field public static final int MEDIA_INFO_VIDEO_TRACK_LAGGING = 700; // 0x2bc
-    field public static final int PLAYER_ERROR_IO = -1004; // 0xfffffc14
-    field public static final int PLAYER_ERROR_MALFORMED = -1007; // 0xfffffc11
-    field public static final int PLAYER_ERROR_TIMED_OUT = -110; // 0xffffff92
-    field public static final int PLAYER_ERROR_UNKNOWN = 1; // 0x1
-    field public static final int PLAYER_ERROR_UNSUPPORTED = -1010; // 0xfffffc0e
-    field public static final int SEEK_CLOSEST = 3; // 0x3
-    field public static final int SEEK_CLOSEST_SYNC = 2; // 0x2
-    field public static final int SEEK_NEXT_SYNC = 1; // 0x1
-    field public static final int SEEK_PREVIOUS_SYNC = 0; // 0x0
-  }
-
-  public static final class XMediaPlayer.DrmInfo {
-    method public java.util.Map<java.util.UUID,byte[]>! getPssh();
-    method public java.util.List<java.util.UUID>! getSupportedSchemes();
-  }
-
-  public static class XMediaPlayer.DrmResult extends androidx.media2.SessionPlayer2.PlayerResult {
-    ctor public XMediaPlayer.DrmResult(int, androidx.media2.MediaItem2?);
-    field public static final int RESULT_CODE_BAD_VALUE = -3; // 0xfffffffd
-    field public static final int RESULT_CODE_INVALID_STATE = -2; // 0xfffffffe
-    field public static final int RESULT_CODE_IO_ERROR = -5; // 0xfffffffb
-    field public static final int RESULT_CODE_NOT_SUPPORTED = -6; // 0xfffffffa
-    field public static final int RESULT_CODE_PERMISSION_DENIED = -4; // 0xfffffffc
-    field public static final int RESULT_CODE_PREPARATION_ERROR = -1003; // 0xfffffc15
-    field public static final int RESULT_CODE_PROVISIONING_NETWORK_ERROR = -1001; // 0xfffffc17
-    field public static final int RESULT_CODE_PROVISIONING_SERVER_ERROR = -1002; // 0xfffffc16
-    field public static final int RESULT_CODE_RESOURCE_BUSY = -1005; // 0xfffffc13
-    field public static final int RESULT_CODE_SKIPPED = 1; // 0x1
-    field public static final int RESULT_CODE_SUCCESS = 0; // 0x0
-    field public static final int RESULT_CODE_UNKNOWN_ERROR = -1; // 0xffffffff
-    field public static final int RESULT_CODE_UNSUPPORTED_SCHEME = -1004; // 0xfffffc14
-  }
-
-  public static class XMediaPlayer.NoDrmSchemeException extends android.media.MediaDrmException {
-    ctor public XMediaPlayer.NoDrmSchemeException(String!);
-  }
-
-  public static interface XMediaPlayer.OnDrmConfigHelper {
-    method public void onDrmConfig(androidx.media2.XMediaPlayer!, androidx.media2.MediaItem2!);
-  }
-
-  public abstract static class XMediaPlayer.PlayerCallback extends androidx.media2.SessionPlayer2.PlayerCallback {
-    ctor public XMediaPlayer.PlayerCallback();
-    method public void onDrmInfo(androidx.media2.XMediaPlayer!, androidx.media2.MediaItem2!, androidx.media2.XMediaPlayer.DrmInfo!);
-    method public void onError(androidx.media2.XMediaPlayer!, androidx.media2.MediaItem2!, int, int);
-    method public void onInfo(androidx.media2.XMediaPlayer!, androidx.media2.MediaItem2!, int, int);
-    method public void onMediaTimeDiscontinuity(androidx.media2.XMediaPlayer!, androidx.media2.MediaItem2!, androidx.media2.MediaTimestamp2!);
-    method public void onSubtitleData(androidx.media2.XMediaPlayer!, androidx.media2.MediaItem2!, androidx.media2.SubtitleData2);
-    method public void onTimedMetaDataAvailable(androidx.media2.XMediaPlayer!, androidx.media2.MediaItem2!, androidx.media2.TimedMetaData2!);
-    method public void onVideoSizeChanged(androidx.media2.XMediaPlayer!, androidx.media2.MediaItem2!, int, int);
-  }
-
-  public static final class XMediaPlayer.TrackInfo {
-    method public android.media.MediaFormat! getFormat();
-    method public String! getLanguage();
-    method public int getTrackType();
-    field public static final int MEDIA_TRACK_TYPE_AUDIO = 2; // 0x2
-    field public static final int MEDIA_TRACK_TYPE_METADATA = 5; // 0x5
-    field public static final int MEDIA_TRACK_TYPE_SUBTITLE = 4; // 0x4
-    field public static final int MEDIA_TRACK_TYPE_UNKNOWN = 0; // 0x0
-    field public static final int MEDIA_TRACK_TYPE_VIDEO = 1; // 0x1
-  }
-
 }
 
diff --git a/media2/src/androidTest/java/androidx/media2/MediaBrowser2Test.java b/media2/src/androidTest/java/androidx/media2/MediaBrowser2Test.java
index c25dd91..4348a2c 100644
--- a/media2/src/androidTest/java/androidx/media2/MediaBrowser2Test.java
+++ b/media2/src/androidTest/java/androidx/media2/MediaBrowser2Test.java
@@ -357,7 +357,7 @@
                     LibraryParams params) {
                 // This wouldn't be called at all.
                 return new LibraryResult(RESULT_CODE_SUCCESS,
-                        TestUtils.createPlaylist(testChildrenCount), null);
+                        TestUtils.createMediaItems(testChildrenCount), null);
             }
         };
         final CountDownLatch latch = new CountDownLatch(1);
@@ -410,7 +410,7 @@
                     ControllerInfo controller, String parentId, int page, int pageSize,
                     LibraryParams params) {
                 return new LibraryResult(RESULT_CODE_SUCCESS,
-                        TestUtils.createPlaylist(testChildrenCount), null);
+                        TestUtils.createMediaItems(testChildrenCount), null);
             }
         };
         final BrowserCallback controllerCallbackProxy = new BrowserCallback() {
@@ -460,7 +460,7 @@
                     ControllerInfo controller, String parentId, int page, int pageSize,
                     LibraryParams params) {
                 return new LibraryResult(RESULT_CODE_SUCCESS,
-                        TestUtils.createPlaylist(testChildrenCount), null);
+                        TestUtils.createMediaItems(testChildrenCount), null);
             }
         };
         final CountDownLatch latch = new CountDownLatch(1);
@@ -515,7 +515,7 @@
                     ControllerInfo controller, String parentId, int page, int pageSize,
                     LibraryParams params) {
                 return new LibraryResult(RESULT_CODE_SUCCESS,
-                        TestUtils.createPlaylist(testChildrenCount), null);
+                        TestUtils.createMediaItems(testChildrenCount), null);
             }
         };
         final BrowserCallback controllerCallbackProxy = new BrowserCallback() {
diff --git a/media2/src/androidTest/java/androidx/media2/MediaController2Test.java b/media2/src/androidTest/java/androidx/media2/MediaController2Test.java
index 52920dc..c5abb08 100644
--- a/media2/src/androidTest/java/androidx/media2/MediaController2Test.java
+++ b/media2/src/androidTest/java/androidx/media2/MediaController2Test.java
@@ -38,6 +38,7 @@
 import android.os.Process;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.media.AudioAttributesCompat;
 import androidx.media.VolumeProviderCompat;
 import androidx.media2.MediaController2.ControllerCallback;
@@ -103,6 +104,12 @@
                         }
                         return null;
                     }
+
+                    @Override
+                    public MediaItem2 onCreateMediaItem(MediaSession2 session,
+                            ControllerInfo controller, String mediaId) {
+                        return TestUtils.createMediaItem(mediaId);
+                    }
                 })
                 .setSessionActivity(mIntent)
                 .setId(TAG).build();
@@ -185,7 +192,7 @@
     @Test
     public void testPrepare() {
         prepareLooper();
-        mController.prefetch();
+        mController.prepare();
         try {
             assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
         } catch (InterruptedException e) {
@@ -241,7 +248,7 @@
     public void testUpdatePlayer() throws InterruptedException {
         prepareLooper();
         final int testState = SessionPlayer2.PLAYER_STATE_PLAYING;
-        final List<MediaItem2> testPlaylist = TestUtils.createPlaylist(3);
+        final List<MediaItem2> testPlaylist = TestUtils.createMediaItems(3);
         final AudioAttributesCompat testAudioAttributes = new AudioAttributesCompat.Builder()
                 .setLegacyStreamType(AudioManager.STREAM_RING).build();
         final CountDownLatch latch = new CountDownLatch(3);
@@ -294,7 +301,7 @@
     @Test
     public void testSetPlaylist() throws InterruptedException {
         prepareLooper();
-        final List<MediaItem2> list = TestUtils.createPlaylist(2);
+        final List<String> list = TestUtils.createMediaIds(2);
         mController.setPlaylist(list, null /* Metadata */);
         assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
 
@@ -305,7 +312,7 @@
         assertEquals(list.size(), mPlayer.mPlaylist.size());
         for (int i = 0; i < list.size(); i++) {
             // MediaController2.setPlaylist does not ensure the equality of the items.
-            assertEquals(list.get(i).getMediaId(), mPlayer.mPlaylist.get(i).getMediaId());
+            assertEquals(list.get(i), mPlayer.mPlaylist.get(i).getMediaId());
         }
     }
 
@@ -313,7 +320,8 @@
     public void testSetMediaItem() throws InterruptedException {
         prepareLooper();
         final MediaItem2 item = TestUtils.createMediaItemWithMetadata();
-        mController.setMediaItem(item);
+        mController.setMediaItem(item.getMetadata()
+                .getString(MediaMetadata2.METADATA_KEY_MEDIA_ID));
         assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
 
         assertNull(mPlayer.mMetadata);
@@ -327,7 +335,7 @@
     @Test
     public void testGetPlaylist() throws InterruptedException {
         prepareLooper();
-        final List<MediaItem2> testList = TestUtils.createPlaylist(2);
+        final List<MediaItem2> testList = TestUtils.createMediaItems(2);
         final MediaMetadata2 testMetadata = TestUtils.createMetadata();
         final AtomicReference<List<MediaItem2>> listFromCallback = new AtomicReference<>();
         final CountDownLatch latch = new CountDownLatch(1);
@@ -360,7 +368,7 @@
     @LargeTest
     public void testGetPlaylist_withLongPlaylist() throws InterruptedException {
         prepareLooper();
-        final List<MediaItem2> testList = TestUtils.createPlaylist(5000);
+        final List<MediaItem2> testList = TestUtils.createMediaItems(5000);
         final MediaMetadata2 testMetadata = TestUtils.createMetadata();
         final AtomicReference<List<MediaItem2>> listFromCallback = new AtomicReference<>();
         final CountDownLatch latch = new CountDownLatch(1);
@@ -474,7 +482,7 @@
     public void testControllerCallback_onPlaylistMetadataChanged() throws InterruptedException {
         prepareLooper();
         final MediaItem2 item = TestUtils.createMediaItemWithMetadata();
-        final List<MediaItem2> list = TestUtils.createPlaylist(2);
+        final List<MediaItem2> list = TestUtils.createMediaItems(2);
         final CountDownLatch latch = new CountDownLatch(1);
         final ControllerCallback callback = new ControllerCallback() {
             @Override
@@ -542,7 +550,7 @@
     @Test
     public void testControllerCallback_onBufferingStateChanged() throws InterruptedException {
         prepareLooper();
-        final List<MediaItem2> testPlaylist = TestUtils.createPlaylist(3);
+        final List<MediaItem2> testPlaylist = TestUtils.createMediaItems(3);
         final MediaItem2 testItem = testPlaylist.get(0);
         final int testBufferingState = SessionPlayer2.BUFFERING_STATE_BUFFERING_AND_PLAYABLE;
         final long testBufferingPosition = 500;
@@ -602,7 +610,7 @@
     public void testControllerCallback_onCurrentMediaItemChanged() throws InterruptedException {
         prepareLooper();
         final int listSize = 5;
-        final List<MediaItem2> list = TestUtils.createPlaylist(listSize);
+        final List<MediaItem2> list = TestUtils.createMediaItems(listSize);
         mPlayer.setPlaylist(list, null);
 
         final MediaItem2 currentItem = list.get(3);
@@ -640,27 +648,26 @@
     public void testAddPlaylistItem() throws InterruptedException {
         prepareLooper();
         final int testIndex = 12;
-        final MediaItem2 testMediaItem = TestUtils.createMediaItemWithMetadata();
-        mController.addPlaylistItem(testIndex, testMediaItem);
+        final String testId = "testAddPlaylistItem";
+        mController.addPlaylistItem(testIndex, testId);
         assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
 
         assertTrue(mPlayer.mAddPlaylistItemCalled);
         assertEquals(testIndex, mPlayer.mIndex);
-        // MediaController2.addPlaylistItem does not ensure the equality of the items.
-        assertEquals(testMediaItem.getMediaId(), mPlayer.mItem.getMediaId());
+        assertEquals(testId, mPlayer.mItem.getMediaId());
     }
 
     @Test
     public void testRemovePlaylistItem() throws InterruptedException {
         prepareLooper();
-        mPlayer.mPlaylist = TestUtils.createPlaylist(2);
+        mPlayer.mPlaylist = TestUtils.createMediaItems(2);
 
         // Recreate controller for sending removePlaylistItem.
         // It's easier to ensure that MediaController2.getPlaylist() returns the playlist from the
-        // agent.
+        // player.
         MediaController2 controller = createController(mSession.getToken());
         MediaItem2 targetItem = controller.getPlaylist().get(0);
-        controller.removePlaylistItem(targetItem);
+        controller.removePlaylistItem(0);
         assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
 
         assertTrue(mPlayer.mRemovePlaylistItemCalled);
@@ -671,13 +678,13 @@
     public void testReplacePlaylistItem() throws InterruptedException {
         prepareLooper();
         final int testIndex = 12;
-        final MediaItem2 testMediaItem = TestUtils.createMediaItemWithMetadata();
-        mController.replacePlaylistItem(testIndex, testMediaItem);
+        final String testId = "testAddPlaylistItem";
+        mController.replacePlaylistItem(testIndex, testId);
         assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
 
         assertTrue(mPlayer.mReplacePlaylistItemCalled);
         // MediaController2.replacePlaylistItem does not ensure the equality of the items.
-        assertEquals(testMediaItem.getMediaId(), mPlayer.mItem.getMediaId());
+        assertEquals(testId, mPlayer.mItem.getMediaId());
     }
 
     @Test
@@ -699,13 +706,15 @@
     @Test
     public void testSkipToPlaylistItem() throws InterruptedException {
         prepareLooper();
+        List<MediaItem2> playlist = TestUtils.createMediaItems(2);
+        mPlayer.mPlaylist = playlist;
+
         MediaController2 controller = createController(mSession.getToken());
-        MediaItem2 targetItem = TestUtils.createMediaItemWithMetadata();
-        controller.skipToPlaylistItem(targetItem);
+        controller.skipToPlaylistItem(1);
         assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
 
         assertTrue(mPlayer.mSkipToPlaylistItemCalled);
-        assertEquals(targetItem, mPlayer.mItem);
+        assertEquals(playlist.get(1), mPlayer.mItem);
     }
 
     /**
@@ -1120,7 +1129,7 @@
         final CountDownLatch latch = new CountDownLatch(1);
         final SessionCallback callback = new SessionCallback() {
             @Override
-            public int onPrefetchFromSearch(MediaSession2 session, ControllerInfo controller,
+            public int onPrepareFromSearch(MediaSession2 session, ControllerInfo controller,
                     String query, Bundle extras) {
                 assertEquals(mContext.getPackageName(), controller.getPackageName());
                 assertEquals(request, query);
@@ -1133,7 +1142,7 @@
                 .setSessionCallback(sHandlerExecutor, callback)
                 .setId("testPrepareFromSearch").build()) {
             MediaController2 controller = createController(session.getToken());
-            controller.prefetchFromSearch(request, bundle);
+            controller.prepareFromSearch(request, bundle);
             assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
         }
     }
@@ -1147,7 +1156,7 @@
         final CountDownLatch latch = new CountDownLatch(1);
         final SessionCallback callback = new SessionCallback() {
             @Override
-            public int onPrefetchFromUri(MediaSession2 session, ControllerInfo controller, Uri uri,
+            public int onPrepareFromUri(MediaSession2 session, ControllerInfo controller, Uri uri,
                     Bundle extras) {
                 assertEquals(mContext.getPackageName(), controller.getPackageName());
                 assertEquals(request, uri);
@@ -1160,7 +1169,7 @@
                 .setSessionCallback(sHandlerExecutor, callback)
                 .setId("testPrepareFromUri").build()) {
             MediaController2 controller = createController(session.getToken());
-            controller.prefetchFromUri(request, bundle);
+            controller.prepareFromUri(request, bundle);
             assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
         }
     }
@@ -1174,7 +1183,7 @@
         final CountDownLatch latch = new CountDownLatch(1);
         final SessionCallback callback = new SessionCallback() {
             @Override
-            public int onPrefetchFromMediaId(MediaSession2 session, ControllerInfo controller,
+            public int onPrepareFromMediaId(MediaSession2 session, ControllerInfo controller,
                     String mediaId, Bundle extras) {
                 assertEquals(mContext.getPackageName(), controller.getPackageName());
                 assertEquals(request, mediaId);
@@ -1187,7 +1196,7 @@
                 .setSessionCallback(sHandlerExecutor, callback)
                 .setId("testPrepareFromMediaId").build()) {
             MediaController2 controller = createController(session.getToken());
-            controller.prefetchFromMediaId(request, bundle);
+            controller.prepareFromMediaId(request, bundle);
             assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
         }
     }
@@ -1531,6 +1540,73 @@
         assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
     }
 
+    @Test
+    public void testSetMetadataForCurrentMediaItem() throws InterruptedException {
+        final CountDownLatch latch = new CountDownLatch(2);
+        final long duration = 1000L;
+        final MediaItem2 item = TestUtils.createMediaItemWithMetadata();
+        final ControllerCallback callback = new ControllerCallback() {
+            @Override
+            public void onCurrentMediaItemChanged(@NonNull MediaController2 controller,
+                    @Nullable MediaItem2 item) {
+                MediaMetadata2 metadata = item.getMetadata();
+                if (metadata != null) {
+                    switch ((int) latch.getCount()) {
+                        case 2:
+                            assertFalse(metadata.containsKey(
+                                    MediaMetadata2.METADATA_KEY_DURATION));
+                            break;
+                        case 1:
+                            assertTrue(metadata.containsKey(
+                                    MediaMetadata2.METADATA_KEY_DURATION));
+                            assertEquals(duration,
+                                    metadata.getLong(MediaMetadata2.METADATA_KEY_DURATION));
+                    }
+                }
+                latch.countDown();
+            }
+        };
+        MediaController2 controller = createController(mSession.getToken(), true, callback);
+        mPlayer.setMediaItem(item);
+        mPlayer.notifyCurrentMediaItemChanged(item);
+        assertFalse(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        item.setMetadata(TestUtils.createMetadata(item.getMediaId(), duration));
+        assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testSetMetadataForMediaItemInPlaylist() throws InterruptedException {
+        final CountDownLatch latch = new CountDownLatch(2);
+        final long duration = 1000L;
+        final List<MediaItem2> list = TestUtils.createMediaItems(2);
+        final MediaMetadata2 oldMetadata = list.get(1).getMetadata();
+        final MediaMetadata2 newMetadata = TestUtils.createMetadata(oldMetadata.getMediaId(),
+                duration);
+        final ControllerCallback callback = new ControllerCallback() {
+            @Override
+            public void onPlaylistChanged(@NonNull MediaController2 controller,
+                    @NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) {
+                switch ((int) latch.getCount()) {
+                    case 2:
+                        assertFalse(oldMetadata.containsKey(MediaMetadata2.METADATA_KEY_DURATION));
+                        break;
+                    case 1:
+                        assertTrue(list.get(1).getMetadata().containsKey(
+                                MediaMetadata2.METADATA_KEY_DURATION));
+                        assertEquals(duration, list.get(1).getMetadata().getLong(
+                                MediaMetadata2.METADATA_KEY_DURATION));
+                }
+                latch.countDown();
+            }
+        };
+        MediaController2 controller = createController(mSession.getToken(), true, callback);
+        mPlayer.setPlaylist(list, null);
+        mPlayer.notifyPlaylistChanged();
+        assertFalse(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        list.get(1).setMetadata(newMetadata);
+        assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+    }
+
     private void testCloseFromService(String id) throws InterruptedException {
         final CountDownLatch latch = new CountDownLatch(1);
         TestServiceRegistry.getInstance().setSessionServiceCallback(new SessionServiceCallback() {
diff --git a/media2/src/androidTest/java/androidx/media2/MediaPlayer2Test.java b/media2/src/androidTest/java/androidx/media2/MediaPlayer2Test.java
index 5d9e156..51ac7844 100644
--- a/media2/src/androidTest/java/androidx/media2/MediaPlayer2Test.java
+++ b/media2/src/androidTest/java/androidx/media2/MediaPlayer2Test.java
@@ -737,7 +737,7 @@
     public void testVideoSurfaceResetting() throws Exception {
         final int tolerance = 150;
         final int audioLatencyTolerance = 1000;  /* covers audio path latency variability */
-        final int seekPos = 4760;  // This is the I-frame position
+        final int seekPos = 1840;  // This is the I-frame position
 
         final CountDownLatch seekDone = new CountDownLatch(1);
 
@@ -754,10 +754,11 @@
             mEventCallbacks.add(ecb);
         }
 
-        if (!checkLoadResource(R.raw.testvideo)) {
+        if (!checkLoadResource(
+                R.raw.video_480x360_mp4_h264_500kbps_25fps_aac_stereo_128kbps_44100hz)) {
             return; // skip;
         }
-        playLoadedVideo(352, 288, -1);
+        playLoadedVideo(480, 360, -1);
 
         Thread.sleep(SLEEP_TIME);
 
diff --git a/media2/src/androidTest/java/androidx/media2/XMediaPlayerDrmTest.java b/media2/src/androidTest/java/androidx/media2/MediaPlayerDrmTest.java
similarity index 96%
rename from media2/src/androidTest/java/androidx/media2/XMediaPlayerDrmTest.java
rename to media2/src/androidTest/java/androidx/media2/MediaPlayerDrmTest.java
index 57dcff4..13ef147 100644
--- a/media2/src/androidTest/java/androidx/media2/XMediaPlayerDrmTest.java
+++ b/media2/src/androidTest/java/androidx/media2/MediaPlayerDrmTest.java
@@ -43,10 +43,10 @@
 import android.view.WindowManager;
 
 import androidx.annotation.CallSuper;
+import androidx.media2.MediaPlayer.DrmInfo;
+import androidx.media2.MediaPlayer.DrmResult;
 import androidx.media2.SessionPlayer2.PlayerResult;
 import androidx.media2.TestUtils.Monitor;
-import androidx.media2.XMediaPlayer.DrmInfo;
-import androidx.media2.XMediaPlayer.DrmResult;
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.LargeTest;
 import androidx.test.filters.Suppress;
@@ -80,12 +80,12 @@
 import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
- * DRM tests which use XMediaPlayer to play audio or video.
+ * DRM tests which use MediaPlayer to play audio or video.
  */
 @LargeTest
 @RunWith(AndroidJUnit4.class)
 @Suppress // Disabled as it 100% fails b/79682973
-public class XMediaPlayerDrmTest {
+public class MediaPlayerDrmTest {
     private static final int STREAM_RETRIES = 3;
 
     private Monitor mOnVideoSizeChangedCalled = new Monitor();
@@ -96,12 +96,12 @@
     private Context mContext;
     private Resources mResources;
 
-    private XMediaPlayer mPlayer = null;
+    private MediaPlayer mPlayer = null;
     private MediaStubActivity mActivity;
     private Instrumentation mInstrumentation;
 
     private ExecutorService mExecutor;
-    private XMediaPlayer.PlayerCallback mECb = null;
+    private MediaPlayer.PlayerCallback mECb = null;
 
     @Rule
     public GrantPermissionRule mRuntimePermissionRule =
@@ -134,7 +134,7 @@
         try {
             mActivityRule.runOnUiThread(new Runnable() {
                 public void run() {
-                    mPlayer = new XMediaPlayer(mActivity);
+                    mPlayer = new MediaPlayer(mActivity);
                 }
             });
         } catch (Throwable e) {
@@ -256,7 +256,7 @@
     //////////////////////////////////////////////////////////////////////////////////////////
     // Modular DRM
 
-    private static final String TAG = "XMediaPlayerDrmTest";
+    private static final String TAG = "MediaPlayerDrmTest";
 
     private static final int PLAY_TIME_MS = 60 * 1000;
     private byte[] mKeySetId;
@@ -377,21 +377,21 @@
 
         mAudioOnly = (width == 0);
 
-        mECb = new XMediaPlayer.PlayerCallback() {
+        mECb = new MediaPlayer.PlayerCallback() {
                 @Override
-                public void onVideoSizeChanged(XMediaPlayer mp, MediaItem2 item, int w, int h) {
+                public void onVideoSizeChanged(MediaPlayer mp, MediaItem2 item, int w, int h) {
                     Log.v(TAG, "VideoSizeChanged" + " w:" + w + " h:" + h);
                     mOnVideoSizeChangedCalled.signal();
                 }
 
                 @Override
-                public void onError(XMediaPlayer mp, MediaItem2 item, int what, int extra) {
+                public void onError(MediaPlayer mp, MediaItem2 item, int what, int extra) {
                     fail("Media player had error " + what + " playing video");
                 }
 
                 @Override
-                public void onInfo(XMediaPlayer mp, MediaItem2 item, int what, int extra) {
-                    if (what == XMediaPlayer.MEDIA_INFO_MEDIA_ITEM_END) {
+                public void onInfo(MediaPlayer mp, MediaItem2 item, int what, int extra) {
+                    if (what == MediaPlayer.MEDIA_INFO_MEDIA_ITEM_END) {
                         Log.v(TAG, "playLoadedVideo: onInfo_PlaybackComplete");
                         mOnPlaybackCompleted.signal();
                     }
@@ -474,9 +474,9 @@
             throws InterruptedException, ExecutionException {
         final AtomicBoolean asyncSetupDrmError = new AtomicBoolean(false);
 
-        mPlayer.registerPlayerCallback(mExecutor, new XMediaPlayer.PlayerCallback() {
+        mPlayer.registerPlayerCallback(mExecutor, new MediaPlayer.PlayerCallback() {
             @Override
-            public void onDrmInfo(XMediaPlayer mp, MediaItem2 item, DrmInfo drmInfo) {
+            public void onDrmInfo(MediaPlayer mp, MediaItem2 item, DrmInfo drmInfo) {
                 Log.v(TAG, "preparePlayerAndDrm_V1: onDrmInfo" + drmInfo);
 
                 // in the callback (async mode) so handling exceptions here
@@ -506,9 +506,9 @@
     }
 
     private void preparePlayerAndDrm_V2_syncDrmSetupPlusConfig() throws Exception {
-        mPlayer.setOnDrmConfigHelper(new XMediaPlayer.OnDrmConfigHelper() {
+        mPlayer.setOnDrmConfigHelper(new MediaPlayer.OnDrmConfigHelper() {
             @Override
-            public void onDrmConfig(XMediaPlayer mp, MediaItem2 item) {
+            public void onDrmConfig(MediaPlayer mp, MediaItem2 item) {
                 String widevineSecurityLevel3 = "L3";
                 String securityLevelProperty = "securityLevel";
 
@@ -520,7 +520,7 @@
                     level = mp.getDrmPropertyString(securityLevelProperty);
                     Log.v(TAG, "preparePlayerAndDrm_V2: getDrmPropertyString: "
                             + securityLevelProperty + " -> " + level);
-                } catch (XMediaPlayer.NoDrmSchemeException e) {
+                } catch (MediaPlayer.NoDrmSchemeException e) {
                     Log.v(TAG, "preparePlayerAndDrm_V2: NoDrmSchemeException");
                 } catch (Exception e) {
                     Log.v(TAG, "preparePlayerAndDrm_V2: onDrmConfig EXCEPTION " + e);
@@ -544,9 +544,9 @@
             throws InterruptedException, ExecutionException {
         final AtomicBoolean asyncSetupDrmError = new AtomicBoolean(false);
 
-        mPlayer.registerPlayerCallback(mExecutor, new XMediaPlayer.PlayerCallback() {
+        mPlayer.registerPlayerCallback(mExecutor, new MediaPlayer.PlayerCallback() {
             @Override
-            public void onDrmInfo(XMediaPlayer mp, MediaItem2 item, DrmInfo drmInfo) {
+            public void onDrmInfo(MediaPlayer mp, MediaItem2 item, DrmInfo drmInfo) {
                 Log.v(TAG, "preparePlayerAndDrm_V3: onDrmInfo" + drmInfo);
 
                 // DRM preperation
@@ -831,7 +831,7 @@
             // storing offline key for a later restore
             mKeySetId = (keyType == MediaDrm.KEY_TYPE_OFFLINE) ? keySetId : null;
 
-        } catch (XMediaPlayer.NoDrmSchemeException e) {
+        } catch (MediaPlayer.NoDrmSchemeException e) {
             Log.d(TAG, "setupDrm: NoDrmSchemeException");
             e.printStackTrace();
             throw e;
@@ -866,7 +866,7 @@
 
             mPlayer.restoreDrmKeys(mKeySetId);
 
-        } catch (XMediaPlayer.NoDrmSchemeException e) {
+        } catch (MediaPlayer.NoDrmSchemeException e) {
             Log.v(TAG, "setupDrmRestore: NoDrmSchemeException");
             e.printStackTrace();
             throw e;
diff --git a/media2/src/androidTest/java/androidx/media2/XMediaPlayerTest.java b/media2/src/androidTest/java/androidx/media2/MediaPlayerTest.java
similarity index 90%
rename from media2/src/androidTest/java/androidx/media2/XMediaPlayerTest.java
rename to media2/src/androidTest/java/androidx/media2/MediaPlayerTest.java
index 568644a..e8a3826 100644
--- a/media2/src/androidTest/java/androidx/media2/XMediaPlayerTest.java
+++ b/media2/src/androidTest/java/androidx/media2/MediaPlayerTest.java
@@ -63,8 +63,8 @@
 
 @RunWith(AndroidJUnit4.class)
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.P)
-public class XMediaPlayerTest extends XMediaPlayerTestBase {
-    private static final String LOG_TAG = "XMediaPlayerTest";
+public class MediaPlayerTest extends MediaPlayerTestBase {
+    private static final String LOG_TAG = "MediaPlayerTest";
 
     private static final int SLEEP_TIME = 1000;
     private static final int WAIT_TIME_MS = 300;
@@ -137,10 +137,10 @@
         ListenableFuture<PlayerResult> future = mPlayer.prepare();
         assertEquals(RESULT_CODE_SUCCESS, future.get().getResultCode());
 
-        assertFalse(mPlayer.getPlayerState() == XMediaPlayer.PLAYER_STATE_PLAYING);
+        assertFalse(mPlayer.getPlayerState() == MediaPlayer.PLAYER_STATE_PLAYING);
         future = mPlayer.play();
         assertEquals(RESULT_CODE_SUCCESS, future.get().getResultCode());
-        assertTrue(mPlayer.getPlayerState() == XMediaPlayer.PLAYER_STATE_PLAYING);
+        assertTrue(mPlayer.getPlayerState() == MediaPlayer.PLAYER_STATE_PLAYING);
 
         assertEquals(mp3Duration, mPlayer.getDuration(), tolerance);
         long pos = mPlayer.getCurrentPosition();
@@ -153,13 +153,13 @@
 
         future = mPlayer.pause();
         assertEquals(RESULT_CODE_SUCCESS, future.get().getResultCode());
-        assertFalse(mPlayer.getPlayerState() == XMediaPlayer.PLAYER_STATE_PLAYING);
+        assertFalse(mPlayer.getPlayerState() == MediaPlayer.PLAYER_STATE_PLAYING);
         future = mPlayer.play();
         assertEquals(RESULT_CODE_SUCCESS, future.get().getResultCode());
-        assertTrue(mPlayer.getPlayerState() == XMediaPlayer.PLAYER_STATE_PLAYING);
+        assertTrue(mPlayer.getPlayerState() == MediaPlayer.PLAYER_STATE_PLAYING);
 
         // waiting to complete
-        while (mPlayer.getPlayerState() == XMediaPlayer.PLAYER_STATE_PLAYING) {
+        while (mPlayer.getPlayerState() == MediaPlayer.PLAYER_STATE_PLAYING) {
             Thread.sleep(SLEEP_TIME);
         }
     }
@@ -178,9 +178,9 @@
 
         final TestUtils.Monitor onVideoSizeChangedCalled = new TestUtils.Monitor();
         final TestUtils.Monitor onVideoRenderingStartCalled = new TestUtils.Monitor();
-        XMediaPlayer.PlayerCallback callback = new XMediaPlayer.PlayerCallback() {
+        MediaPlayer.PlayerCallback callback = new MediaPlayer.PlayerCallback() {
             @Override
-            public void onVideoSizeChanged(XMediaPlayer mp, MediaItem2 dsd, int w, int h) {
+            public void onVideoSizeChanged(MediaPlayer mp, MediaItem2 dsd, int w, int h) {
                 if (w == 0 && h == 0) {
                     // A size of 0x0 can be sent initially one time when using NuPlayer.
                     assertFalse(onVideoSizeChangedCalled.isSignalled());
@@ -192,13 +192,13 @@
             }
 
             @Override
-            public void onError(XMediaPlayer mp, MediaItem2 dsd, int what, int extra) {
+            public void onError(MediaPlayer mp, MediaItem2 dsd, int what, int extra) {
                 fail("Media player had error " + what + " playing video");
             }
 
             @Override
-            public void onInfo(XMediaPlayer mp, MediaItem2 dsd, int what, int extra) {
-                if (what == XMediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START) {
+            public void onInfo(MediaPlayer mp, MediaItem2 dsd, int what, int extra) {
+                if (what == MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START) {
                     onVideoRenderingStartCalled.signal();
                 }
             }
@@ -214,7 +214,7 @@
         mPlayer.setPlayerVolume(volume);
 
         // waiting to complete
-        while (mPlayer.getPlayerState() == XMediaPlayer.PLAYER_STATE_PLAYING) {
+        while (mPlayer.getPlayerState() == MediaPlayer.PLAYER_STATE_PLAYING) {
             Thread.sleep(SLEEP_TIME);
         }
 
@@ -266,34 +266,34 @@
         final int tolerance = 70;
 
         mPlayer.setSurface(mActivity.getSurfaceHolder2().getSurface());
-        assertEquals(XMediaPlayer.PLAYER_STATE_IDLE, mPlayer.getPlayerState());
-        assertEquals(XMediaPlayer.UNKNOWN_TIME, mPlayer.getDuration());
+        assertEquals(MediaPlayer.PLAYER_STATE_IDLE, mPlayer.getPlayerState());
+        assertEquals(MediaPlayer.UNKNOWN_TIME, mPlayer.getDuration());
 
         ListenableFuture<PlayerResult> future = mPlayer.prepare();
         assertEquals(RESULT_CODE_SUCCESS, future.get().getResultCode());
 
-        assertEquals(XMediaPlayer.PLAYER_STATE_PAUSED, mPlayer.getPlayerState());
+        assertEquals(MediaPlayer.PLAYER_STATE_PAUSED, mPlayer.getPlayerState());
         assertEquals(expectedDuration, mPlayer.getDuration(), tolerance);
     }
 
     @Test
     @SmallTest
     public void testGetCurrentPosition() throws Exception {
-        assertEquals(XMediaPlayer.PLAYER_STATE_IDLE, mPlayer.getPlayerState());
-        assertEquals(XMediaPlayer.UNKNOWN_TIME, mPlayer.getCurrentPosition());
+        assertEquals(MediaPlayer.PLAYER_STATE_IDLE, mPlayer.getPlayerState());
+        assertEquals(MediaPlayer.UNKNOWN_TIME, mPlayer.getCurrentPosition());
     }
 
     @Test
     @SmallTest
     public void testGetBufferedPosition() throws Exception {
-        assertEquals(XMediaPlayer.PLAYER_STATE_IDLE, mPlayer.getPlayerState());
-        assertEquals(XMediaPlayer.UNKNOWN_TIME, mPlayer.getBufferedPosition());
+        assertEquals(MediaPlayer.PLAYER_STATE_IDLE, mPlayer.getPlayerState());
+        assertEquals(MediaPlayer.UNKNOWN_TIME, mPlayer.getBufferedPosition());
     }
 
     @Test
     @SmallTest
     public void testGetPlaybackSpeed() throws Exception {
-        assertEquals(XMediaPlayer.PLAYER_STATE_IDLE, mPlayer.getPlayerState());
+        assertEquals(MediaPlayer.PLAYER_STATE_IDLE, mPlayer.getPlayerState());
         try {
             mPlayer.getPlaybackSpeed();
         } catch (Exception e) {
@@ -334,7 +334,7 @@
             PlaybackParams2 pbp = mPlayer.getPlaybackParams();
             assertEquals(playbackRate, pbp.getSpeed(), FLOAT_TOLERANCE);
             assertTrue("The player should still be playing",
-                    mPlayer.getPlayerState() == XMediaPlayer.PLAYER_STATE_PLAYING);
+                    mPlayer.getPlayerState() == MediaPlayer.PLAYER_STATE_PLAYING);
 
             long playedMediaDurationMs = mPlayer.getCurrentPosition();
             int diff = Math.abs((int) (playedMediaDurationMs / playbackRate) - playTime);
@@ -478,7 +478,7 @@
 
     private void readSubtitleTracks() throws Exception {
         mSubtitleTrackIndex.clear();
-        List<XMediaPlayer.TrackInfo> trackInfos = mPlayer.getTrackInfo();
+        List<MediaPlayer.TrackInfo> trackInfos = mPlayer.getTrackInfo();
         if (trackInfos == null || trackInfos.size() == 0) {
             return;
         }
@@ -520,16 +520,16 @@
 
         mInstrumentation.waitForIdleSync();
 
-        XMediaPlayer.PlayerCallback callback = new XMediaPlayer.PlayerCallback() {
+        MediaPlayer.PlayerCallback callback = new MediaPlayer.PlayerCallback() {
             @Override
-            public void onInfo(XMediaPlayer mp, MediaItem2 dsd, int what, int extra) {
+            public void onInfo(MediaPlayer mp, MediaItem2 dsd, int what, int extra) {
                 if (what == MediaPlayer2.MEDIA_INFO_METADATA_UPDATE) {
                     mOnInfoCalled.signal();
                 }
             }
 
             @Override
-            public void onSubtitleData(XMediaPlayer mp, MediaItem2 dsd, SubtitleData2 data) {
+            public void onSubtitleData(MediaPlayer mp, MediaItem2 dsd, SubtitleData2 data) {
                 if (data != null && data.getData() != null) {
                     mOnSubtitleDataCalled.signal();
                 }
@@ -539,7 +539,7 @@
         mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
         mPlayer.prepare();
         mPlayer.play().get();
-        assertTrue(mPlayer.getPlayerState() == XMediaPlayer.PLAYER_STATE_PLAYING);
+        assertTrue(mPlayer.getPlayerState() == MediaPlayer.PLAYER_STATE_PLAYING);
 
         // Closed caption tracks are in-band.
         // So, those tracks will be found after processing a number of frames.
@@ -575,9 +575,9 @@
             fail();
         }
 
-        XMediaPlayer.PlayerCallback callback = new XMediaPlayer.PlayerCallback() {
+        MediaPlayer.PlayerCallback callback = new MediaPlayer.PlayerCallback() {
             @Override
-            public void onInfo(XMediaPlayer mp, MediaItem2 dsd, int what, int extra) {
+            public void onInfo(MediaPlayer mp, MediaItem2 dsd, int what, int extra) {
                 if (what == MediaPlayer2.MEDIA_INFO_METADATA_UPDATE) {
                     mOnInfoCalled.signal();
                 }
@@ -585,7 +585,7 @@
 
             @Override
             public void onSubtitleData(
-                    XMediaPlayer mp, MediaItem2 dsd, SubtitleData2 data) {
+                    MediaPlayer mp, MediaItem2 dsd, SubtitleData2 data) {
                 if (data != null && data.getData() != null) {
                     mOnSubtitleDataCalled.signal();
                 }
@@ -595,7 +595,7 @@
         mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
         mPlayer.prepare();
         mPlayer.play().get();
-        assertTrue(mPlayer.getPlayerState() == XMediaPlayer.PLAYER_STATE_PLAYING);
+        assertTrue(mPlayer.getPlayerState() == MediaPlayer.PLAYER_STATE_PLAYING);
 
         // Closed caption tracks are in-band.
         // So, those tracks will be found after processing a number of frames.
@@ -624,9 +624,9 @@
             fail();
         }
 
-        XMediaPlayer.PlayerCallback callback = new XMediaPlayer.PlayerCallback() {
+        MediaPlayer.PlayerCallback callback = new MediaPlayer.PlayerCallback() {
             @Override
-            public void onInfo(XMediaPlayer mp, MediaItem2 dsd, int what, int extra) {
+            public void onInfo(MediaPlayer mp, MediaItem2 dsd, int what, int extra) {
                 if (what == MediaPlayer2.MEDIA_INFO_METADATA_UPDATE) {
                     mOnInfoCalled.signal();
                 }
@@ -636,7 +636,7 @@
         mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
         mPlayer.prepare();
         mPlayer.play().get();
-        assertTrue(mPlayer.getPlayerState() == XMediaPlayer.PLAYER_STATE_PLAYING);
+        assertTrue(mPlayer.getPlayerState() == MediaPlayer.PLAYER_STATE_PLAYING);
 
         // The media metadata will be changed while playing since closed caption tracks are in-band
         // and those tracks will be found after processing a number of frames. These tracks will be
@@ -661,10 +661,10 @@
         }
 
         final BlockingDeque<MediaTimestamp2> timestamps = new LinkedBlockingDeque<>();
-        XMediaPlayer.PlayerCallback callback = new XMediaPlayer.PlayerCallback() {
+        MediaPlayer.PlayerCallback callback = new MediaPlayer.PlayerCallback() {
             @Override
             public void onMediaTimeDiscontinuity(
-                    XMediaPlayer mp, MediaItem2 dsd, MediaTimestamp2 timestamp) {
+                    MediaPlayer mp, MediaItem2 dsd, MediaTimestamp2 timestamp) {
                 timestamps.add(timestamp);
                 mOnMediaTimeDiscontinuityCalled.signal();
             }
@@ -719,15 +719,15 @@
         mPlayer.setMediaItem(new CallbackMediaItem2.Builder(dataSource).build());
         mPlayer.prepare();
         mPlayer.play().get();
-        assertTrue(mPlayer.getPlayerState() == XMediaPlayer.PLAYER_STATE_PLAYING);
+        assertTrue(mPlayer.getPlayerState() == MediaPlayer.PLAYER_STATE_PLAYING);
 
         // Test pause and restart.
         mPlayer.pause();
         Thread.sleep(SLEEP_TIME);
-        assertFalse(mPlayer.getPlayerState() == XMediaPlayer.PLAYER_STATE_PLAYING);
+        assertFalse(mPlayer.getPlayerState() == MediaPlayer.PLAYER_STATE_PLAYING);
 
         mPlayer.play().get();
-        assertTrue(mPlayer.getPlayerState() == XMediaPlayer.PLAYER_STATE_PLAYING);
+        assertTrue(mPlayer.getPlayerState() == MediaPlayer.PLAYER_STATE_PLAYING);
 
         // Test reset.
         mPlayer.reset();
@@ -735,12 +735,12 @@
 
         mPlayer.prepare();
         mPlayer.play().get();
-        assertTrue(mPlayer.getPlayerState() == XMediaPlayer.PLAYER_STATE_PLAYING);
+        assertTrue(mPlayer.getPlayerState() == MediaPlayer.PLAYER_STATE_PLAYING);
 
         // Test seek. Note: the seek position is cached and returned as the
         // current position so there's no point in comparing them.
         mPlayer.seekTo(duration - SLEEP_TIME, MediaPlayer2.SEEK_PREVIOUS_SYNC);
-        while (mPlayer.getPlayerState() == XMediaPlayer.PLAYER_STATE_PLAYING) {
+        while (mPlayer.getPlayerState() == MediaPlayer.PLAYER_STATE_PLAYING) {
             Thread.sleep(SLEEP_TIME);
         }
     }
@@ -772,10 +772,10 @@
     public void testPlaybackFailsIfMedia2DataSourceThrows() throws Exception {
         final int resid = R.raw.video_480x360_mp4_h264_1350kbps_30fps_aac_stereo_192kbps_44100hz;
 
-        XMediaPlayer.PlayerCallback callback = new XMediaPlayer.PlayerCallback() {
+        MediaPlayer.PlayerCallback callback = new MediaPlayer.PlayerCallback() {
             @Override
             public void onError(
-                    XMediaPlayer mp, MediaItem2 dsd, int what, int extra) {
+                    MediaPlayer mp, MediaItem2 dsd, int what, int extra) {
                 mOnErrorCalled.signal();
             }
         };
@@ -802,10 +802,10 @@
                 TestDataSourceCallback2.fromAssetFd(mResources.openRawResourceFd(resid));
         mPlayer.setMediaItem(new CallbackMediaItem2.Builder(dataSource).build());
 
-        XMediaPlayer.PlayerCallback callback = new XMediaPlayer.PlayerCallback() {
+        MediaPlayer.PlayerCallback callback = new MediaPlayer.PlayerCallback() {
             @Override
             public void onError(
-                    XMediaPlayer mp, MediaItem2 dsd, int what, int extra) {
+                    MediaPlayer mp, MediaItem2 dsd, int what, int extra) {
                 mOnErrorCalled.signal();
             }
         };
@@ -821,7 +821,7 @@
     @Test
     @LargeTest
     public void testPreservePlaybackProperties() throws Exception {
-        /* TODO: enable this test once XMediaPlayer has playlist implementation.
+        /* TODO: enable this test once MediaPlayer has playlist implementation.
         final int resid1 = R.raw.video_480x360_mp4_h264_1350kbps_30fps_aac_stereo_192kbps_44100hz;
         final long start1 = 6000;
         final long end1 = 7000;
@@ -846,7 +846,7 @@
         mPlayer.setNextMediaItem(dsd2);
         mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
 
-        XMediaPlayer.PlayerCallback callback = new XMediaPlayer.PlayerCallback() {
+        MediaPlayer.PlayerCallback callback = new MediaPlayer.PlayerCallback() {
             @Override
             public void onInfo(MediaPlayer2 mp, MediaItem2 dsd, int what, int extra) {
                 if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
@@ -936,9 +936,9 @@
             }
         };
 
-        XMediaPlayer.PlayerCallback callback = new XMediaPlayer.PlayerCallback() {
+        MediaPlayer.PlayerCallback callback = new MediaPlayer.PlayerCallback() {
             @Override
-            public void onError(XMediaPlayer mp, MediaItem2 dsd, int what, int extra) {
+            public void onError(MediaPlayer mp, MediaItem2 dsd, int what, int extra) {
                 mOnErrorCalled.signal();
             }
         };
@@ -971,33 +971,33 @@
         mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
 
         ListenableFuture<PlayerResult> future;
-        assertEquals(XMediaPlayer.BUFFERING_STATE_UNKNOWN, mPlayer.getBufferingState());
-        assertEquals(XMediaPlayer.PLAYER_STATE_IDLE, mPlayer.getPlayerState());
+        assertEquals(MediaPlayer.BUFFERING_STATE_UNKNOWN, mPlayer.getBufferingState());
+        assertEquals(MediaPlayer.PLAYER_STATE_IDLE, mPlayer.getPlayerState());
 
         future = mPlayer.prepare();
         assertEquals(MediaPlayer2.CALL_STATUS_NO_ERROR, future.get().getResultCode());
 
-        assertEquals(XMediaPlayer.BUFFERING_STATE_BUFFERING_AND_PLAYABLE,
+        assertEquals(MediaPlayer.BUFFERING_STATE_BUFFERING_AND_PLAYABLE,
                 mPlayer.getBufferingState());
-        assertEquals(XMediaPlayer.PLAYER_STATE_PAUSED, mPlayer.getPlayerState());
+        assertEquals(MediaPlayer.PLAYER_STATE_PAUSED, mPlayer.getPlayerState());
 
         future = mPlayer.play();
         assertEquals(MediaPlayer2.CALL_STATUS_NO_ERROR, future.get().getResultCode());
 
-        assertEquals(XMediaPlayer.BUFFERING_STATE_BUFFERING_AND_PLAYABLE,
+        assertEquals(MediaPlayer.BUFFERING_STATE_BUFFERING_AND_PLAYABLE,
                 mPlayer.getBufferingState());
-        assertEquals(XMediaPlayer.PLAYER_STATE_PLAYING, mPlayer.getPlayerState());
+        assertEquals(MediaPlayer.PLAYER_STATE_PLAYING, mPlayer.getPlayerState());
 
         future = mPlayer.pause();
         assertEquals(MediaPlayer2.CALL_STATUS_NO_ERROR, future.get().getResultCode());
 
-        assertEquals(XMediaPlayer.BUFFERING_STATE_BUFFERING_AND_PLAYABLE,
+        assertEquals(MediaPlayer.BUFFERING_STATE_BUFFERING_AND_PLAYABLE,
                 mPlayer.getBufferingState());
-        assertEquals(XMediaPlayer.PLAYER_STATE_PAUSED, mPlayer.getPlayerState());
+        assertEquals(MediaPlayer.PLAYER_STATE_PAUSED, mPlayer.getPlayerState());
 
         mPlayer.reset();
-        assertEquals(XMediaPlayer.BUFFERING_STATE_UNKNOWN, mPlayer.getBufferingState());
-        assertEquals(XMediaPlayer.PLAYER_STATE_IDLE, mPlayer.getPlayerState());
+        assertEquals(MediaPlayer.BUFFERING_STATE_UNKNOWN, mPlayer.getBufferingState());
+        assertEquals(MediaPlayer.PLAYER_STATE_IDLE, mPlayer.getPlayerState());
     }
 
     @Test
@@ -1017,7 +1017,7 @@
         final TestUtils.Monitor onPlaybackSpeedChanged = new TestUtils.Monitor();
         final AtomicReference<Float> playbackSpeed = new AtomicReference<>();
 
-        XMediaPlayer.PlayerCallback callback = new XMediaPlayer.PlayerCallback() {
+        MediaPlayer.PlayerCallback callback = new MediaPlayer.PlayerCallback() {
             @Override
             public void onPlayerStateChanged(SessionPlayer2 player, int state) {
                 playerState.set(state);
@@ -1050,16 +1050,16 @@
         future = mPlayer.prepare();
         do {
             assertTrue(onBufferingStateChangedCalled.waitForSignal(1000));
-        } while (bufferingState.get() != XMediaPlayer.BUFFERING_STATE_BUFFERING_AND_STARVED);
+        } while (bufferingState.get() != MediaPlayer.BUFFERING_STATE_BUFFERING_AND_STARVED);
 
         assertEquals(MediaPlayer2.CALL_STATUS_NO_ERROR, future.get().getResultCode());
 
         do {
             assertTrue(onPlayerStateChangedCalled.waitForSignal(1000));
-        } while (playerState.get() != XMediaPlayer.PLAYER_STATE_PAUSED);
+        } while (playerState.get() != MediaPlayer.PLAYER_STATE_PAUSED);
         do {
             assertTrue(onBufferingStateChangedCalled.waitForSignal(1000));
-        } while (bufferingState.get() != XMediaPlayer.BUFFERING_STATE_BUFFERING_AND_PLAYABLE);
+        } while (bufferingState.get() != MediaPlayer.BUFFERING_STATE_BUFFERING_AND_PLAYABLE);
 
         onSeekCompleteCalled.reset();
         mPlayer.seekTo(mp4Duration >> 1);
@@ -1072,7 +1072,7 @@
         } while (Math.abs(playbackSpeed.get() - 0.5f) > FLOAT_TOLERANCE);
 
         mPlayer.reset();
-        assertEquals(XMediaPlayer.PLAYER_STATE_IDLE, mPlayer.getPlayerState());
+        assertEquals(MediaPlayer.PLAYER_STATE_IDLE, mPlayer.getPlayerState());
 
         mPlayer.unregisterPlayerCallback(callback);
     }
@@ -1138,9 +1138,9 @@
                 mTestSource.close();
             }
         };
-        XMediaPlayer.PlayerCallback ecb = new XMediaPlayer.PlayerCallback() {
+        MediaPlayer.PlayerCallback ecb = new MediaPlayer.PlayerCallback() {
             @Override
-            public void onError(XMediaPlayer mp, MediaItem2 item, int what, int extra) {
+            public void onError(MediaPlayer mp, MediaItem2 item, int what, int extra) {
                 mOnErrorCalled.signal();
             }
         };
@@ -1175,7 +1175,7 @@
     @SmallTest
     public void testSetAndGetShuflleMode() throws Exception {
         final TestUtils.Monitor onShuffleModeChangedMonitor = new TestUtils.Monitor();
-        XMediaPlayer.PlayerCallback callback = new XMediaPlayer.PlayerCallback() {
+        MediaPlayer.PlayerCallback callback = new MediaPlayer.PlayerCallback() {
             @Override
             public void onShuffleModeChanged(SessionPlayer2 player, int shuffleMode) {
                 mPlayerCbArg1 = player;
@@ -1231,7 +1231,7 @@
     @SmallTest
     public void testSetAndGetRepeatMode() throws Exception {
         final TestUtils.Monitor onRepeatModeChangedMonitor = new TestUtils.Monitor();
-        XMediaPlayer.PlayerCallback callback = new XMediaPlayer.PlayerCallback() {
+        MediaPlayer.PlayerCallback callback = new MediaPlayer.PlayerCallback() {
             @Override
             public void onRepeatModeChanged(SessionPlayer2 player, int repeatMode) {
                 mPlayerCbArg1 = player;
@@ -1287,6 +1287,20 @@
     @SmallTest
     public void testSetPlaylist() throws Exception {
         List<MediaItem2> playlist = createPlaylist(10);
+        try {
+            mPlayer.setPlaylist(null, null);
+            fail();
+        } catch (Exception e) {
+            // pass-through
+        }
+        try {
+            List<MediaItem2> list = new ArrayList<>();
+            list.add(null);
+            mPlayer.setPlaylist(list, null);
+            fail();
+        } catch (Exception e) {
+            // pass-through
+        }
         ListenableFuture<PlayerResult> future = mPlayer.setPlaylist(playlist, null);
         PlayerResult result = future.get();
         assertEquals(RESULT_CODE_SUCCESS, result.getResultCode());
@@ -1429,7 +1443,7 @@
         MediaItem2 item = createMediaItem(100);
 
         final TestUtils.Monitor onCurrentMediaItemChangedMonitor = new TestUtils.Monitor();
-        XMediaPlayer.PlayerCallback callback = new XMediaPlayer.PlayerCallback() {
+        MediaPlayer.PlayerCallback callback = new MediaPlayer.PlayerCallback() {
             @Override
             public void onCurrentMediaItemChanged(SessionPlayer2 player, MediaItem2 item) {
                 onCurrentMediaItemChangedMonitor.signal();
@@ -1449,7 +1463,7 @@
         List<MediaItem2> playlist = createPlaylist(listSize);
 
         final TestUtils.Monitor onCurrentMediaItemChangedMonitor = new TestUtils.Monitor();
-        XMediaPlayer.PlayerCallback callback = new XMediaPlayer.PlayerCallback() {
+        MediaPlayer.PlayerCallback callback = new MediaPlayer.PlayerCallback() {
             @Override
             public void onCurrentMediaItemChanged(SessionPlayer2 player, MediaItem2 item) {
                 onCurrentMediaItemChangedMonitor.signal();
@@ -1465,9 +1479,7 @@
     private MediaItem2 createMediaItem(int key) throws Exception {
         AssetFileDescriptor afd = mResources.openRawResourceFd(R.raw.testvideo);
         return new FileMediaItem2.Builder(
-                afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength())
-                .setMediaId("TEST_MEDIA_" + key)
-                .build();
+                afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength()).build();
     }
 
     private List<MediaItem2> createPlaylist(int size) throws Exception {
diff --git a/media2/src/androidTest/java/androidx/media2/XMediaPlayerTestBase.java b/media2/src/androidTest/java/androidx/media2/MediaPlayerTestBase.java
similarity index 93%
rename from media2/src/androidTest/java/androidx/media2/XMediaPlayerTestBase.java
rename to media2/src/androidTest/java/androidx/media2/MediaPlayerTestBase.java
index a13b3a3..cef0dedf 100644
--- a/media2/src/androidTest/java/androidx/media2/XMediaPlayerTestBase.java
+++ b/media2/src/androidTest/java/androidx/media2/MediaPlayerTestBase.java
@@ -43,10 +43,10 @@
 import java.util.concurrent.Executors;
 
 /**
- * Base class for {@link XMediaPlayer} tests.
+ * Base class for {@link MediaPlayer} tests.
  */
-abstract class XMediaPlayerTestBase extends MediaTestBase {
-    private static final String TAG = "XMediaPlayerTest";
+abstract class MediaPlayerTestBase extends MediaTestBase {
+    private static final String TAG = "MediaPlayerTest";
 
     @Rule
     public ActivityTestRule<MediaStubActivity> mActivityRule =
@@ -56,7 +56,7 @@
     Resources mResources;
     ExecutorService mExecutor;
 
-    XMediaPlayer mPlayer;
+    MediaPlayer mPlayer;
     MediaStubActivity mActivity;
     Instrumentation mInstrumentation;
 
@@ -75,11 +75,11 @@
         try {
             mActivityRule.runOnUiThread(new Runnable() {
                 public void run() {
-                    mPlayer = new XMediaPlayer(mActivity);
+                    mPlayer = new MediaPlayer(mActivity);
                 }
             });
         } catch (Throwable e) {
-            Log.e(TAG, "Failed to instantiate XMediaPlayer", e);
+            Log.e(TAG, "Failed to instantiate MediaPlayer", e);
             fail(e.getMessage());
         }
         mContext = mActivityRule.getActivity();
diff --git a/media2/src/androidTest/java/androidx/media2/XMediaPlayer_AudioFocusTest.java b/media2/src/androidTest/java/androidx/media2/MediaPlayer_AudioFocusTest.java
similarity index 94%
rename from media2/src/androidTest/java/androidx/media2/XMediaPlayer_AudioFocusTest.java
rename to media2/src/androidTest/java/androidx/media2/MediaPlayer_AudioFocusTest.java
index 54bc1f0..5501575 100644
--- a/media2/src/androidTest/java/androidx/media2/XMediaPlayer_AudioFocusTest.java
+++ b/media2/src/androidTest/java/androidx/media2/MediaPlayer_AudioFocusTest.java
@@ -53,7 +53,6 @@
 import androidx.media.AudioAttributesCompat;
 import androidx.media2.test.R;
 import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.FlakyTest;
 import androidx.test.filters.MediumTest;
 import androidx.test.filters.SdkSuppress;
 import androidx.test.runner.AndroidJUnit4;
@@ -72,7 +71,7 @@
 import java.util.concurrent.TimeUnit;
 
 /**
- * Tests {@link XMediaPlayer} for audio focus and noisy intent handling.
+ * Tests {@link MediaPlayer} for audio focus and noisy intent handling.
  * <p>
  * This may be flaky test because another app including system component may take audio focus.
  */
@@ -80,7 +79,7 @@
 @RunWith(AndroidJUnit4.class)
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.P)
 @MediumTest
-public class XMediaPlayer_AudioFocusTest extends XMediaPlayerTestBase {
+public class MediaPlayer_AudioFocusTest extends MediaPlayerTestBase {
     private static final int WAIT_TIME_MS = 2000;
 
     static TestUtils.SyncHandler sHandler;
@@ -91,12 +90,12 @@
 
     @BeforeClass
     public static void setUpThread() {
-        synchronized (XMediaPlayer_AudioFocusTest.class) {
+        synchronized (MediaPlayer_AudioFocusTest.class) {
             if (sHandler != null) {
                 return;
             }
             prepareLooper();
-            HandlerThread handlerThread = new HandlerThread("XMediaPlayer_AudioFocusTest");
+            HandlerThread handlerThread = new HandlerThread("MediaPlayer_AudioFocusTest");
             handlerThread.start();
             sHandler = new TestUtils.SyncHandler(handlerThread.getLooper());
             sHandlerExecutor = new Executor() {
@@ -124,7 +123,7 @@
 
     @AfterClass
     public static void cleanUpThread() {
-        synchronized (XMediaPlayer_AudioFocusTest.class) {
+        synchronized (MediaPlayer_AudioFocusTest.class) {
             if (sHandler == null) {
                 return;
             }
@@ -159,7 +158,7 @@
                 .setContentType(contentType).setUsage(usage).build();
     }
 
-    private void sendNoisyIntent(XMediaPlayer player) {
+    private void sendNoisyIntent(MediaPlayer player) {
         // We cannot use Context.sendBroadcast() because it throws SecurityException for such
         // framework related intent.
         Intent intent = new Intent(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
@@ -216,7 +215,7 @@
         try {
             mInstrumentation.runOnMainSync(new Runnable() {
                 public void run() {
-                    mPlayer = new XMediaPlayer(mActivity) {
+                    mPlayer = new MediaPlayer(mActivity) {
                         @Override
                         public ListenableFuture<PlayerResult> setPlayerVolume(float volume) {
                             if (volume < getMaxPlayerVolume()) {
@@ -254,14 +253,13 @@
     }
 
     @Test
-    @FlakyTest(bugId = 117352673)
     public void testNoisyIntent_pausePlaybackForMedia() throws Exception {
         prepareLooper();
 
         testPausedAfterAction(createAudioAttributes(CONTENT_TYPE_MUSIC, USAGE_MEDIA),
                 new PlayerRunnable() {
                     @Override
-                    public void run(XMediaPlayer player) {
+                    public void run(MediaPlayer player) {
                         // Noisy intent would pause for USAGE_MEDIA.
                         sendNoisyIntent(player);
                     }
@@ -269,14 +267,13 @@
     }
 
     @Test
-    @FlakyTest(bugId = 117107087)
     public void testNoisyIntent_lowerVolumeForGame() throws Exception {
         prepareLooper();
 
         testDuckedAfterAction(createAudioAttributes(CONTENT_TYPE_MUSIC, USAGE_GAME),
                 new PlayerRunnable() {
                     @Override
-                    public void run(XMediaPlayer player) {
+                    public void run(MediaPlayer player) {
                         // Noisy intent would duck for USAGE_GAME.
                         sendNoisyIntent(player);
                     }
@@ -400,7 +397,7 @@
         testPausedAfterAction(createAudioAttributes(CONTENT_TYPE_MUSIC, USAGE_MEDIA),
                 new PlayerRunnable() {
                     @Override
-                    public void run(XMediaPlayer player) throws InterruptedException {
+                    public void run(MediaPlayer player) throws InterruptedException {
                         // Somebody else has request audio focus.
                         // Session should lose audio focus and pause playback.
                         requestAudioFocus(AUDIOFOCUS_GAIN_TRANSIENT);
@@ -415,7 +412,7 @@
         testPausedAfterAction(createAudioAttributes(CONTENT_TYPE_SPEECH, USAGE_MEDIA),
                 new PlayerRunnable() {
                     @Override
-                    public void run(XMediaPlayer player) throws InterruptedException {
+                    public void run(MediaPlayer player) throws InterruptedException {
                         // Although ducking is possible, CONTENT_TYPE_SPEECH should prefer pause.
                         requestAudioFocus(AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
                     }
@@ -434,7 +431,7 @@
         testDuckedAfterAction(createAudioAttributes(CONTENT_TYPE_MUSIC, USAGE_MEDIA),
                 new PlayerRunnable() {
                     @Override
-                    public void run(XMediaPlayer player) throws InterruptedException {
+                    public void run(MediaPlayer player) throws InterruptedException {
                         // This will trigger duck (lower volume).
                         requestAudioFocus(AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
                     }
@@ -443,7 +440,7 @@
 
     @FunctionalInterface
     private interface PlayerRunnable {
-        void run(XMediaPlayer player) throws InterruptedException;
+        void run(MediaPlayer player) throws InterruptedException;
     }
 
     private class AudioFocusListener implements OnAudioFocusChangeListener {
diff --git a/media2/src/androidTest/java/androidx/media2/MediaSession2Test.java b/media2/src/androidTest/java/androidx/media2/MediaSession2Test.java
index 36b924f..e1350d2 100644
--- a/media2/src/androidTest/java/androidx/media2/MediaSession2Test.java
+++ b/media2/src/androidTest/java/androidx/media2/MediaSession2Test.java
@@ -333,7 +333,7 @@
     @Test
     public void testSetPlaylist() {
         prepareLooper();
-        final List<MediaItem2> list = TestUtils.createPlaylist(2);
+        final List<MediaItem2> list = TestUtils.createMediaItems(2);
         mSession.getPlayer().setPlaylist(list, null);
         assertTrue(mPlayer.mSetPlaylistCalled);
         assertSame(list, mPlayer.mPlaylist);
@@ -343,7 +343,7 @@
     @Test
     public void testGetPlaylist() {
         prepareLooper();
-        final List<MediaItem2> list = TestUtils.createPlaylist(2);
+        final List<MediaItem2> list = TestUtils.createMediaItems(2);
         mPlayer.mPlaylist = list;
         assertEquals(list, mSession.getPlayer().getPlaylist());
     }
@@ -565,7 +565,7 @@
     public void testSendCustomCommand() throws InterruptedException {
         prepareLooper();
         final SessionCommand2 testCommand = new SessionCommand2(
-                SessionCommand2.COMMAND_CODE_PLAYER_PREFETCH);
+                SessionCommand2.COMMAND_CODE_PLAYER_PREPARE);
         final Bundle testArgs = new Bundle();
         testArgs.putString("args", "testSendCustomAction");
 
diff --git a/media2/src/androidTest/java/androidx/media2/MockMediaLibraryService2.java b/media2/src/androidTest/java/androidx/media2/MockMediaLibraryService2.java
index 3e7a19b..cc02c0e 100644
--- a/media2/src/androidTest/java/androidx/media2/MockMediaLibraryService2.java
+++ b/media2/src/androidTest/java/androidx/media2/MockMediaLibraryService2.java
@@ -19,8 +19,10 @@
 import static androidx.media2.MediaLibraryService2.LibraryResult.RESULT_CODE_BAD_VALUE;
 import static androidx.media2.MediaLibraryService2.LibraryResult.RESULT_CODE_INVALID_STATE;
 import static androidx.media2.MediaLibraryService2.LibraryResult.RESULT_CODE_SUCCESS;
-import static androidx.media2.MediaMetadata2.FLAG_BROWSABLE;
+import static androidx.media2.MediaMetadata2.BROWSABLE_TYPE_MIXED;
+import static androidx.media2.MediaMetadata2.METADATA_KEY_BROWSABLE;
 import static androidx.media2.MediaMetadata2.METADATA_KEY_MEDIA_ID;
+import static androidx.media2.MediaMetadata2.METADATA_KEY_PLAYABLE;
 import static androidx.media2.TestUtils.assertLibraryParamsEquals;
 
 import static org.junit.Assert.assertEquals;
@@ -75,9 +77,11 @@
     public static final int SEARCH_RESULT_COUNT = 50;
 
     static {
-        ROOT_ITEM = new MediaItem2.Builder(FLAG_BROWSABLE)
+        ROOT_ITEM = new MediaItem2.Builder()
                 .setMetadata(new MediaMetadata2.Builder()
-                        .putString(METADATA_KEY_MEDIA_ID, "rootId").build()).build();
+                        .putString(METADATA_KEY_MEDIA_ID, "rootId")
+                        .putLong(METADATA_KEY_BROWSABLE, BROWSABLE_TYPE_MIXED)
+                        .putLong(METADATA_KEY_PLAYABLE, 1).build()).build();
         ROOT_PARAMS_EXTRA = new Bundle();
         ROOT_PARAMS_EXTRA.putString(ID, ID);
         ROOT_PARAMS = new LibraryParams.Builder().setExtras(ROOT_PARAMS_EXTRA).build();
@@ -274,10 +278,13 @@
     }
 
     private MediaItem2 createMediaItem(String mediaId) {
-        return new MediaItem2.Builder(MediaItem2.FLAG_PLAYABLE)
-                .setMediaId(mediaId)
-                .setMetadata(new MediaMetadata2.Builder()
-                                .putString(METADATA_KEY_MEDIA_ID, mediaId)
+        return new MediaItem2.Builder()
+                .setMetadata(
+                        new MediaMetadata2.Builder()
+                                .putString(MediaMetadata2.METADATA_KEY_MEDIA_ID, mediaId)
+                                .putLong(MediaMetadata2.METADATA_KEY_BROWSABLE,
+                                        MediaMetadata2.BROWSABLE_TYPE_MIXED)
+                                .putLong(MediaMetadata2.METADATA_KEY_PLAYABLE, 1)
                                 .build())
                 .build();
     }
diff --git a/media2/src/androidTest/java/androidx/media2/MockPlayer.java b/media2/src/androidTest/java/androidx/media2/MockPlayer.java
index 242188d..12649ff 100644
--- a/media2/src/androidTest/java/androidx/media2/MockPlayer.java
+++ b/media2/src/androidTest/java/androidx/media2/MockPlayer.java
@@ -268,6 +268,7 @@
     @Override
     public ListenableFuture<PlayerResult> setMediaItem(MediaItem2 item) {
         mItem = item;
+        mCurrentMediaItem = item;
         ArrayList list = new ArrayList<>();
         list.add(item);
         return setPlaylist(list, null);
diff --git a/media2/src/androidTest/java/androidx/media2/Rating2Test.java b/media2/src/androidTest/java/androidx/media2/Rating2Test.java
index cf50472..f92f8d4 100644
--- a/media2/src/androidTest/java/androidx/media2/Rating2Test.java
+++ b/media2/src/androidTest/java/androidx/media2/Rating2Test.java
@@ -25,7 +25,6 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 import androidx.versionedparcelable.ParcelImpl;
-import androidx.versionedparcelable.ParcelUtils;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -108,12 +107,12 @@
     }
 
     private Rating2 writeToParcelAndCreateRating2(Rating2 rating2) {
-        ParcelImpl parcelImpl = (ParcelImpl) ParcelUtils.toParcelable(rating2);
+        ParcelImpl parcelImpl = MediaUtils2.toParcelable(rating2);
         Parcel parcel = Parcel.obtain();
         parcelImpl.writeToParcel(parcel, 0);
         parcel.setDataPosition(0);
         ParcelImpl newParcelImpl = ParcelImpl.CREATOR.createFromParcel(parcel);
         parcel.recycle();
-        return ParcelUtils.fromParcelable(newParcelImpl);
+        return MediaUtils2.fromParcelable(newParcelImpl);
     }
 }
diff --git a/media2/src/androidTest/java/androidx/media2/TestUtils.java b/media2/src/androidTest/java/androidx/media2/TestUtils.java
index 5427d1d..34b4ef2 100644
--- a/media2/src/androidTest/java/androidx/media2/TestUtils.java
+++ b/media2/src/androidTest/java/androidx/media2/TestUtils.java
@@ -101,19 +101,25 @@
     }
 
     /**
-     * Create a playlist for testing purpose
+     * Create a list of media items for testing purpose
      * <p>
      * Caller's method name will be used for prefix of each media item's media id.
      *
      * @param size list size
-     * @return the newly created playlist
+     * @return the newly created media item list
      */
-    public static List<MediaItem2> createPlaylist(int size) {
+    public static List<MediaItem2> createMediaItems(int size) {
         final List<MediaItem2> list = new ArrayList<>();
         String caller = Thread.currentThread().getStackTrace()[3].getMethodName();
         for (int i = 0; i < size; i++) {
             MediaItem2 item = new FileMediaItem2.Builder(new FileDescriptor())
-                    .setMediaId(caller + "_item_" + (size + 1))
+                    .setMetadata(new MediaMetadata2.Builder()
+                            .putString(MediaMetadata2.METADATA_KEY_MEDIA_ID,
+                                    caller + "_item_" + (size + 1))
+                            .putLong(MediaMetadata2.METADATA_KEY_BROWSABLE,
+                                    MediaMetadata2.BROWSABLE_TYPE_NONE)
+                            .putLong(MediaMetadata2.METADATA_KEY_PLAYABLE, 1)
+                            .build())
                     .build();
             list.add(item);
         }
@@ -121,6 +127,23 @@
     }
 
     /**
+     * Create a list of media ids for testing purpose
+     * <p>
+     * Caller's method name will be used for prefix of media id.
+     *
+     * @param size list size
+     * @return the newly created ids
+     */
+    public static List<String> createMediaIds(int size) {
+        final List<String> list = new ArrayList<>();
+        String caller = Thread.currentThread().getStackTrace()[3].getMethodName();
+        for (int i = 0; i < size; i++) {
+            list.add(caller + "_item_" + (size + 1));
+        }
+        return list;
+    }
+
+    /**
      * Create a media item with the metadata for testing purpose.
      *
      * @return the newly created media item
@@ -141,18 +164,33 @@
     public static MediaMetadata2 createMetadata() {
         String mediaId = Thread.currentThread().getStackTrace()[3].getMethodName();
         return new MediaMetadata2.Builder()
-                .putString(MediaMetadata2.METADATA_KEY_MEDIA_ID, mediaId).build();
+                .putString(MediaMetadata2.METADATA_KEY_MEDIA_ID, mediaId)
+                .putLong(MediaMetadata2.METADATA_KEY_BROWSABLE, MediaMetadata2.BROWSABLE_TYPE_NONE)
+                .putLong(MediaMetadata2.METADATA_KEY_PLAYABLE, 1)
+                .build();
     }
 
     /**
-     * Create a bundle for testing purpose.
+     * Create a media metadata for testing purpose.
      *
-     * @return the newly created bundle.
+     * @return the newly created media item
      */
-    public static Bundle createTestBundle() {
-        Bundle bundle = new Bundle();
-        bundle.putString("test_key", "test_value");
-        return bundle;
+    public static MediaMetadata2 createMetadata(String mediaId, long duration) {
+        return new MediaMetadata2.Builder()
+                .putString(MediaMetadata2.METADATA_KEY_MEDIA_ID, mediaId)
+                .putLong(MediaMetadata2.METADATA_KEY_DURATION, duration)
+                .putLong(MediaMetadata2.METADATA_KEY_BROWSABLE, MediaMetadata2.BROWSABLE_TYPE_NONE)
+                .putLong(MediaMetadata2.METADATA_KEY_PLAYABLE, 1)
+                .build();
+    }
+
+    /**
+     * Create a {@link MediaItem2} with the id.
+     *
+     * @return the newly created media item.
+     */
+    public static MediaItem2 createMediaItem(String mediaId) {
+        return new MediaItem2.Builder().setMetadata(createMetadata(mediaId, 0)).build();
     }
 
     public static LibraryParams createLibraryParams() {
@@ -185,7 +223,6 @@
             }
 
             assertEquals(aItem.getMediaId(), bItem.getMediaId());
-            assertEquals(aItem.getFlags(), bItem.getFlags());
             TestUtils.assertMetadataEquals(aItem.getMetadata(), bItem.getMetadata());
 
             // Note: Here it does not check whether MediaItem2 are equal,
diff --git a/media2/src/main/aidl/androidx/media2/IMediaController2.aidl b/media2/src/main/aidl/androidx/media2/IMediaController2.aidl
index 1a489a8..df889c8 100644
--- a/media2/src/main/aidl/androidx/media2/IMediaController2.aidl
+++ b/media2/src/main/aidl/androidx/media2/IMediaController2.aidl
@@ -35,8 +35,8 @@
     void onPlayerStateChanged(long eventTimeMs, long positionMs, int state) = 1;
     void onPlaybackSpeedChanged(long eventTimeMs, long positionMs, float speed) = 2;
     void onBufferingStateChanged(in ParcelImpl item, int state, long bufferedPositionMs) = 3;
-    void onPlaylistChanged(in ParcelImplListSlice listSlice, in Bundle metadata) = 4;
-    void onPlaylistMetadataChanged(in Bundle metadata) = 5;
+    void onPlaylistChanged(in ParcelImplListSlice listSlice, in ParcelImpl metadata) = 4;
+    void onPlaylistMetadataChanged(in ParcelImpl metadata) = 5;
     void onPlaybackInfoChanged(in ParcelImpl playbackInfo) = 6;
     void onRepeatModeChanged(int repeatMode) = 7;
     void onShuffleModeChanged(int shuffleMode) = 8;
diff --git a/media2/src/main/aidl/androidx/media2/IMediaSession2.aidl b/media2/src/main/aidl/androidx/media2/IMediaSession2.aidl
index 952aa8e..ca995f6 100644
--- a/media2/src/main/aidl/androidx/media2/IMediaSession2.aidl
+++ b/media2/src/main/aidl/androidx/media2/IMediaSession2.aidl
@@ -39,56 +39,56 @@
 
     void play(IMediaController2 caller, int seq) = 4;
     void pause(IMediaController2 caller, int seq) = 5;
-    void prefetch(IMediaController2 caller, int seq) = 7;
+    void prepare(IMediaController2 caller, int seq) = 7;
     void fastForward(IMediaController2 caller, int seq) = 8;
     void rewind(IMediaController2 caller, int seq) = 9;
-    void seekTo(IMediaController2 caller, int seq, long pos) = 10;
+    void skipForward(IMediaController2 caller, int seq) = 10;
+    void skipBackward(IMediaController2 caller, int seq) = 11;
+    void seekTo(IMediaController2 caller, int seq, long pos) = 12;
     void onCustomCommand(IMediaController2 caller, int seq, in ParcelImpl sessionCommand2,
-            in Bundle args) = 11;
-    void prefetchFromUri(IMediaController2 caller, int seq, in Uri uri, in Bundle extras) = 12;
-    void prefetchFromSearch(IMediaController2 caller, int seq, String query, in Bundle extras) = 13;
-    void prefetchFromMediaId(IMediaController2 caller, int seq, String mediaId,
-            in Bundle extras) = 14;
-    void playFromUri(IMediaController2 caller, int seq, in Uri uri, in Bundle extras) = 15;
-    void playFromSearch(IMediaController2 caller, int seq, String query, in Bundle extras) = 16;
-    void playFromMediaId(IMediaController2 caller, int seq, String mediaId, in Bundle extras) = 17;
-    void setRating(IMediaController2 caller, int seq, String mediaId, in ParcelImpl rating2) = 18;
-    void setPlaybackSpeed(IMediaController2 caller, int seq, float speed) = 19;
+            in Bundle args) = 13;
+    void prepareFromUri(IMediaController2 caller, int seq, in Uri uri, in Bundle extras) = 14;
+    void prepareFromSearch(IMediaController2 caller, int seq, String query, in Bundle extras) = 15;
+    void prepareFromMediaId(IMediaController2 caller, int seq, String mediaId,
+            in Bundle extras) = 16;
+    void playFromUri(IMediaController2 caller, int seq, in Uri uri, in Bundle extras) = 17;
+    void playFromSearch(IMediaController2 caller, int seq, String query, in Bundle extras) = 18;
+    void playFromMediaId(IMediaController2 caller, int seq, String mediaId, in Bundle extras) = 19;
+    void setRating(IMediaController2 caller, int seq, String mediaId, in ParcelImpl rating2) = 20;
+    void setPlaybackSpeed(IMediaController2 caller, int seq, float speed) = 21;
 
-    void setPlaylist(IMediaController2 caller, int seq, in ParcelImplListSlice listSlice,
-            in Bundle metadata) = 20;
-    void setMediaItem(IMediaController2 caller, int seq, in ParcelImpl mediaItem) = 40;
-    void updatePlaylistMetadata(IMediaController2 caller, int seq, in Bundle metadata) = 21;
-    void addPlaylistItem(IMediaController2 caller, int seq, int index,
-            in ParcelImpl mediaItem) = 22;
-    void removePlaylistItem(IMediaController2 caller, int seq, in ParcelImpl mediaItem) = 23;
-    void replacePlaylistItem(IMediaController2 caller, int seq, int index,
-            in ParcelImpl mediaItem) = 24;
-    void skipToPlaylistItem(IMediaController2 caller, int seq, in ParcelImpl mediaItem) = 25;
-    void skipToPreviousItem(IMediaController2 caller, int seq) = 26;
-    void skipToNextItem(IMediaController2 caller, int seq) = 27;
-    void setRepeatMode(IMediaController2 caller, int seq, int repeatMode) = 28;
-    void setShuffleMode(IMediaController2 caller, int seq, int shuffleMode) = 29;
+    void setPlaylist(IMediaController2 caller, int seq, in List<String> list,
+            in ParcelImpl metadata) = 22;
+    void setMediaItem(IMediaController2 caller, int seq, String mediaId) = 23;
+    void updatePlaylistMetadata(IMediaController2 caller, int seq, in ParcelImpl metadata) = 24;
+    void addPlaylistItem(IMediaController2 caller, int seq, int index, String mediaId) = 25;
+    void removePlaylistItem(IMediaController2 caller, int seq, int index) = 26;
+    void replacePlaylistItem(IMediaController2 caller, int seq, int index, String mediaId) = 27;
+    void skipToPlaylistItem(IMediaController2 caller, int seq, int index) = 28;
+    void skipToPreviousItem(IMediaController2 caller, int seq) = 29;
+    void skipToNextItem(IMediaController2 caller, int seq) = 30;
+    void setRepeatMode(IMediaController2 caller, int seq, int repeatMode) = 31;
+    void setShuffleMode(IMediaController2 caller, int seq, int shuffleMode) = 32;
 
-    void subscribeRoutesInfo(IMediaController2 caller, int seq) = 30;
-    void unsubscribeRoutesInfo(IMediaController2 caller, int seq) = 31;
-    void selectRoute(IMediaController2 caller, int seq, in Bundle route) = 32;
+    void subscribeRoutesInfo(IMediaController2 caller, int seq) = 33;
+    void unsubscribeRoutesInfo(IMediaController2 caller, int seq) = 34;
+    void selectRoute(IMediaController2 caller, int seq, in Bundle route) = 35;
 
     void onControllerResult(IMediaController2 caller, int seq,
-            in ParcelImpl controllerResult) = 41;
+            in ParcelImpl controllerResult) = 46;
 
     //////////////////////////////////////////////////////////////////////////////////////////////
     // library service specific
     //////////////////////////////////////////////////////////////////////////////////////////////
-    void getLibraryRoot(IMediaController2 caller, int seq, in ParcelImpl libraryParams) = 33;
-    void getItem(IMediaController2 caller, int seq, String mediaId) = 34;
+    void getLibraryRoot(IMediaController2 caller, int seq, in ParcelImpl libraryParams) = 37;
+    void getItem(IMediaController2 caller, int seq, String mediaId) = 38;
     void getChildren(IMediaController2 caller, int seq, String parentId, int page, int pageSize,
-            in ParcelImpl libraryParams) = 35;
-    void search(IMediaController2 caller, int seq, String query, in ParcelImpl libraryParams) = 36;
+            in ParcelImpl libraryParams) = 39;
+    void search(IMediaController2 caller, int seq, String query, in ParcelImpl libraryParams) = 40;
     void getSearchResult(IMediaController2 caller, int seq, String query, int page, int pageSize,
-            in ParcelImpl libraryParams) = 37;
+            in ParcelImpl libraryParams) = 41;
     void subscribe(IMediaController2 caller, int seq, String parentId,
-            in ParcelImpl libraryParams) = 38;
-    void unsubscribe(IMediaController2 caller, int seq, String parentId) = 39;
-    // Next Id : 42
+            in ParcelImpl libraryParams) = 42;
+    void unsubscribe(IMediaController2 caller, int seq, String parentId) = 43;
+    // Next Id : 44
 }
diff --git a/media2/src/main/java/androidx/media2/AudioFocusHandler.java b/media2/src/main/java/androidx/media2/AudioFocusHandler.java
index 0410120..7d41b71 100644
--- a/media2/src/main/java/androidx/media2/AudioFocusHandler.java
+++ b/media2/src/main/java/androidx/media2/AudioFocusHandler.java
@@ -17,10 +17,7 @@
 package androidx.media2;
 
 import static androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE;
-import static androidx.media2.SessionPlayer2.PLAYER_STATE_ERROR;
-import static androidx.media2.SessionPlayer2.PLAYER_STATE_IDLE;
 import static androidx.media2.SessionPlayer2.PLAYER_STATE_PAUSED;
-import static androidx.media2.SessionPlayer2.PLAYER_STATE_PLAYING;
 
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -34,8 +31,6 @@
 import androidx.annotation.RestrictTo;
 import androidx.annotation.RestrictTo.Scope;
 import androidx.annotation.VisibleForTesting;
-import androidx.core.content.ContextCompat;
-import androidx.core.util.ObjectsCompat;
 import androidx.media.AudioAttributesCompat;
 
 /**
@@ -56,38 +51,43 @@
     private static final boolean DEBUG = true;
 
     interface AudioFocusHandlerImpl {
-        boolean onPlayRequested();
-        void onPauseRequested();
+        boolean onPlay();
+        void onPause();
+        void onReset();
         void close();
         void sendIntent(Intent intent);
     }
 
     private final AudioFocusHandlerImpl mImpl;
 
-    AudioFocusHandler(Context context, XMediaPlayer player) {
+    AudioFocusHandler(Context context, MediaPlayer player) {
         mImpl = new AudioFocusHandlerImplBase(context, player);
     }
 
     /**
-     * Should be called when the {@link MediaSession2#play()} is called. Returns whether the play()
+     * Should be called when the {@link MediaPlayer#play()} is called. Returns whether the play()
      * can be proceed.
-     * <p>
-     * This matches with the Session.Callback#onPlay() written in the guideline.
      *
      * @return {@code true} if it's OK to start playback because audio focus was successfully
-     *         granted or audio focus isn't needed for starting playback. {@code false} otherwise.
-     *         (i.e. Audio focus is needed for starting playback but failed)
+     * granted or audio focus isn't needed for starting playback. {@code false} otherwise.
+     * (i.e. Audio focus is needed for starting playback but failed)
      */
-    public boolean onPlayRequested() {
-        return mImpl.onPlayRequested();
+    public boolean onPlay() {
+        return mImpl.onPlay();
     }
 
     /**
-     * Should be called when the {@link MediaSession2#pause()} is called. Returns whether the
-     * pause() can be proceed.
+     * Called when the {@link MediaPlayer#pause()} is called.
      */
-    public void onPauseRequested() {
-        mImpl.onPauseRequested();
+    public void onPause() {
+        mImpl.onPause();
+    }
+
+    /**
+     * Called when the {@link MediaPlayer#reset()} is called.
+     */
+    public void onReset() {
+        mImpl.onReset();
     }
 
     /**
@@ -106,20 +106,19 @@
         mImpl.sendIntent(intent);
     }
 
-    private static class AudioFocusHandlerImplBase extends SessionPlayer2.PlayerCallback
-            implements AudioFocusHandlerImpl {
+    private static class AudioFocusHandlerImplBase implements AudioFocusHandlerImpl {
         // Value is from the {@link AudioFocusRequest} as follows
         // 'A typical attenuation by the “ducked” application is a factor of 0.2f (or -14dB), that
         // can for instance be applied with MediaPlayer.setVolume(0.2f) when using this class for
         // playback.'
         private static final float VOLUME_DUCK_FACTOR = 0.2f;
-        private final BroadcastReceiver mBecomingNoisyIntentReceiver = new NoisyIntentReceiver();
+        private final BroadcastReceiver mBecomingNoisyReceiver = new BecomingNoisyReceiver();
         private final IntentFilter mIntentFilter =
                 new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
         private final OnAudioFocusChangeListener mAudioFocusListener = new AudioFocusListener();
         final Object mLock = new Object();
         private final Context mContext;
-        final XMediaPlayer mPlayer;
+        final MediaPlayer mPlayer;
         private final AudioManager mAudioManager;
 
         @GuardedBy("mLock")
@@ -129,12 +128,11 @@
         @GuardedBy("mLock")
         boolean mResumeWhenAudioFocusGain;
         @GuardedBy("mLock")
-        boolean mHasRegisteredReceiver;
+        boolean mBecomingNoisyReceiverRegistered;
 
-        AudioFocusHandlerImplBase(Context context, XMediaPlayer player) {
+        AudioFocusHandlerImplBase(Context context, MediaPlayer player) {
             mContext = context;
             mPlayer = player;
-            mPlayer.registerPlayerCallback(ContextCompat.getMainExecutor(context), this);
 
             // Cannot use session.getContext() because session's impl isn't initialized at this
             // moment.
@@ -142,129 +140,54 @@
         }
 
         @Override
-        public boolean onPlayRequested() {
+        public boolean onPlay() {
             final AudioAttributesCompat attrs = mPlayer.getAudioAttributes();
             boolean result = true;
             synchronized (mLock) {
+                mAudioAttributes = attrs;
+                // Checks whether the audio attributes is {@code null}, to check indirectly whether
+                // the media item has audio track.
                 if (attrs == null) {
-                    mAudioAttributes = null;
+                    // No sound.
                     abandonAudioFocusLocked();
+                    unregisterBecomingNoisyReceiverLocked();
                 } else {
-                    // Keep cache of the audio attributes. Otherwise audio attributes may be changed
-                    // between the audio focus request and audio focus change, resulting the
-                    // unexpected situation.
-                    mAudioAttributes = attrs;
                     result = requestAudioFocusLocked();
+                    if (result) {
+                        registerBecomingNoisyReceiverLocked();
+                    }
                 }
             }
-            if (attrs == null) {
-                mPlayer.setPlayerVolume(0);
-            }
             return result;
         }
 
         @Override
-        public void onPauseRequested() {
+        public void onPause() {
             synchronized (mLock) {
                 mResumeWhenAudioFocusGain = false;
-            }
-        }
-
-        /**
-         * Check we need to abandon/request audio focus when playback state becomes playing. It's
-         * needed to handle following cases.
-         *   1. Audio attribute has changed between {@link SessionPlayer2#play} and actual
-         *      start of playback. Note that {@link MediaPlayer2} only allows changing audio
-         *      attributes in IDLE state, so such issue wouldn't happen.
-         *   2. Or, playback is started without MediaSession2#play().
-         * <p>
-         * If there's no huge issue, then register noisy intent receiver here.
-         */
-        private void onPlayingNotLocked() {
-            final AudioAttributesCompat attrs = mPlayer.getAudioAttributes();
-            final int expectedFocusGain;
-            boolean pauseNeeded = false;
-            synchronized (mLock) {
-                expectedFocusGain = convertAudioAttributesToFocusGain(attrs);
-                if (ObjectsCompat.equals(mAudioAttributes, attrs)
-                        && expectedFocusGain == mCurrentFocusGainType) {
-                    // No change in audio attributes, and audio focus is granted as expected.
-                    // Register noisy intent if it has an audio attribute (i.e. has sound).
-                    if (attrs != null) {
-                        registerReceiverLocked();
-                    }
-                    return;
-                }
-                Log.w(TAG, "Expected " + mAudioAttributes + " and audioFocusGainType="
-                        + mCurrentFocusGainType + " when playback is started, but actually "
-                        + attrs
-                        + " and audioFocusGainType=" + mCurrentFocusGainType + ". Use"
-                        + " MediaSession2#play() for starting playback.");
-                mAudioAttributes = attrs;
-                if (mCurrentFocusGainType != expectedFocusGain) {
-                    // Note: Calling AudioManager#requestAudioFocus() again with the same
-                    //       listener but different focus gain type only updates the focus gain
-                    //       type.
-                    if (expectedFocusGain == AudioManager.AUDIOFOCUS_NONE) {
-                        abandonAudioFocusLocked();
-                    } else {
-                        if (requestAudioFocusLocked()) {
-                            registerReceiverLocked();
-                        } else {
-                            Log.e(TAG, "Playback is started without audio focus, and requesting"
-                                    + " audio focus is failed. Forcefully pausing playback");
-                            pauseNeeded = true;
-                        }
-                    }
-                }
-            }
-            if (attrs == null) {
-                // If attributes becomes null (i.e. no sound)
-                mPlayer.setPlayerVolume(0);
-            }
-            if (pauseNeeded) {
-                mPlayer.pause();
+                unregisterBecomingNoisyReceiverLocked();
             }
         }
 
         @Override
-        public void onPlayerStateChanged(SessionPlayer2 player, int playerState) {
-            switch (playerState) {
-                case PLAYER_STATE_IDLE: {
-                    synchronized (mLock) {
-                        abandonAudioFocusLocked();
-                    }
-                    break;
-                }
-                case PLAYER_STATE_PAUSED: {
-                    synchronized (mLock) {
-                        unregisterReceiverLocked();
-                    }
-                    break;
-                }
-                case PLAYER_STATE_PLAYING: {
-                    onPlayingNotLocked();
-                    break;
-                }
-                case PLAYER_STATE_ERROR: {
-                    close();
-                    break;
-                }
+        public void onReset() {
+            synchronized (mLock) {
+                abandonAudioFocusLocked();
+                unregisterBecomingNoisyReceiverLocked();
             }
         }
 
         @Override
         public void close() {
-            mPlayer.unregisterPlayerCallback(this);
             synchronized (mLock) {
-                unregisterReceiverLocked();
+                unregisterBecomingNoisyReceiverLocked();
                 abandonAudioFocusLocked();
             }
         }
 
         @Override
         public void sendIntent(Intent intent) {
-            mBecomingNoisyIntentReceiver.onReceive(mContext, intent);
+            mBecomingNoisyReceiver.onReceive(mContext, intent);
         }
 
         /**
@@ -321,29 +244,29 @@
         }
 
         @GuardedBy("mLock")
-        private void registerReceiverLocked() {
-            if (mHasRegisteredReceiver) {
+        private void registerBecomingNoisyReceiverLocked() {
+            if (mBecomingNoisyReceiverRegistered) {
                 return;
             }
             if (DEBUG) {
-                Log.d(TAG, "registering noisy intent");
+                Log.d(TAG, "registering becoming noisy receiver");
             }
             // Registering the receiver multiple-times may not be allowed for newer platform.
             // Register only when it's not registered.
-            mContext.registerReceiver(mBecomingNoisyIntentReceiver, mIntentFilter);
-            mHasRegisteredReceiver = true;
+            mContext.registerReceiver(mBecomingNoisyReceiver, mIntentFilter);
+            mBecomingNoisyReceiverRegistered = true;
         }
 
         @GuardedBy("mLock")
-        private void unregisterReceiverLocked() {
-            if (!mHasRegisteredReceiver) {
+        private void unregisterBecomingNoisyReceiverLocked() {
+            if (!mBecomingNoisyReceiverRegistered) {
                 return;
             }
             if (DEBUG) {
-                Log.d(TAG, "unregistering noisy intent");
+                Log.d(TAG, "unregistering becoming noisy receiver");
             }
-            mContext.unregisterReceiver(mBecomingNoisyIntentReceiver);
-            mHasRegisteredReceiver = false;
+            mContext.unregisterReceiver(mBecomingNoisyReceiver);
+            mBecomingNoisyReceiverRegistered = false;
         }
 
         // Converts {@link AudioAttributesCompat} to one of the audio focus request. This follows
@@ -416,44 +339,41 @@
             return AudioManager.AUDIOFOCUS_NONE;
         }
 
-        private class NoisyIntentReceiver extends BroadcastReceiver {
-            NoisyIntentReceiver() {
+        private class BecomingNoisyReceiver extends BroadcastReceiver {
+            BecomingNoisyReceiver() {
             }
 
+            // Note: This is always the main thread, except for the test.
             @Override
             public void onReceive(Context context, Intent intent) {
-                if (DEBUG) {
-                    Log.d(TAG, "Received noisy intent " + intent);
+                if (!AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
+                    return;
                 }
-                // This is always the main thread, except for the test.
+                final int usage;
                 synchronized (mLock) {
-                    if (!mHasRegisteredReceiver) {
+                    if (DEBUG) {
+                        Log.d(TAG, "Received noisy intent, intent=" + intent + ", registered="
+                                + mBecomingNoisyReceiverRegistered + ", attr=" + mAudioAttributes);
+                    }
+                    if (!mBecomingNoisyReceiverRegistered || mAudioAttributes == null) {
                         return;
                     }
+                    usage = mAudioAttributes.getUsage();
                 }
-                if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
-                    final int usage;
-                    synchronized (mLock) {
-                        if (mAudioAttributes == null) {
-                            return;
-                        }
-                        usage = mAudioAttributes.getUsage();
-                    }
-                    switch (usage) {
-                        case AudioAttributesCompat.USAGE_MEDIA:
-                            // Noisy intent guide says 'In the case of music players, users
-                            // typically expect the playback to be paused.'
-                            mPlayer.pause();
-                            break;
-                        case AudioAttributesCompat.USAGE_GAME:
-                            // Noisy intent guide says 'For gaming apps, you may choose to
-                            // significantly lower the volume instead'.
-                            mPlayer.setPlayerVolume(mPlayer.getPlayerVolume() * VOLUME_DUCK_FACTOR);
-                            break;
-                        default:
-                            // Noisy intent guide didn't say anything more for this. No-op for now.
-                            break;
-                    }
+                switch (usage) {
+                    case AudioAttributesCompat.USAGE_MEDIA:
+                        // Noisy intent guide says 'In the case of music players, users
+                        // typically expect the playback to be paused.'
+                        mPlayer.pause();
+                        break;
+                    case AudioAttributesCompat.USAGE_GAME:
+                        // Noisy intent guide says 'For gaming apps, you may choose to
+                        // significantly lower the volume instead'.
+                        mPlayer.setPlayerVolume(mPlayer.getPlayerVolume() * VOLUME_DUCK_FACTOR);
+                        break;
+                    default:
+                        // Noisy intent guide didn't say anything more for this. No-op for now.
+                        break;
                 }
             }
         }
@@ -473,7 +393,7 @@
                     case AudioManager.AUDIOFOCUS_GAIN:
                         // Regains focus after the LOSS_TRANSIENT or LOSS_TRANSIENT_CAN_DUCK.
                         if (mPlayer.getPlayerState() == PLAYER_STATE_PAUSED) {
-                            // Note: onPlayRequested() will be called again with this.
+                            // Note: onPlay() will be called again with this.
                             synchronized (mLock) {
                                 if (!mResumeWhenAudioFocusGain) {
                                     break;
diff --git a/media2/src/main/java/androidx/media2/BaseResult2.java b/media2/src/main/java/androidx/media2/BaseResult2.java
index 41fc4df..c316ef3 100644
--- a/media2/src/main/java/androidx/media2/BaseResult2.java
+++ b/media2/src/main/java/androidx/media2/BaseResult2.java
@@ -78,6 +78,11 @@
     int getResultCode();
 
     // Subclasses should write its own documentation.
+    // Should use SystemClock#elapsedRealtime() instead of System#currentTimeMillis() because
+    //    1. System#currentTimeMillis() can be unexpectedly set by System#setCurrentTimeMillis() or
+    //       changes in the timezone. So receiver cannot know when the command is finished.
+    //    2. For matching the timestamp with the PlaybackState(Compat) which uses
+    //       SystemClock#elapsedRealtime().
     long getCompletionTime();
 
     // Subclasses should write its own documentation.
diff --git a/media2/src/main/java/androidx/media2/CallbackMediaItem2.java b/media2/src/main/java/androidx/media2/CallbackMediaItem2.java
index 7f10efa..db2aba2 100644
--- a/media2/src/main/java/androidx/media2/CallbackMediaItem2.java
+++ b/media2/src/main/java/androidx/media2/CallbackMediaItem2.java
@@ -19,16 +19,20 @@
 import androidx.annotation.NonNull;
 import androidx.core.util.Preconditions;
 import androidx.versionedparcelable.NonParcelField;
+import androidx.versionedparcelable.ParcelUtils;
 import androidx.versionedparcelable.VersionedParcelize;
 
 /**
  * Structure for media item descriptor for {@link DataSourceCallback2}.
  * <p>
  * Users should use {@link Builder} to create {@link CallbackMediaItem2}.
+ * <p>
+ * You cannot directly send this object across the process through {@link ParcelUtils}. See
+ * {@link MediaItem2} for detail.
  *
  * @see MediaItem2
  */
-@VersionedParcelize
+@VersionedParcelize(isCustom = true)
 public class CallbackMediaItem2 extends MediaItem2 {
     @NonParcelField
     @SuppressWarnings("WeakerAccess") /* synthetic access */
@@ -70,7 +74,6 @@
          * @param dsc2 the DataSourceCallback2 for the media you want to play
          */
         public Builder(@NonNull DataSourceCallback2 dsc2) {
-            super(FLAG_PLAYABLE);
             Preconditions.checkNotNull(dsc2);
             mDataSourceCallback2 = dsc2;
         }
@@ -79,7 +82,7 @@
          * @return A new CallbackMediaItem2 with values supplied by the Builder.
          */
         @Override
-        public CallbackMediaItem2 build() {
+        public @NonNull CallbackMediaItem2 build() {
             return new CallbackMediaItem2(this);
         }
     }
diff --git a/media2/src/main/java/androidx/media2/DataSourceCallback2.java b/media2/src/main/java/androidx/media2/DataSourceCallback2.java
index d057659..fe53777 100644
--- a/media2/src/main/java/androidx/media2/DataSourceCallback2.java
+++ b/media2/src/main/java/androidx/media2/DataSourceCallback2.java
@@ -17,6 +17,8 @@
 
 package androidx.media2;
 
+import androidx.annotation.NonNull;
+
 import java.io.Closeable;
 import java.io.IOException;
 
@@ -49,7 +51,7 @@
      * @throws IOException on fatal errors.
      * @return the number of bytes read, or -1 if there was an error.
      */
-    public abstract int readAt(long position, byte[] buffer, int offset, int size)
+    public abstract int readAt(long position, @NonNull byte[] buffer, int offset, int size)
             throws IOException;
 
     /**
diff --git a/media2/src/main/java/androidx/media2/FileMediaItem2.java b/media2/src/main/java/androidx/media2/FileMediaItem2.java
index 5a32bbc..a2f941f 100644
--- a/media2/src/main/java/androidx/media2/FileMediaItem2.java
+++ b/media2/src/main/java/androidx/media2/FileMediaItem2.java
@@ -19,6 +19,7 @@
 import androidx.annotation.NonNull;
 import androidx.core.util.Preconditions;
 import androidx.versionedparcelable.NonParcelField;
+import androidx.versionedparcelable.ParcelUtils;
 import androidx.versionedparcelable.VersionedParcelize;
 
 import java.io.FileDescriptor;
@@ -27,10 +28,13 @@
  * Structure for media item for a file.
  * <p>
  * Users should use {@link Builder} to create {@link FileMediaItem2}.
+ * <p>
+ * You cannot directly send this object across the process through {@link ParcelUtils}. See
+ * {@link MediaItem2} for detail.
  *
  * @see MediaItem2
  */
-@VersionedParcelize
+@VersionedParcelize(isCustom = true)
 public class FileMediaItem2 extends MediaItem2 {
     /**
      * Used when the length of file descriptor is unknown.
@@ -109,7 +113,6 @@
          * @param fd the FileDescriptor for the file you want to play
          */
         public Builder(@NonNull FileDescriptor fd) {
-            super(FLAG_PLAYABLE);
             Preconditions.checkNotNull(fd);
             mFD = fd;
             mFDOffset = 0;
@@ -129,7 +132,6 @@
          * @param length the length in bytes of the data to be played
          */
         public Builder(@NonNull FileDescriptor fd, long offset, long length) {
-            super(FLAG_PLAYABLE);
             Preconditions.checkNotNull(fd);
             if (offset < 0) {
                 offset = 0;
@@ -146,7 +148,7 @@
          * @return A new FileMediaItem2 with values supplied by the Builder.
          */
         @Override
-        public FileMediaItem2 build() {
+        public @NonNull FileMediaItem2 build() {
             return new FileMediaItem2(this);
         }
     }
diff --git a/media2/src/main/java/androidx/media2/MediaBrowser2.java b/media2/src/main/java/androidx/media2/MediaBrowser2.java
index e87591d..cfd9305 100644
--- a/media2/src/main/java/androidx/media2/MediaBrowser2.java
+++ b/media2/src/main/java/androidx/media2/MediaBrowser2.java
@@ -62,28 +62,28 @@
          * {@link MediaLibrarySession#notifyChildrenChanged} for the parent.
          *
          * @param browser the browser for this event
-         * @param parentId parent id that you've specified with
+         * @param parentId non-empty parent id that you've specified with
          *                 {@link #subscribe(String, LibraryParams)}
          * @param itemCount number of children
          * @param params library params from the library service. Can be differ from params
          *               that you've specified with {@link #subscribe(String, LibraryParams)}.
          */
         public void onChildrenChanged(@NonNull MediaBrowser2 browser, @NonNull String parentId,
-                int itemCount, @Nullable LibraryParams params) { }
+                @IntRange(from = 0) int itemCount, @Nullable LibraryParams params) { }
 
         /**
          * Called when there's change in the search result requested by the previous
          * {@link MediaBrowser2#search(String, LibraryParams)}.
          *
          * @param browser the browser for this event
-         * @param query search query that you've specified with
+         * @param query non-empty search query that you've specified with
          *              {@link #search(String, LibraryParams)}
          * @param itemCount The item count for the search result
          * @param params library params from the library service. Can be differ from params
          *               that you've specified with {@link #search(String, LibraryParams)}.
          */
         public void onSearchResultChanged(@NonNull MediaBrowser2 browser, @NonNull String query,
-                int itemCount, @Nullable LibraryParams params) { }
+                @IntRange(from = 0) int itemCount, @Nullable LibraryParams params) { }
     }
 
     public MediaBrowser2(@NonNull Context context, @NonNull SessionToken2 token,
@@ -122,6 +122,7 @@
      * @param params library params getting root
      * @see BrowserResult#getMediaItem()
      */
+    @NonNull
     public ListenableFuture<BrowserResult> getLibraryRoot(@Nullable final LibraryParams params) {
         if (isConnected()) {
             return getImpl().getLibraryRoot(params);
@@ -135,10 +136,10 @@
      * called with the library params. You should call
      * {@link #getChildren(String, int, int, LibraryParams)} to get the items under the parent.
      *
-     * @param parentId parent id
+     * @param parentId non-empty parent id
      * @param params library params
      */
-    public ListenableFuture<BrowserResult> subscribe(@NonNull String parentId,
+    public @NonNull ListenableFuture<BrowserResult> subscribe(@NonNull String parentId,
             @Nullable LibraryParams params) {
         if (TextUtils.isEmpty(parentId)) {
             throw new IllegalArgumentException("parentId shouldn't be empty");
@@ -156,9 +157,9 @@
      * This unsubscribes all previous subscription with the parent id, regardless of the library
      * param that was previously sent to the library service.
      *
-     * @param parentId parent id
+     * @param parentId non-empty parent id
      */
-    public ListenableFuture<BrowserResult> unsubscribe(@NonNull String parentId) {
+    public @NonNull ListenableFuture<BrowserResult> unsubscribe(@NonNull String parentId) {
         if (TextUtils.isEmpty(parentId)) {
             throw new IllegalArgumentException("parentId shouldn't be empty");
         }
@@ -174,13 +175,13 @@
      * If it's successfully completed, {@link BrowserResult#getMediaItems()} will return the list
      * of children.
      *
-     * @param parentId parent id for getting the children.
+     * @param parentId non-empty parent id for getting the children
      * @param page page number to get the result. Starts from {@code 0}
      * @param pageSize page size. Should be greater or equal to {@code 1}
      * @param params library params
      * @see BrowserResult#getMediaItems()
      */
-    public ListenableFuture<BrowserResult> getChildren(@NonNull String parentId,
+    public @NonNull ListenableFuture<BrowserResult> getChildren(@NonNull String parentId,
             @IntRange(from = 0) int page, @IntRange(from = 1) int pageSize,
             @Nullable LibraryParams params) {
         if (TextUtils.isEmpty(parentId)) {
@@ -204,10 +205,10 @@
      * If it's successfully completed, {@link BrowserResult#getMediaItem()} will return the media
      * item.
      *
-     * @param mediaId media id for specifying the item
+     * @param mediaId non-empty media id for specifying the item
      * @see BrowserResult#getMediaItems()
      */
-    public ListenableFuture<BrowserResult> getItem(@NonNull final String mediaId) {
+    public @NonNull ListenableFuture<BrowserResult> getItem(@NonNull final String mediaId) {
         if (TextUtils.isEmpty(mediaId)) {
             throw new IllegalArgumentException("mediaId shouldn't be empty");
         }
@@ -225,12 +226,12 @@
      * {@link BrowserCallback#getSearchResult(String, int, int, LibraryParams)} the search result
      * and calls {@link #getSearchResult(String, int, int, LibraryParams)}} for getting the result.
      *
-     * @param query search query. Should not be an empty string.
+     * @param query non-empty search query
      * @param params library params
      * @see BrowserCallback#getSearchResult(String, int, int, LibraryParams)
      * @see #getSearchResult(String, int, int, LibraryParams)
      */
-    public ListenableFuture<BrowserResult> search(@NonNull String query,
+    public @NonNull ListenableFuture<BrowserResult> search(@NonNull String query,
             @Nullable LibraryParams params) {
         if (TextUtils.isEmpty(query)) {
             throw new IllegalArgumentException("query shouldn't be empty");
@@ -247,13 +248,14 @@
      * If it's successfully completed, {@link BrowserResult#getMediaItems()} will return the search
      * result.
      *
-     * @param query search query that you've specified with {@link #search(String, LibraryParams)}
+     * @param query non-empty search query that you've specified with
+     *              {@link #search(String, LibraryParams)}.
      * @param page page number to get search result. Starts from {@code 0}
      * @param pageSize page size. Should be greater or equal to {@code 1}
      * @param params library params
      * @see BrowserResult#getMediaItems()
      */
-    public ListenableFuture<BrowserResult> getSearchResult(final @NonNull String query,
+    public @NonNull ListenableFuture<BrowserResult> getSearchResult(final @NonNull String query,
             @IntRange(from = 0) int page, @IntRange(from = 1) int pageSize,
             final @Nullable LibraryParams params) {
         if (TextUtils.isEmpty(query)) {
@@ -438,7 +440,7 @@
          *
          * @return library params.
          */
-        public LibraryParams getLibraryParams() {
+        public @Nullable LibraryParams getLibraryParams() {
             return mParams;
         }
 
diff --git a/media2/src/main/java/androidx/media2/MediaBrowser2ImplBase.java b/media2/src/main/java/androidx/media2/MediaBrowser2ImplBase.java
index e9c13ea..9e7482e 100644
--- a/media2/src/main/java/androidx/media2/MediaBrowser2ImplBase.java
+++ b/media2/src/main/java/androidx/media2/MediaBrowser2ImplBase.java
@@ -35,8 +35,6 @@
 import androidx.media2.MediaBrowser2.BrowserResult;
 import androidx.media2.MediaLibraryService2.LibraryParams;
 import androidx.media2.SequencedFutureManager.SequencedFuture;
-import androidx.versionedparcelable.ParcelImpl;
-import androidx.versionedparcelable.ParcelUtils;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
@@ -67,7 +65,7 @@
                     @Override
                     public void run(IMediaSession2 iSession2, int seq) throws RemoteException {
                         iSession2.getLibraryRoot(mControllerStub, seq,
-                                (ParcelImpl) ParcelUtils.toParcelable(params));
+                                MediaUtils2.toParcelable(params));
                     }
                 });
     }
@@ -80,7 +78,7 @@
                     @Override
                     public void run(IMediaSession2 iSession2, int seq) throws RemoteException {
                         iSession2.subscribe(mControllerStub, seq, parentId,
-                                (ParcelImpl) ParcelUtils.toParcelable(params));
+                                MediaUtils2.toParcelable(params));
                     }
                 });
     }
@@ -104,7 +102,7 @@
                     @Override
                     public void run(IMediaSession2 iSession2, int seq) throws RemoteException {
                         iSession2.getChildren(mControllerStub, seq, parentId, page, pageSize,
-                                (ParcelImpl) ParcelUtils.toParcelable(params));
+                                MediaUtils2.toParcelable(params));
                     }
                 });
     }
@@ -127,7 +125,7 @@
                     @Override
                     public void run(IMediaSession2 iSession2, int seq) throws RemoteException {
                         iSession2.search(mControllerStub, seq, query,
-                                (ParcelImpl) ParcelUtils.toParcelable(params));
+                                MediaUtils2.toParcelable(params));
                     }
                 });
     }
@@ -140,7 +138,7 @@
                     @Override
                     public void run(IMediaSession2 iSession2, int seq) throws RemoteException {
                         iSession2.getSearchResult(mControllerStub, seq, query, page, pageSize,
-                                (ParcelImpl) ParcelUtils.toParcelable(params));
+                                MediaUtils2.toParcelable(params));
                     }
                 });
     }
diff --git a/media2/src/main/java/androidx/media2/MediaBrowser2ImplLegacy.java b/media2/src/main/java/androidx/media2/MediaBrowser2ImplLegacy.java
index 88e84d4..bb07a19 100644
--- a/media2/src/main/java/androidx/media2/MediaBrowser2ImplLegacy.java
+++ b/media2/src/main/java/androidx/media2/MediaBrowser2ImplLegacy.java
@@ -20,7 +20,6 @@
 import static androidx.media2.MediaBrowser2.BrowserResult.RESULT_CODE_DISCONNECTED;
 import static androidx.media2.MediaBrowser2.BrowserResult.RESULT_CODE_SUCCESS;
 import static androidx.media2.MediaBrowser2.BrowserResult.RESULT_CODE_UNKNOWN_ERROR;
-import static androidx.media2.MediaItem2.FLAG_BROWSABLE;
 
 import android.content.Context;
 import android.os.Bundle;
@@ -313,9 +312,11 @@
         // TODO: Query again with getMediaItem() to get real media item.
         MediaMetadata2 metadata = new MediaMetadata2.Builder()
                 .putString(MediaMetadata2.METADATA_KEY_MEDIA_ID, browser.getRoot())
+                .putLong(MediaMetadata2.METADATA_KEY_BROWSABLE, MediaMetadata2.BROWSABLE_TYPE_MIXED)
+                .putLong(MediaMetadata2.METADATA_KEY_PLAYABLE, 0)
                 .setExtras(browser.getExtras())
                 .build();
-        return new MediaItem2.Builder(FLAG_BROWSABLE).setMetadata(metadata).build();
+        return new MediaItem2.Builder().setMetadata(metadata).build();
     }
 
     private class GetLibraryRootCallback extends MediaBrowserCompat.ConnectionCallback {
diff --git a/media2/src/main/java/androidx/media2/MediaController2.java b/media2/src/main/java/androidx/media2/MediaController2.java
index e3f1d69..bbd2b84 100644
--- a/media2/src/main/java/androidx/media2/MediaController2.java
+++ b/media2/src/main/java/androidx/media2/MediaController2.java
@@ -38,6 +38,7 @@
 
 import androidx.annotation.GuardedBy;
 import androidx.annotation.IntDef;
+import androidx.annotation.IntRange;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
@@ -250,7 +251,8 @@
      *
      * @return SessionToken2 of the connected session, or {@code null} if not connected
      */
-    public @Nullable SessionToken2 getConnectedSessionToken() {
+    @Nullable
+    public SessionToken2 getConnectedSessionToken() {
         return isConnected() ? getImpl().getConnectedSessionToken() : null;
     }
 
@@ -265,6 +267,7 @@
     /**
      * Requests that the player start or resume playback.
      */
+    @NonNull
     public ListenableFuture<ControllerResult> play() {
         if (isConnected()) {
             return getImpl().play();
@@ -275,6 +278,7 @@
     /**
      * Requests that the player pause playback.
      */
+    @NonNull
     public ListenableFuture<ControllerResult> pause() {
         if (isConnected()) {
             return getImpl().pause();
@@ -283,23 +287,26 @@
     }
 
     /**
-     * Requests that the player prefetch the media items for playback. In other words, other
-     * sessions can continue to play during the prefetch of this session. This method can be used
-     * to speed up the start of the playback. Once the prefetch is done, the player will change
+     * Requests that the player prepare the media items for playback. In other words, other
+     * sessions can continue to play during the prepare of this session. This method can be used
+     * to speed up the start of the playback. Once the prepare is done, the player will change
      * its playback state to {@link SessionPlayer2#PLAYER_STATE_PAUSED}. Afterwards, {@link #play}
      * can be called to start playback.
      */
-    public ListenableFuture<ControllerResult> prefetch() {
+    @NonNull
+    public ListenableFuture<ControllerResult> prepare() {
         if (isConnected()) {
-            return getImpl().prefetch();
+            return getImpl().prepare();
         }
         return createDisconnectedFuture();
     }
 
     /**
-     * Start fast forwarding. If playback is already fast forwarding this
-     * may increase the rate.
+     * Requests session to increase the playback speed.
+     *
+     * @see MediaSession2.SessionCallback#onFastForward(MediaSession2, ControllerInfo)
      */
+    @NonNull
     public ListenableFuture<ControllerResult> fastForward() {
         if (isConnected()) {
             return getImpl().fastForward();
@@ -308,9 +315,11 @@
     }
 
     /**
-     * Start rewinding. If playback is already rewinding this may increase
-     * the rate.
+     * Requests session to decrease the playback speed.
+     *
+     * @see MediaSession2.SessionCallback#onRewind(MediaSession2, ControllerInfo)
      */
+    @NonNull
     public ListenableFuture<ControllerResult> rewind() {
         if (isConnected()) {
             return getImpl().rewind();
@@ -319,21 +328,11 @@
     }
 
     /**
-     * Move to a new location in the media stream.
+     * Requests session to skip backward within the current media item.
      *
-     * @param pos Position to move to, in milliseconds.
+     * @see MediaSession2.SessionCallback#onSkipForward(MediaSession2, ControllerInfo)
      */
-    public ListenableFuture<ControllerResult> seekTo(long pos) {
-        if (isConnected()) {
-            return getImpl().seekTo(pos);
-        }
-        return createDisconnectedFuture();
-    }
-
-    /**
-     * @hide
-     */
-    @RestrictTo(LIBRARY_GROUP)
+    @NonNull
     public ListenableFuture<ControllerResult> skipForward() {
         // To match with KEYCODE_MEDIA_SKIP_FORWARD
         if (isConnected()) {
@@ -343,9 +342,11 @@
     }
 
     /**
-     * @hide
+     * Requests session to skip forward within the current media item.
+     *
+     * @see MediaSession2.SessionCallback#onSkipBackward(MediaSession2, ControllerInfo)
      */
-    @RestrictTo(LIBRARY_GROUP)
+    @NonNull
     public ListenableFuture<ControllerResult> skipBackward() {
         // To match with KEYCODE_MEDIA_SKIP_BACKWARD
         if (isConnected()) {
@@ -355,9 +356,22 @@
     }
 
     /**
+     * Move to a new location in the media stream.
+     *
+     * @param pos Position to move to, in milliseconds.
+     */
+    @NonNull
+    public ListenableFuture<ControllerResult> seekTo(long pos) {
+        if (isConnected()) {
+            return getImpl().seekTo(pos);
+        }
+        return createDisconnectedFuture();
+    }
+
+    /**
      * Requests that the player start playback for a specific media id.
      *
-     * @param mediaId The id of the requested media.
+     * @param mediaId The non-empty media id
      * @param extras Optional extras that can include extra information about the media item
      *               to be played.
      * @hide
@@ -365,8 +379,8 @@
     @RestrictTo(LIBRARY_GROUP)
     public ListenableFuture<ControllerResult> playFromMediaId(@NonNull String mediaId,
             @Nullable Bundle extras) {
-        if (mediaId == null) {
-            throw new IllegalArgumentException("mediaId shouldn't be null");
+        if (TextUtils.isEmpty(mediaId)) {
+            throw new IllegalArgumentException("mediaId shouldn't be empty");
         }
         if (isConnected()) {
             return getImpl().playFromMediaId(mediaId, extras);
@@ -377,7 +391,7 @@
     /**
      * Requests that the player start playback for a specific search query.
      *
-     * @param query The search query. Should not be an empty string.
+     * @param query The non-empty search query
      * @param extras Optional extras that can include extra information about the query.
      * @hide
      */
@@ -414,63 +428,63 @@
     }
 
     /**
-     * Requests that the player prefetch a media item with the media id for playback.
+     * Requests that the player prepare a media item with the media id for playback.
      * In other words, other sessions can continue to play during the preparation of this session.
      * This method can be used to speed up the start of the playback.
-     * Once the prefetch is done, the session will change its playback state to
+     * Once the prepare is done, the session will change its playback state to
      * {@link SessionPlayer2#PLAYER_STATE_PAUSED}. Afterwards, {@link #play} can be called to start
-     * playback. If the prefetch is not needed, {@link #playFromMediaId} can be directly called
+     * playback. If the prepare is not needed, {@link #playFromMediaId} can be directly called
      * without this method.
      *
-     * @param mediaId The id of the requested media.
+     * @param mediaId The non-empty media id
      * @param extras Optional extras that can include extra information about the media item
      *               to be prepared.
      * @hide
      */
     @RestrictTo(LIBRARY_GROUP)
-    public ListenableFuture<ControllerResult> prefetchFromMediaId(@NonNull String mediaId,
+    public ListenableFuture<ControllerResult> prepareFromMediaId(@NonNull String mediaId,
             @Nullable Bundle extras) {
-        if (mediaId == null) {
-            throw new IllegalArgumentException("mediaId shouldn't be null");
+        if (TextUtils.isEmpty(mediaId)) {
+            throw new IllegalArgumentException("mediaId shouldn't be empty");
         }
         if (isConnected()) {
-            return getImpl().prefetchFromMediaId(mediaId, extras);
+            return getImpl().prepareFromMediaId(mediaId, extras);
         }
         return createDisconnectedFuture();
     }
 
     /**
-     * Requests that the player prefetch a media item with the specific search query for playback.
+     * Requests that the player prepare a media item with the specific search query for playback.
      * In other words, other sessions can continue to play during the preparation of this session.
      * This method can be used to speed up the start of the playback.
-     * Once the prefetch is done, the session will change its playback state to
+     * Once the prepare is done, the session will change its playback state to
      * {@link SessionPlayer2#PLAYER_STATE_PAUSED}. Afterwards, {@link #play} can be called to start
-     * playback. If the prefetch is not needed, {@link #playFromSearch} can be directly called
+     * playback. If the prepare is not needed, {@link #playFromSearch} can be directly called
      * without this method.
      *
-     * @param query The search query. Should not be an empty string.
+     * @param query The non-empty search query
      * @param extras Optional extras that can include extra information about the query.
      * @hide
      */
     @RestrictTo(LIBRARY_GROUP)
-    public ListenableFuture<ControllerResult> prefetchFromSearch(@NonNull String query,
+    public ListenableFuture<ControllerResult> prepareFromSearch(@NonNull String query,
             @Nullable Bundle extras) {
         if (TextUtils.isEmpty(query)) {
             throw new IllegalArgumentException("query shouldn't be empty");
         }
         if (isConnected()) {
-            return getImpl().prefetchFromSearch(query, extras);
+            return getImpl().prepareFromSearch(query, extras);
         }
         return createDisconnectedFuture();
     }
 
     /**
-     * Requests that the player prefetch a media item with the specific {@link Uri} for playback.
+     * Requests that the player prepare a media item with the specific {@link Uri} for playback.
      * In other words, other sessions can continue to play during the preparation of this session.
      * This method can be used to speed up the start of the playback.
-     * Once the prefetch is done, the session will change its playback state to
+     * Once the prepare is done, the session will change its playback state to
      * {@link SessionPlayer2#PLAYER_STATE_PAUSED}. Afterwards, {@link #play} can be called to start
-     * playback. If the prefetch is not needed, {@link #playFromUri} can be directly called
+     * playback. If the prepare is not needed, {@link #playFromUri} can be directly called
      * without this method.
      *
      * @param uri The URI of the requested media.
@@ -479,13 +493,13 @@
      * @hide
      */
     @RestrictTo(LIBRARY_GROUP)
-    public ListenableFuture<ControllerResult> prefetchFromUri(@NonNull Uri uri,
+    public ListenableFuture<ControllerResult> prepareFromUri(@NonNull Uri uri,
             @Nullable Bundle extras) {
         if (uri == null) {
             throw new IllegalArgumentException("uri shouldn't be null");
         }
         if (isConnected()) {
-            return getImpl().prefetchFromUri(uri, extras);
+            return getImpl().prepareFromUri(uri, extras);
         }
         return createDisconnectedFuture();
     }
@@ -505,6 +519,7 @@
      * @param flags flags from {@link AudioManager} to include with the volume request for local
      *              playback
      */
+    @NonNull
     public ListenableFuture<ControllerResult> setVolumeTo(int value, @VolumeFlags int flags) {
         if (isConnected()) {
             return getImpl().setVolumeTo(value, flags);
@@ -532,6 +547,7 @@
      * @param flags flags from {@link AudioManager} to include with the volume request for local
      *              playback
      */
+    @NonNull
     public ListenableFuture<ControllerResult> adjustVolume(@VolumeDirection int direction,
             @VolumeFlags int flags) {
         if (isConnected()) {
@@ -546,7 +562,8 @@
      *
      * @return A {@link PendingIntent} to launch UI or null
      */
-    public @Nullable PendingIntent getSessionActivity() {
+    @Nullable
+    public PendingIntent getSessionActivity() {
         return isConnected() ? getImpl().getSessionActivity() : null;
     }
 
@@ -597,6 +614,7 @@
     /**
      * Set the playback speed.
      */
+    @NonNull
     public ListenableFuture<ControllerResult> setPlaybackSpeed(float speed) {
         if (isConnected()) {
             return getImpl().setPlaybackSpeed(speed);
@@ -634,7 +652,8 @@
      *
      * @return The current playback info or null
      */
-    public @Nullable PlaybackInfo getPlaybackInfo() {
+    @Nullable
+    public PlaybackInfo getPlaybackInfo() {
         return isConnected() ? getImpl().getPlaybackInfo() : null;
     }
 
@@ -647,13 +666,14 @@
      * <p>
      * If the user rating was {@code null}, the media item does not accept setting user rating.
      *
-     * @param mediaId The id of the media
+     * @param mediaId The non-empty media id
      * @param rating The rating to set
      */
+    @NonNull
     public ListenableFuture<ControllerResult> setRating(@NonNull String mediaId,
             @NonNull Rating2 rating) {
-        if (mediaId == null) {
-            throw new IllegalArgumentException("mediaId shouldn't be null");
+        if (TextUtils.isEmpty(mediaId)) {
+            throw new IllegalArgumentException("mediaId shouldn't be empty");
         }
         if (rating == null) {
             throw new IllegalArgumentException("rating shouldn't be null");
@@ -676,6 +696,7 @@
      * @param command custom command
      * @param args optional argument
      */
+    @NonNull
     public ListenableFuture<ControllerResult> sendCustomCommand(@NonNull SessionCommand2 command,
             @Nullable Bundle args) {
         if (command == null) {
@@ -702,27 +723,31 @@
      *         or it doesn't have enough permission
      * @see SessionCommand2#COMMAND_CODE_PLAYER_GET_PLAYLIST
      */
-    public @Nullable List<MediaItem2> getPlaylist() {
+    @Nullable
+    public List<MediaItem2> getPlaylist() {
         return isConnected() ? getImpl().getPlaylist() : null;
     }
 
     /**
-     * Sets the playlist.
-     * <p>
-     * Even when the playlist is successfully set, use the playlist returned from
-     * {@link #getPlaylist()} for playlist APIs such as {@link #skipToPlaylistItem(MediaItem2)}.
-     * Otherwise the session in the remote process can't distinguish between media items.
+     * Sets the playlist with the list of media IDs. All media IDs in the list shouldn't be empty.
      *
-     * @param list playlist
+     * @param list list of media id. Shouldn't contain an empty id.
      * @param metadata metadata of the playlist
      * @see #getPlaylist()
      * @see ControllerCallback#onPlaylistChanged
+     * @see MediaMetadata2#METADATA_KEY_MEDIA_ID
      */
-    public ListenableFuture<ControllerResult> setPlaylist(@NonNull List<MediaItem2> list,
+    @NonNull
+    public ListenableFuture<ControllerResult> setPlaylist(@NonNull List<String> list,
             @Nullable MediaMetadata2 metadata) {
         if (list == null) {
             throw new IllegalArgumentException("list shouldn't be null");
         }
+        for (int i = 0; i < list.size(); i++) {
+            if (TextUtils.isEmpty(list.get(i))) {
+                throw new IllegalArgumentException("list shouldn't contain empty id, index=" + i);
+            }
+        }
         if (isConnected()) {
             return getImpl().setPlaylist(list, metadata);
         }
@@ -732,15 +757,18 @@
     /**
      * Sets a {@link MediaItem2} for playback.
      *
-     * @param item the descriptor of media item you want to play
+     * @param mediaId The non-empty media id of the item to play
+     * @see MediaMetadata2#METADATA_KEY_MEDIA_ID
      */
-    public void setMediaItem(@NonNull MediaItem2 item) {
-        if (item == null) {
-            throw new IllegalArgumentException("item shouldn't be null");
+    @NonNull
+    public ListenableFuture<ControllerResult> setMediaItem(@NonNull String mediaId) {
+        if (TextUtils.isEmpty(mediaId)) {
+            throw new IllegalArgumentException("mediaId shouldn't be empty");
         }
         if (isConnected()) {
-            getImpl().setMediaItem(item);
+            getImpl().setMediaItem(mediaId);
         }
+        return createDisconnectedFuture();
     }
 
     /**
@@ -748,6 +776,7 @@
      *
      * @param metadata metadata of the playlist
      */
+    @NonNull
     public ListenableFuture<ControllerResult> updatePlaylistMetadata(
             @Nullable MediaMetadata2 metadata) {
         if (isConnected()) {
@@ -764,70 +793,76 @@
      * @return metadata metadata of the playlist, or null if none is set or the controller is not
      *         connected
      */
-    public @Nullable MediaMetadata2 getPlaylistMetadata() {
+    @Nullable
+    public MediaMetadata2 getPlaylistMetadata() {
         return isConnected() ? getImpl().getPlaylistMetadata() : null;
     }
 
     /**
-     * Adds the media item to the playlist at position index. Index equals or greater than
-     * the current playlist size (e.g. {@link Integer#MAX_VALUE}) will add the item at the end of
-     * the playlist.
+     * Adds the media item to the playlist at the index with the media ID. Index equals or greater
+     * than the current playlist size (e.g. {@link Integer#MAX_VALUE}) will add the item at the end
+     * of the playlist.
      * <p>
      * This will not change the currently playing media item.
      * If index is less than or equal to the current index of the playlist,
      * the current index of the playlist will be incremented correspondingly.
      *
      * @param index the index you want to add
-     * @param item the media item you want to add
+     * @param mediaId The non-empty media id of the new item
+     * @see MediaMetadata2#METADATA_KEY_MEDIA_ID
      */
-    public ListenableFuture<ControllerResult> addPlaylistItem(int index, @NonNull MediaItem2 item) {
+    @NonNull
+    public ListenableFuture<ControllerResult> addPlaylistItem(@IntRange(from = 0) int index,
+            @NonNull String mediaId) {
         if (index < 0) {
             throw new IllegalArgumentException("index shouldn't be negative");
         }
-        if (item == null) {
-            throw new IllegalArgumentException("item shouldn't be null");
+        if (TextUtils.isEmpty(mediaId)) {
+            throw new IllegalArgumentException("mediaId shouldn't be empty");
         }
         if (isConnected()) {
-            return getImpl().addPlaylistItem(index, item);
+            return getImpl().addPlaylistItem(index, mediaId);
         }
         return createDisconnectedFuture();
     }
 
     /**
      * Removes the media item at index in the playlist.
-     *<p>
+     * <p>
      * If the item is the currently playing item of the playlist, current playback
      * will be stopped and playback moves to next source in the list.
      *
-     * @param item the media item you want to add
+     * @param index the media item you want to add
      */
-    public ListenableFuture<ControllerResult> removePlaylistItem(@NonNull MediaItem2 item) {
-        if (item == null) {
-            throw new IllegalArgumentException("item shouldn't be null");
+    @NonNull
+    public ListenableFuture<ControllerResult> removePlaylistItem(@IntRange(from = 0) int index) {
+        if (index < 0) {
+            throw new IllegalArgumentException("index shouldn't be negative");
         }
         if (isConnected()) {
-            return getImpl().removePlaylistItem(item);
+            return getImpl().removePlaylistItem(index);
         }
         return createDisconnectedFuture();
     }
 
     /**
-     * Replace the media item at index in the playlist. This can be also used to update metadata of
-     * an item.
+     * Replaces the media item at index in the playlist with the media ID.
      *
      * @param index the index of the item to replace
-     * @param item the new item
+     * @param mediaId The non-empty media id of the new item
+     * @see MediaMetadata2#METADATA_KEY_MEDIA_ID
      */
-    public ListenableFuture<ControllerResult> replacePlaylistItem(int index,
-            @NonNull MediaItem2 item) {
+    @NonNull
+    public ListenableFuture<ControllerResult> replacePlaylistItem(@IntRange(from = 0) int index,
+            @NonNull String mediaId) {
         if (index < 0) {
             throw new IllegalArgumentException("index shouldn't be negative");
         }
-        if (item == null) {
-            throw new IllegalArgumentException("item shouldn't be null");
+        if (TextUtils.isEmpty(mediaId)) {
+            throw new IllegalArgumentException("mediaId shouldn't be empty");
         }
         if (isConnected()) {
-            return getImpl().replacePlaylistItem(index, item);
+            return getImpl().replacePlaylistItem(index, mediaId);
         }
         return createDisconnectedFuture();
     }
@@ -838,6 +873,7 @@
      *
      * @return the currently playing item, or null if unknown or not connected
      */
+    @Nullable
     public MediaItem2 getCurrentMediaItem() {
         return isConnected() ? getImpl().getCurrentMediaItem() : null;
     }
@@ -847,6 +883,7 @@
      * <p>
      * This calls {@link SessionPlayer2#skipToPreviousPlaylistItem()}.
      */
+    @NonNull
     public ListenableFuture<ControllerResult> skipToPreviousPlaylistItem() {
         if (isConnected()) {
             return getImpl().skipToPreviousItem();
@@ -859,6 +896,7 @@
      * <p>
      * This calls {@link SessionPlayer2#skipToNextPlaylistItem()}.
      */
+    @NonNull
     public ListenableFuture<ControllerResult> skipToNextPlaylistItem() {
         if (isConnected()) {
             return getImpl().skipToNextItem();
@@ -867,18 +905,19 @@
     }
 
     /**
-     * Skips to the item in the playlist.
+     * Skips to the item in the playlist at the index.
      * <p>
      * This calls {@link SessionPlayer2#skipToPlaylistItem(MediaItem2)}.
      *
-     * @param item The item in the playlist you want to play
+     * @param index The item in the playlist you want to play
      */
-    public ListenableFuture<ControllerResult> skipToPlaylistItem(@NonNull MediaItem2 item) {
-        if (item == null) {
-            throw new IllegalArgumentException("item shouldn't be null");
+    @NonNull
+    public ListenableFuture<ControllerResult> skipToPlaylistItem(@IntRange(from = 0) int index) {
+        if (index < 0) {
+            throw new IllegalArgumentException("index shouldn't be negative");
         }
         if (isConnected()) {
-            return getImpl().skipToPlaylistItem(item);
+            return getImpl().skipToPlaylistItem(index);
         }
         return createDisconnectedFuture();
     }
@@ -906,6 +945,7 @@
      * @see SessionPlayer2#REPEAT_MODE_ALL
      * @see SessionPlayer2#REPEAT_MODE_GROUP
      */
+    @NonNull
     public ListenableFuture<ControllerResult> setRepeatMode(@RepeatMode int repeatMode) {
         if (isConnected()) {
             return getImpl().setRepeatMode(repeatMode);
@@ -934,6 +974,7 @@
      * @see SessionPlayer2#SHUFFLE_MODE_ALL
      * @see SessionPlayer2#SHUFFLE_MODE_GROUP
      */
+    @NonNull
     public ListenableFuture<ControllerResult> setShuffleMode(@ShuffleMode int shuffleMode) {
         if (isConnected()) {
             return getImpl().setShuffleMode(shuffleMode);
@@ -1014,7 +1055,7 @@
         boolean isConnected();
         ListenableFuture<ControllerResult> play();
         ListenableFuture<ControllerResult> pause();
-        ListenableFuture<ControllerResult> prefetch();
+        ListenableFuture<ControllerResult> prepare();
         ListenableFuture<ControllerResult> fastForward();
         ListenableFuture<ControllerResult> rewind();
         ListenableFuture<ControllerResult> seekTo(long pos);
@@ -1025,11 +1066,11 @@
         ListenableFuture<ControllerResult> playFromSearch(@NonNull String query,
                 @Nullable Bundle extras);
         ListenableFuture<ControllerResult> playFromUri(@NonNull Uri uri, @Nullable Bundle extras);
-        ListenableFuture<ControllerResult> prefetchFromMediaId(@NonNull String mediaId,
+        ListenableFuture<ControllerResult> prepareFromMediaId(@NonNull String mediaId,
                 @Nullable Bundle extras);
-        ListenableFuture<ControllerResult> prefetchFromSearch(@NonNull String query,
+        ListenableFuture<ControllerResult> prepareFromSearch(@NonNull String query,
                 @Nullable Bundle extras);
-        ListenableFuture<ControllerResult> prefetchFromUri(@NonNull Uri uri,
+        ListenableFuture<ControllerResult> prepareFromUri(@NonNull Uri uri,
                 @Nullable Bundle extras);
         ListenableFuture<ControllerResult> setVolumeTo(int value, @VolumeFlags int flags);
         ListenableFuture<ControllerResult> adjustVolume(@VolumeDirection int direction,
@@ -1048,20 +1089,20 @@
         ListenableFuture<ControllerResult> sendCustomCommand(@NonNull SessionCommand2 command,
                 @Nullable Bundle args);
         @Nullable List<MediaItem2> getPlaylist();
-        ListenableFuture<ControllerResult> setPlaylist(@NonNull List<MediaItem2> list,
+        ListenableFuture<ControllerResult> setPlaylist(@NonNull List<String> list,
                 @Nullable MediaMetadata2 metadata);
-        ListenableFuture<ControllerResult>  setMediaItem(@NonNull MediaItem2 item);
+        ListenableFuture<ControllerResult> setMediaItem(@NonNull String mediaId);
         ListenableFuture<ControllerResult> updatePlaylistMetadata(
                 @Nullable MediaMetadata2 metadata);
         @Nullable MediaMetadata2 getPlaylistMetadata();
-        ListenableFuture<ControllerResult> addPlaylistItem(int index, @NonNull MediaItem2 item);
-        ListenableFuture<ControllerResult> removePlaylistItem(@NonNull MediaItem2 item);
+        ListenableFuture<ControllerResult> addPlaylistItem(int index, @NonNull String mediaId);
+        ListenableFuture<ControllerResult> removePlaylistItem(@NonNull int index);
         ListenableFuture<ControllerResult> replacePlaylistItem(int index,
-                @NonNull MediaItem2 item);
+                @NonNull String mediaId);
         MediaItem2 getCurrentMediaItem();
         ListenableFuture<ControllerResult> skipToPreviousItem();
         ListenableFuture<ControllerResult> skipToNextItem();
-        ListenableFuture<ControllerResult> skipToPlaylistItem(@NonNull MediaItem2 item);
+        ListenableFuture<ControllerResult> skipToPlaylistItem(@NonNull int index);
         @RepeatMode int getRepeatMode();
         ListenableFuture<ControllerResult> setRepeatMode(@RepeatMode int repeatMode);
         @ShuffleMode int getShuffleMode();
@@ -1166,7 +1207,8 @@
          * @param args
          * @return result of handling custom command
          */
-        public @NonNull ControllerResult onCustomCommand(@NonNull MediaController2 controller,
+        @NonNull
+        public ControllerResult onCustomCommand(@NonNull MediaController2 controller,
                 @NonNull SessionCommand2 command, @Nullable Bundle args) {
             return new ControllerResult(ControllerResult.RESULT_CODE_NOT_SUPPORTED);
         }
@@ -1230,7 +1272,7 @@
          * @param metadata new metadata
          */
         public void onPlaylistChanged(@NonNull MediaController2 controller,
-                @NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) { }
+                @Nullable List<MediaItem2> list, @Nullable MediaMetadata2 metadata) { }
 
         /**
          * Called when a playlist metadata is changed.
@@ -1295,12 +1337,6 @@
     // The same as MediaController.PlaybackInfo
     @VersionedParcelize
     public static final class PlaybackInfo implements VersionedParcelable {
-        private static final String KEY_PLAYBACK_TYPE = "android.media.audio_info.playback_type";
-        private static final String KEY_CONTROL_TYPE = "android.media.audio_info.control_type";
-        private static final String KEY_MAX_VOLUME = "android.media.audio_info.max_volume";
-        private static final String KEY_CURRENT_VOLUME = "android.media.audio_info.current_volume";
-        private static final String KEY_AUDIO_ATTRIBUTES = "android.media.audio_info.audio_attrs";
-
         @ParcelField(1)
         int mPlaybackType;
         @ParcelField(2)
@@ -1357,6 +1393,7 @@
          *
          * @return The attributes for this session
          */
+        @Nullable
         public AudioAttributesCompat getAudioAttributes() {
             return mAudioAttrsCompat;
         }
@@ -1416,36 +1453,10 @@
                     && ObjectsCompat.equals(mAudioAttrsCompat, other.mAudioAttrsCompat);
         }
 
-        Bundle toBundle() {
-            Bundle bundle = new Bundle();
-            bundle.putInt(KEY_PLAYBACK_TYPE, mPlaybackType);
-            bundle.putInt(KEY_CONTROL_TYPE, mControlType);
-            bundle.putInt(KEY_MAX_VOLUME, mMaxVolume);
-            bundle.putInt(KEY_CURRENT_VOLUME, mCurrentVolume);
-            if (mAudioAttrsCompat != null) {
-                bundle.putBundle(KEY_AUDIO_ATTRIBUTES, mAudioAttrsCompat.toBundle());
-            }
-            return bundle;
-        }
-
         static PlaybackInfo createPlaybackInfo(int playbackType, AudioAttributesCompat attrs,
                 int controlType, int max, int current) {
             return new PlaybackInfo(playbackType, attrs, controlType, max, current);
         }
-
-        static PlaybackInfo fromBundle(Bundle bundle) {
-            if (bundle == null) {
-                return null;
-            }
-            final int volumeType = bundle.getInt(KEY_PLAYBACK_TYPE);
-            final int volumeControl = bundle.getInt(KEY_CONTROL_TYPE);
-            final int maxVolume = bundle.getInt(KEY_MAX_VOLUME);
-            final int currentVolume = bundle.getInt(KEY_CURRENT_VOLUME);
-            final AudioAttributesCompat attrs = AudioAttributesCompat.fromBundle(
-                    bundle.getBundle(KEY_AUDIO_ATTRIBUTES));
-            return createPlaybackInfo(volumeType, attrs, volumeControl, maxVolume,
-                    currentVolume);
-        }
     }
 
     /**
@@ -1589,7 +1600,8 @@
          * @see #sendCustomCommand(SessionCommand2, Bundle)
          * @return result of send custom command
          */
-        public @Nullable Bundle getCustomCommandResult() {
+        @Nullable
+        public Bundle getCustomCommandResult() {
             return mCustomCommandResult;
         }
 
@@ -1608,7 +1620,8 @@
          *         current media item was {@code null}, or any other reason.
          */
         @Override
-        public @Nullable MediaItem2 getMediaItem() {
+        @Nullable
+        public MediaItem2 getMediaItem() {
             return mItem;
         }
     }
diff --git a/media2/src/main/java/androidx/media2/MediaController2ImplBase.java b/media2/src/main/java/androidx/media2/MediaController2ImplBase.java
index 8c39126..73d4723 100644
--- a/media2/src/main/java/androidx/media2/MediaController2ImplBase.java
+++ b/media2/src/main/java/androidx/media2/MediaController2ImplBase.java
@@ -25,7 +25,7 @@
 import static androidx.media2.SessionCommand2.COMMAND_CODE_PLAYER_ADD_PLAYLIST_ITEM;
 import static androidx.media2.SessionCommand2.COMMAND_CODE_PLAYER_PAUSE;
 import static androidx.media2.SessionCommand2.COMMAND_CODE_PLAYER_PLAY;
-import static androidx.media2.SessionCommand2.COMMAND_CODE_PLAYER_PREFETCH;
+import static androidx.media2.SessionCommand2.COMMAND_CODE_PLAYER_PREPARE;
 import static androidx.media2.SessionCommand2.COMMAND_CODE_PLAYER_REMOVE_PLAYLIST_ITEM;
 import static androidx.media2.SessionCommand2.COMMAND_CODE_PLAYER_REPLACE_PLAYLIST_ITEM;
 import static androidx.media2.SessionCommand2.COMMAND_CODE_PLAYER_SEEK_TO;
@@ -42,12 +42,14 @@
 import static androidx.media2.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID;
 import static androidx.media2.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_SEARCH;
 import static androidx.media2.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_URI;
-import static androidx.media2.SessionCommand2.COMMAND_CODE_SESSION_PREFETCH_FROM_MEDIA_ID;
-import static androidx.media2.SessionCommand2.COMMAND_CODE_SESSION_PREFETCH_FROM_SEARCH;
-import static androidx.media2.SessionCommand2.COMMAND_CODE_SESSION_PREFETCH_FROM_URI;
+import static androidx.media2.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID;
+import static androidx.media2.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH;
+import static androidx.media2.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_URI;
 import static androidx.media2.SessionCommand2.COMMAND_CODE_SESSION_REWIND;
 import static androidx.media2.SessionCommand2.COMMAND_CODE_SESSION_SELECT_ROUTE;
 import static androidx.media2.SessionCommand2.COMMAND_CODE_SESSION_SET_RATING;
+import static androidx.media2.SessionCommand2.COMMAND_CODE_SESSION_SKIP_BACKWARD;
+import static androidx.media2.SessionCommand2.COMMAND_CODE_SESSION_SKIP_FORWARD;
 import static androidx.media2.SessionCommand2.COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO;
 import static androidx.media2.SessionCommand2.COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO;
 import static androidx.media2.SessionCommand2.COMMAND_CODE_VOLUME_ADJUST_VOLUME;
@@ -82,8 +84,6 @@
 import androidx.media2.SessionCommand2.CommandCode;
 import androidx.media2.SessionPlayer2.RepeatMode;
 import androidx.media2.SessionPlayer2.ShuffleMode;
-import androidx.versionedparcelable.ParcelImpl;
-import androidx.versionedparcelable.ParcelUtils;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
@@ -96,7 +96,7 @@
             new ControllerResult(RESULT_CODE_SKIPPED);
 
     static final String TAG = "MC2ImplBase";
-    static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+    static final boolean DEBUG = true; //Log.isLoggable(TAG, Log.DEBUG);
 
     @SuppressWarnings("WeakerAccess") /* synthetic access */
     final MediaController2 mInstance;
@@ -302,11 +302,11 @@
     }
 
     @Override
-    public ListenableFuture<ControllerResult> prefetch() {
-        return dispatchRemoteSessionTask(COMMAND_CODE_PLAYER_PREFETCH, new RemoteSessionTask() {
+    public ListenableFuture<ControllerResult> prepare() {
+        return dispatchRemoteSessionTask(COMMAND_CODE_PLAYER_PREPARE, new RemoteSessionTask() {
             @Override
             public void run(IMediaSession2 iSession2, int seq) throws RemoteException {
-                iSession2.prefetch(mControllerStub, seq);
+                iSession2.prepare(mControllerStub, seq);
             }
         });
     }
@@ -333,6 +333,28 @@
     }
 
     @Override
+    public ListenableFuture<ControllerResult> skipForward() {
+        return dispatchRemoteSessionTask(COMMAND_CODE_SESSION_SKIP_FORWARD,
+                new RemoteSessionTask() {
+                    @Override
+                    public void run(IMediaSession2 iSession2, int seq) throws RemoteException {
+                        iSession2.skipForward(mControllerStub, seq);
+                    }
+                });
+    }
+
+    @Override
+    public ListenableFuture<ControllerResult> skipBackward() {
+        return dispatchRemoteSessionTask(COMMAND_CODE_SESSION_SKIP_BACKWARD,
+                new RemoteSessionTask() {
+                    @Override
+                    public void run(IMediaSession2 iSession2, int seq) throws RemoteException {
+                        iSession2.skipBackward(mControllerStub, seq);
+                    }
+                });
+    }
+
+    @Override
     public ListenableFuture<ControllerResult> seekTo(final long pos) {
         if (pos < 0) {
             throw new IllegalArgumentException("position shouldn't be negative");
@@ -346,18 +368,6 @@
     }
 
     @Override
-    public ListenableFuture<ControllerResult> skipForward() {
-        // To match with KEYCODE_MEDIA_SKIP_FORWARD
-        return null;
-    }
-
-    @Override
-    public ListenableFuture<ControllerResult> skipBackward() {
-        // To match with KEYCODE_MEDIA_SKIP_BACKWARD
-        return null;
-    }
-
-    @Override
     public ListenableFuture<ControllerResult> playFromMediaId(final @NonNull String mediaId,
             final @Nullable Bundle extras) {
         return dispatchRemoteSessionTask(COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID,
@@ -394,37 +404,37 @@
     }
 
     @Override
-    public ListenableFuture<ControllerResult> prefetchFromMediaId(final @NonNull String mediaId,
+    public ListenableFuture<ControllerResult> prepareFromMediaId(final @NonNull String mediaId,
             final @Nullable Bundle extras) {
-        return dispatchRemoteSessionTask(COMMAND_CODE_SESSION_PREFETCH_FROM_MEDIA_ID,
+        return dispatchRemoteSessionTask(COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID,
                 new RemoteSessionTask() {
                     @Override
                     public void run(IMediaSession2 iSession2, int seq) throws RemoteException {
-                        iSession2.prefetchFromMediaId(mControllerStub, seq, mediaId, extras);
+                        iSession2.prepareFromMediaId(mControllerStub, seq, mediaId, extras);
                     }
                 });
     }
 
     @Override
-    public ListenableFuture<ControllerResult> prefetchFromSearch(final @NonNull String query,
+    public ListenableFuture<ControllerResult> prepareFromSearch(final @NonNull String query,
             final @Nullable Bundle extras) {
-        return dispatchRemoteSessionTask(COMMAND_CODE_SESSION_PREFETCH_FROM_SEARCH,
+        return dispatchRemoteSessionTask(COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH,
                 new RemoteSessionTask() {
                     @Override
                     public void run(IMediaSession2 iSession2, int seq) throws RemoteException {
-                        iSession2.prefetchFromSearch(mControllerStub, seq, query, extras);
+                        iSession2.prepareFromSearch(mControllerStub, seq, query, extras);
                     }
                 });
     }
 
     @Override
-    public ListenableFuture<ControllerResult> prefetchFromUri(final @NonNull Uri uri,
+    public ListenableFuture<ControllerResult> prepareFromUri(final @NonNull Uri uri,
             final @Nullable Bundle extras) {
-        return dispatchRemoteSessionTask(COMMAND_CODE_SESSION_PREFETCH_FROM_URI,
+        return dispatchRemoteSessionTask(COMMAND_CODE_SESSION_PREPARE_FROM_URI,
                 new RemoteSessionTask() {
                     @Override
                     public void run(IMediaSession2 iSession2, int seq) throws RemoteException {
-                        iSession2.prefetchFromUri(mControllerStub, seq, uri, extras);
+                        iSession2.prepareFromUri(mControllerStub, seq, uri, extras);
                     }
                 });
     }
@@ -552,7 +562,7 @@
             @Override
             public void run(IMediaSession2 iSession2, int seq) throws RemoteException {
                 iSession2.setRating(mControllerStub, seq, mediaId,
-                        (ParcelImpl) ParcelUtils.toParcelable(rating));
+                        MediaUtils2.toParcelable(rating));
             }
         });
     }
@@ -563,8 +573,8 @@
         return dispatchRemoteSessionTask(command, new RemoteSessionTask() {
             @Override
             public void run(IMediaSession2 iSession2, int seq) throws RemoteException {
-                iSession2.onCustomCommand(mControllerStub, seq,
-                        (ParcelImpl) ParcelUtils.toParcelable(command), args);
+                iSession2.onCustomCommand(mControllerStub, seq, MediaUtils2.toParcelable(command),
+                        args);
             }
         });
     }
@@ -577,26 +587,24 @@
     }
 
     @Override
-    public ListenableFuture<ControllerResult> setPlaylist(final @NonNull List<MediaItem2> list,
+    public ListenableFuture<ControllerResult> setPlaylist(final @NonNull List<String> list,
             final @Nullable MediaMetadata2 metadata) {
         return dispatchRemoteSessionTask(COMMAND_CODE_PLAYER_SET_PLAYLIST, new RemoteSessionTask() {
             @Override
             public void run(IMediaSession2 iSession2, int seq) throws RemoteException {
-                iSession2.setPlaylist(mControllerStub, seq,
-                        MediaUtils2.convertMediaItem2ListToParcelImplListSlice(list),
-                        (metadata == null) ? null : metadata.toBundle());
+                iSession2.setPlaylist(mControllerStub, seq, list,
+                        MediaUtils2.toParcelable(metadata));
             }
         });
     }
 
     @Override
-    public ListenableFuture<ControllerResult> setMediaItem(final MediaItem2 item) {
+    public ListenableFuture<ControllerResult> setMediaItem(final String mediaId) {
         return dispatchRemoteSessionTask(COMMAND_CODE_PLAYER_SET_MEDIA_ITEM,
                 new RemoteSessionTask() {
                     @Override
                     public void run(IMediaSession2 iSession2, int seq) throws RemoteException {
-                        iSession2.setMediaItem(mControllerStub, seq,
-                                (ParcelImpl) ParcelUtils.toParcelable(item));
+                        iSession2.setMediaItem(mControllerStub, seq, mediaId);
                     }
                 });
     }
@@ -609,7 +617,7 @@
                     @Override
                     public void run(IMediaSession2 iSession2, int seq) throws RemoteException {
                         iSession2.updatePlaylistMetadata(mControllerStub, seq,
-                                (metadata == null) ? null : metadata.toBundle());
+                                MediaUtils2.toParcelable(metadata));
                     }
                 });
     }
@@ -623,38 +631,35 @@
 
     @Override
     public ListenableFuture<ControllerResult> addPlaylistItem(final int index,
-            final @NonNull MediaItem2 item) {
+            final @NonNull String mediaId) {
         return dispatchRemoteSessionTask(COMMAND_CODE_PLAYER_ADD_PLAYLIST_ITEM,
                 new RemoteSessionTask() {
                     @Override
                     public void run(IMediaSession2 iSession2, int seq) throws RemoteException {
-                        iSession2.addPlaylistItem(mControllerStub, seq, index,
-                                (ParcelImpl) ParcelUtils.toParcelable(item));
+                        iSession2.addPlaylistItem(mControllerStub, seq, index, mediaId);
                     }
                 });
     }
 
     @Override
-    public ListenableFuture<ControllerResult> removePlaylistItem(final @NonNull MediaItem2 item) {
+    public ListenableFuture<ControllerResult> removePlaylistItem(final @NonNull int index) {
         return dispatchRemoteSessionTask(COMMAND_CODE_PLAYER_REMOVE_PLAYLIST_ITEM,
                 new RemoteSessionTask() {
                     @Override
                     public void run(IMediaSession2 iSession2, int seq) throws RemoteException {
-                        iSession2.removePlaylistItem(mControllerStub, seq,
-                                (ParcelImpl) ParcelUtils.toParcelable(item));
+                        iSession2.removePlaylistItem(mControllerStub, seq, index);
                     }
                 });
     }
 
     @Override
     public ListenableFuture<ControllerResult> replacePlaylistItem(final int index,
-            final @NonNull MediaItem2 item) {
+            final @NonNull String mediaId) {
         return dispatchRemoteSessionTask(COMMAND_CODE_PLAYER_REPLACE_PLAYLIST_ITEM,
                 new RemoteSessionTask() {
                     @Override
                     public void run(IMediaSession2 iSession2, int seq) throws RemoteException {
-                        iSession2.replacePlaylistItem(mControllerStub, seq, index,
-                                (ParcelImpl) ParcelUtils.toParcelable(item));
+                        iSession2.replacePlaylistItem(mControllerStub, seq, index, mediaId);
                     }
                 });
     }
@@ -689,13 +694,12 @@
     }
 
     @Override
-    public ListenableFuture<ControllerResult> skipToPlaylistItem(final @NonNull MediaItem2 item) {
+    public ListenableFuture<ControllerResult> skipToPlaylistItem(final @NonNull int index) {
         return dispatchRemoteSessionTask(COMMAND_CODE_PLAYER_SKIP_TO_PLAYLIST_ITEM,
                 new RemoteSessionTask() {
                     @Override
                     public void run(IMediaSession2 iSession2, int seq) throws RemoteException {
-                        iSession2.skipToPlaylistItem(mControllerStub, seq,
-                                (ParcelImpl) ParcelUtils.toParcelable(item));
+                        iSession2.skipToPlaylistItem(mControllerStub, seq, index);
                     }
                 });
     }
@@ -1132,7 +1136,7 @@
         }
         try {
             iSession2.onControllerResult(mControllerStub, seq,
-                    (ParcelImpl) ParcelUtils.toParcelable(result));
+                    MediaUtils2.toParcelable(result));
         } catch (RemoteException e) {
             Log.w(TAG, "Error in sending");
         }
diff --git a/media2/src/main/java/androidx/media2/MediaController2ImplLegacy.java b/media2/src/main/java/androidx/media2/MediaController2ImplLegacy.java
index 4600b85..b81cf27 100644
--- a/media2/src/main/java/androidx/media2/MediaController2ImplLegacy.java
+++ b/media2/src/main/java/androidx/media2/MediaController2ImplLegacy.java
@@ -20,6 +20,7 @@
 import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_MEDIA_ID;
 import static android.support.v4.media.session.MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS;
 
+import static androidx.media2.BaseResult2.RESULT_CODE_BAD_VALUE;
 import static androidx.media2.MediaConstants2.ARGUMENT_COMMAND_CODE;
 import static androidx.media2.MediaConstants2.ARGUMENT_ICONTROLLER_CALLBACK;
 import static androidx.media2.MediaConstants2.ARGUMENT_PACKAGE_NAME;
@@ -55,6 +56,7 @@
 import android.support.v4.media.session.MediaSessionCompat;
 import android.support.v4.media.session.MediaSessionCompat.QueueItem;
 import android.support.v4.media.session.PlaybackStateCompat;
+import android.text.TextUtils;
 import android.util.Log;
 
 import androidx.annotation.GuardedBy;
@@ -75,9 +77,7 @@
 
 import com.google.common.util.concurrent.ListenableFuture;
 
-import java.util.ArrayList;
 import java.util.List;
-import java.util.UUID;
 import java.util.concurrent.Executor;
 
 // TODO: Find better way to return listenable future.
@@ -150,7 +150,7 @@
     int mCurrentMediaItemIndex;
     @GuardedBy("mLock")
     @SuppressWarnings("WeakerAccess") /* synthetic access */
-    MediaItem2 mSkipToPlaylistItem;
+    int mSkipToPlaylistIndex = -1;
     @GuardedBy("mLock")
     @SuppressWarnings("WeakerAccess") /* synthetic access */
     long mBufferedPosition;
@@ -287,7 +287,7 @@
     }
 
     @Override
-    public ListenableFuture<ControllerResult> prefetch() {
+    public ListenableFuture<ControllerResult> prepare() {
         synchronized (mLock) {
             if (!mConnected) {
                 Log.w(TAG, "Session isn't active", new IllegalStateException());
@@ -323,6 +323,18 @@
     }
 
     @Override
+    public ListenableFuture<ControllerResult> skipForward() {
+        // Unsupported action
+        return createFutureWithResult(RESULT_CODE_NOT_SUPPORTED);
+    }
+
+    @Override
+    public ListenableFuture<ControllerResult> skipBackward() {
+        // Unsupported action
+        return createFutureWithResult(RESULT_CODE_NOT_SUPPORTED);
+    }
+
+    @Override
     public ListenableFuture<ControllerResult> seekTo(long pos) {
         synchronized (mLock) {
             if (!mConnected) {
@@ -335,18 +347,6 @@
     }
 
     @Override
-    public ListenableFuture<ControllerResult> skipForward() {
-        // To match with KEYCODE_MEDIA_SKIP_FORWARD
-        return null;
-    }
-
-    @Override
-    public ListenableFuture<ControllerResult> skipBackward() {
-        // To match with KEYCODE_MEDIA_SKIP_BACKWARD
-        return null;
-    }
-
-    @Override
     public ListenableFuture<ControllerResult> playFromMediaId(@NonNull String mediaId,
             @Nullable Bundle extras) {
         synchronized (mLock) {
@@ -386,7 +386,7 @@
     }
 
     @Override
-    public ListenableFuture<ControllerResult> prefetchFromMediaId(@NonNull String mediaId,
+    public ListenableFuture<ControllerResult> prepareFromMediaId(@NonNull String mediaId,
             @Nullable Bundle extras) {
         synchronized (mLock) {
             if (!mConnected) {
@@ -399,7 +399,7 @@
     }
 
     @Override
-    public ListenableFuture<ControllerResult> prefetchFromSearch(@NonNull String query,
+    public ListenableFuture<ControllerResult> prepareFromSearch(@NonNull String query,
             @Nullable Bundle extras) {
         synchronized (mLock) {
             if (!mConnected) {
@@ -412,7 +412,7 @@
     }
 
     @Override
-    public ListenableFuture<ControllerResult> prefetchFromUri(@NonNull Uri uri,
+    public ListenableFuture<ControllerResult> prepareFromUri(@NonNull Uri uri,
             @Nullable Bundle extras) {
         synchronized (mLock) {
             if (!mConnected) {
@@ -595,18 +595,18 @@
                 Log.w(TAG, "Session isn't active", new IllegalStateException());
                 return null;
             }
-            return mPlaylist;
+            return (mPlaylist == null || mPlaylist.size() == 0) ? null : mPlaylist;
         }
     }
 
     @Override
-    public ListenableFuture<ControllerResult> setPlaylist(@NonNull List<MediaItem2> list,
+    public ListenableFuture<ControllerResult> setPlaylist(@NonNull List<String> list,
             @Nullable MediaMetadata2 metadata) {
         return createFutureWithResult(RESULT_CODE_NOT_SUPPORTED);
     }
 
     @Override
-    public ListenableFuture<ControllerResult> setMediaItem(MediaItem2 item) {
+    public ListenableFuture<ControllerResult> setMediaItem(String mediaId) {
         return createFutureWithResult(RESULT_CODE_NOT_SUPPORTED);
     }
 
@@ -628,45 +628,46 @@
     }
 
     @Override
-    public ListenableFuture<ControllerResult> addPlaylistItem(int index, @NonNull MediaItem2 item) {
+    public ListenableFuture<ControllerResult> addPlaylistItem(int index, @NonNull String mediaId) {
         synchronized (mLock) {
             if (!mConnected) {
                 Log.w(TAG, "Session isn't active", new IllegalStateException());
                 return createFutureWithResult(RESULT_CODE_DISCONNECTED);
             }
             mControllerCompat.addQueueItem(
-                    MediaUtils2.convertToMediaMetadataCompat(item.getMetadata()).getDescription(),
-                    index);
+                    MediaUtils2.createMediaDescriptionCompat(mediaId), index);
         }
         return createFutureWithResult(RESULT_CODE_SUCCESS);
     }
 
     @Override
-    public ListenableFuture<ControllerResult> removePlaylistItem(@NonNull MediaItem2 item) {
+    public ListenableFuture<ControllerResult> removePlaylistItem(@NonNull int index) {
         synchronized (mLock) {
             if (!mConnected) {
                 Log.w(TAG, "Session isn't active", new IllegalStateException());
                 return createFutureWithResult(RESULT_CODE_DISCONNECTED);
             }
-            mControllerCompat.removeQueueItem(
-                    MediaUtils2.convertToQueueItem(item).getDescription());
+            if (mQueue == null || index < 0 || index >= mQueue.size()) {
+                return createFutureWithResult(RESULT_CODE_BAD_VALUE);
+            }
+            mControllerCompat.removeQueueItem(mQueue.get(index).getDescription());
         }
         return createFutureWithResult(RESULT_CODE_SUCCESS);
     }
 
     @Override
     public ListenableFuture<ControllerResult> replacePlaylistItem(int index,
-            @NonNull MediaItem2 item) {
+            @NonNull String mediaId) {
         synchronized (mLock) {
             if (!mConnected) {
                 Log.w(TAG, "Session isn't active", new IllegalStateException());
                 return createFutureWithResult(RESULT_CODE_DISCONNECTED);
             }
-            if (mPlaylist == null || mPlaylist.size() <= index) {
+            if (mPlaylist == null || index < 0 || mPlaylist.size() <= index) {
                 return createFutureWithResult(RESULT_CODE_DISCONNECTED);
             }
-            removePlaylistItem(mPlaylist.get(index));
-            addPlaylistItem(index, item);
+            removePlaylistItem(index);
+            addPlaylistItem(index, mediaId);
         }
         return createFutureWithResult(RESULT_CODE_SUCCESS);
     }
@@ -707,15 +708,15 @@
     }
 
     @Override
-    public ListenableFuture<ControllerResult> skipToPlaylistItem(@NonNull MediaItem2 item) {
+    public ListenableFuture<ControllerResult> skipToPlaylistItem(@NonNull int index) {
         synchronized (mLock) {
             if (!mConnected) {
                 Log.w(TAG, "Session isn't active", new IllegalStateException());
                 return createFutureWithResult(RESULT_CODE_DISCONNECTED);
             }
-            mSkipToPlaylistItem = item;
+            mSkipToPlaylistIndex = index;
             mControllerCompat.getTransportControls().skipToQueueItem(
-                    MediaUtils2.convertToQueueItem(item).getQueueId());
+                    mQueue.get(index).getQueueId());
         }
         return createFutureWithResult(RESULT_CODE_SUCCESS);
     }
@@ -877,8 +878,15 @@
             mRepeatMode = mControllerCompat.getRepeatMode();
             mShuffleMode = mControllerCompat.getShuffleMode();
 
-            mPlaylist = MediaUtils2.convertQueueItemListToMediaItem2List(
-                    mControllerCompat.getQueue());
+            mQueue = MediaUtils2.removeNullElements(mControllerCompat.getQueue());
+            if (mQueue == null || mQueue.size() == 0) {
+                // MediaSessionCompat can set queue as null or empty. However, SessionPlayer2 should
+                // not set playlist as null or empty. Therefore, we treat them as the same.
+                mQueue = null;
+                mPlaylist = null;
+            } else {
+                mPlaylist = MediaUtils2.convertQueueItemListToMediaItem2List(mQueue);
+            }
             mPlaylistMetadata = MediaUtils2.convertToMediaMetadata2(
                     mControllerCompat.getQueueTitle());
 
@@ -971,27 +979,26 @@
             return;
         }
 
-        if (mPlaylist == null) {
+        if (mQueue == null) {
             mCurrentMediaItemIndex = -1;
             mCurrentMediaItem = MediaUtils2.convertToMediaItem2(metadata);
             return;
         }
 
-        String mediaId = metadata.getString(METADATA_KEY_MEDIA_ID);
         if (mPlaybackStateCompat != null) {
-            // If playback state is updated before, compare UUID using queue id and media id.
-            UUID uuid = MediaUtils2.createUuidByQueueIdAndMediaId(
-                    mPlaybackStateCompat.getActiveQueueItemId(), mediaId);
-            for (int i = 0; i < mPlaylist.size(); ++i) {
-                MediaItem2 item = mPlaylist.get(i);
-                if (item != null && uuid.equals(item.getUuid())) {
-                    mCurrentMediaItem = item;
+            // If playback state is updated before, compare use queue id and media id.
+            long queueId = mPlaybackStateCompat.getActiveQueueItemId();
+            for (int i = 0; i < mQueue.size(); ++i) {
+                QueueItem item = mQueue.get(i);
+                if (item.getQueueId() == queueId) {
+                    mCurrentMediaItem = MediaUtils2.convertToMediaItem2(metadata);
                     mCurrentMediaItemIndex = i;
                     return;
                 }
             }
         }
 
+        String mediaId = metadata.getString(METADATA_KEY_MEDIA_ID);
         if (mediaId == null) {
             mCurrentMediaItemIndex = -1;
             mCurrentMediaItem = MediaUtils2.convertToMediaItem2(metadata);
@@ -1000,21 +1007,22 @@
 
         // Need to find the media item in the playlist using mediaId.
         // Note that there can be multiple media items with the same media id.
-        if (mSkipToPlaylistItem != null && mediaId.equals(mSkipToPlaylistItem.getMediaId())) {
+        if (mSkipToPlaylistIndex >= 0 && mSkipToPlaylistIndex < mQueue.size()
+                && TextUtils.equals(mediaId,
+                        mQueue.get(mSkipToPlaylistIndex).getDescription().getMediaId())) {
             // metadata changed after skipToPlaylistIItem() was called.
-            mCurrentMediaItem = mSkipToPlaylistItem;
-            mCurrentMediaItemIndex = mPlaylist.indexOf(mSkipToPlaylistItem);
-            mSkipToPlaylistItem = null;
+            mCurrentMediaItem = MediaUtils2.convertToMediaItem2(metadata);
+            mCurrentMediaItemIndex = mSkipToPlaylistIndex;
+            mSkipToPlaylistIndex = -1;
             return;
         }
 
-        MediaItem2 item;
         // Find mediaId from the playlist.
-        for (int i = 0; i < mPlaylist.size(); ++i) {
-            item = mPlaylist.get(i);
-            if (item != null && mediaId.equals(item.getMediaId())) {
+        for (int i = 0; i < mQueue.size(); ++i) {
+            QueueItem item = mQueue.get(i);
+            if (TextUtils.equals(mediaId, item.getDescription().getMediaId())) {
                 mCurrentMediaItemIndex = i;
-                mCurrentMediaItem = item;
+                mCurrentMediaItem = MediaUtils2.convertToMediaItem2(metadata);
                 return;
             }
         }
@@ -1077,8 +1085,10 @@
         @Override
         public void onPlaybackStateChanged(final PlaybackStateCompat state) {
             final PlaybackStateCompat prevState;
+            final MediaItem2 prevItem;
             final MediaItem2 currentItem;
             synchronized (mLock) {
+                prevItem = mCurrentMediaItem;
                 prevState = mPlaybackStateCompat;
                 mPlaybackStateCompat = state;
                 mPlayerState = MediaUtils2.convertToPlayerState(state);
@@ -1095,6 +1105,16 @@
                 }
                 currentItem = mCurrentMediaItem;
             }
+
+            if (prevItem != currentItem) {
+                mCallbackExecutor.execute(new Runnable() {
+                    @Override
+                    public void run() {
+                        mCallback.onCurrentMediaItemChanged(mInstance, currentItem);
+                    }
+                });
+            }
+
             if (state == null) {
                 if (prevState != null) {
                     mCallbackExecutor.execute(new Runnable() {
@@ -1158,8 +1178,20 @@
 
         @Override
         public void onMetadataChanged(MediaMetadataCompat metadata) {
+            final MediaItem2 prevItem;
+            final MediaItem2 currentItem;
             synchronized (mLock) {
+                prevItem = mCurrentMediaItem;
                 setCurrentMediaItemLocked(metadata);
+                currentItem = mCurrentMediaItem;
+            }
+            if (prevItem != currentItem) {
+                mCallbackExecutor.execute(new Runnable() {
+                    @Override
+                    public void run() {
+                        mCallback.onCurrentMediaItemChanged(mInstance, currentItem);
+                    }
+                });
             }
         }
 
@@ -1168,12 +1200,15 @@
             final List<MediaItem2> playlist;
             final MediaMetadata2 playlistMetadata;
             synchronized (mLock) {
-                mQueue = queue;
-                mPlaylist = MediaUtils2.convertQueueItemListToMediaItem2List(queue);
-                if (mPlaylist == null) {
-                    // MediaSessionCompat can set queue as null. However, SessionPlayer2 should not
-                    // set playlist as null. Therefore, we treat a null queue as an empty playlist.
-                    mPlaylist = new ArrayList<>();
+                mQueue = MediaUtils2.removeNullElements(queue);
+                if (mQueue == null || mQueue.size() == 0) {
+                    // MediaSessionCompat can set queue as null or empty. However, SessionPlayer2
+                    // should not set playlist as null or empty. Therefore, we treat them as the
+                    // same.
+                    mQueue = null;
+                    mPlaylist = null;
+                } else {
+                    mPlaylist = MediaUtils2.convertQueueItemListToMediaItem2List(mQueue);
                 }
                 playlist = mPlaylist;
                 playlistMetadata = mPlaylistMetadata;
diff --git a/media2/src/main/java/androidx/media2/MediaController2Stub.java b/media2/src/main/java/androidx/media2/MediaController2Stub.java
index df244e8..14f2050 100644
--- a/media2/src/main/java/androidx/media2/MediaController2Stub.java
+++ b/media2/src/main/java/androidx/media2/MediaController2Stub.java
@@ -30,7 +30,6 @@
 import androidx.media2.MediaSession2.SessionResult;
 import androidx.media2.SessionPlayer2.BuffState;
 import androidx.versionedparcelable.ParcelImpl;
-import androidx.versionedparcelable.ParcelUtils;
 
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
@@ -50,6 +49,9 @@
 
     @Override
     public void onSessionResult(int seq, ParcelImpl sessionResult) {
+        if (sessionResult == null) {
+            return;
+        }
         final MediaController2ImplBase controller;
         try {
             controller = getController();
@@ -57,24 +59,36 @@
             Log.w(TAG, "Don't fail silently here. Highly likely a bug");
             return;
         }
-        SessionResult result = ParcelUtils.fromParcelable(sessionResult);
+        SessionResult result = MediaUtils2.fromParcelable(sessionResult);
+        if (result == null) {
+            return;
+        }
         mSequencedFutureManager.setFutureResult(seq, ControllerResult.from(result));
     }
 
     @Override
     public void onLibraryResult(int seq, ParcelImpl libraryResult) {
+        if (libraryResult == null) {
+            return;
+        }
         try {
             final MediaBrowser2 browser = getBrowser();
         } catch (IllegalStateException e) {
             Log.w(TAG, "Don't fail silently here. Highly likely a bug");
             return;
         }
-        LibraryResult result = ParcelUtils.fromParcelable(libraryResult);
+        LibraryResult result = MediaUtils2.fromParcelable(libraryResult);
+        if (result == null) {
+            return;
+        }
         mSequencedFutureManager.setFutureResult(seq, BrowserResult.from(result));
     }
 
     @Override
     public void onCurrentMediaItemChanged(ParcelImpl item) {
+        if (item == null) {
+            return;
+        }
         final MediaController2ImplBase controller;
         try {
             controller = getController();
@@ -82,7 +96,7 @@
             Log.w(TAG, "Don't fail silently here. Highly likely a bug");
             return;
         }
-        controller.notifyCurrentMediaItemChanged((MediaItem2) ParcelUtils.fromParcelable(item));
+        controller.notifyCurrentMediaItemChanged((MediaItem2) MediaUtils2.fromParcelable(item));
     }
 
     @Override
@@ -112,6 +126,9 @@
     @Override
     public void onBufferingStateChanged(ParcelImpl item, @BuffState int state,
             long bufferedPositionMs) {
+        if (item == null) {
+            return;
+        }
         final MediaController2ImplBase controller;
         try {
             controller = getController();
@@ -119,7 +136,7 @@
             Log.w(TAG, "Don't fail silently here. Highly likely a bug");
             return;
         }
-        MediaItem2 item2 = (item == null) ? null : (MediaItem2) ParcelUtils.fromParcelable(item);
+        MediaItem2 item2 = MediaUtils2.fromParcelable(item);
         if (item2 == null) {
             Log.w(TAG, "onBufferingStateChanged(): Ignoring null item");
             return;
@@ -128,7 +145,10 @@
     }
 
     @Override
-    public void onPlaylistChanged(ParcelImplListSlice listSlice, Bundle metadataBundle) {
+    public void onPlaylistChanged(ParcelImplListSlice listSlice, ParcelImpl metadata) {
+        if (metadata == null) {
+            return;
+        }
         final MediaController2ImplBase controller;
         try {
             controller = getController();
@@ -138,16 +158,15 @@
         }
         List<MediaItem2> playlist =
                 MediaUtils2.convertParcelImplListSliceToMediaItem2List(listSlice);
-        if (playlist == null) {
-            Log.w(TAG, "onPlaylistChanged(): Ignoring null playlist");
-            return;
-        }
-        MediaMetadata2 metadata = MediaMetadata2.fromBundle(metadataBundle);
-        controller.notifyPlaylistChanges(playlist, metadata);
+        controller.notifyPlaylistChanges(playlist,
+                (MediaMetadata2) MediaUtils2.fromParcelable(metadata));
     }
 
     @Override
-    public void onPlaylistMetadataChanged(Bundle metadataBundle) throws RuntimeException {
+    public void onPlaylistMetadataChanged(ParcelImpl metadata) throws RuntimeException {
+        if (metadata == null) {
+            return;
+        }
         final MediaController2ImplBase controller;
         try {
             controller = getController();
@@ -155,8 +174,8 @@
             Log.w(TAG, "Don't fail silently here. Highly likely a bug");
             return;
         }
-        MediaMetadata2 metadata = MediaMetadata2.fromBundle(metadataBundle);
-        controller.notifyPlaylistMetadataChanges(metadata);
+        controller.notifyPlaylistMetadataChanges(
+                (MediaMetadata2) MediaUtils2.fromParcelable(metadata));
     }
 
     @Override
@@ -197,6 +216,9 @@
 
     @Override
     public void onPlaybackInfoChanged(ParcelImpl playbackInfo) throws RuntimeException {
+        if (playbackInfo == null) {
+            return;
+        }
         if (DEBUG) {
             Log.d(TAG, "onPlaybackInfoChanged");
         }
@@ -207,7 +229,7 @@
             Log.w(TAG, "Don't fail silently here. Highly likely a bug");
             return;
         }
-        PlaybackInfo info = ParcelUtils.fromParcelable(playbackInfo);
+        PlaybackInfo info = MediaUtils2.fromParcelable(playbackInfo);
         if (info == null) {
             Log.w(TAG, "onPlaybackInfoChanged(): Ignoring null playbackInfo");
             return;
@@ -245,6 +267,10 @@
             ParcelImpl currentItem, long positionEventTimeMs, long positionMs, float playbackSpeed,
             long bufferedPositionMs, ParcelImpl playbackInfo, int shuffleMode, int repeatMode,
             ParcelImplListSlice listSlice, PendingIntent sessionActivity) {
+        if (sessionBinder == null || commandGroup == null || currentItem == null
+                || playbackInfo == null) {
+            return;
+        }
         final MediaController2ImplBase controller = mController.get();
         if (controller == null) {
             if (DEBUG) {
@@ -255,10 +281,10 @@
         List<MediaItem2> itemList =
                 MediaUtils2.convertParcelImplListSliceToMediaItem2List(listSlice);
         controller.onConnectedNotLocked(sessionBinder,
-                (SessionCommandGroup2) ParcelUtils.fromParcelable(commandGroup), playerState,
-                (MediaItem2) ParcelUtils.fromParcelable(currentItem),
+                (SessionCommandGroup2) MediaUtils2.fromParcelable(commandGroup), playerState,
+                (MediaItem2) MediaUtils2.fromParcelable(currentItem),
                 positionEventTimeMs, positionMs, playbackSpeed, bufferedPositionMs,
-                (PlaybackInfo) ParcelUtils.fromParcelable(playbackInfo), repeatMode, shuffleMode,
+                (PlaybackInfo) MediaUtils2.fromParcelable(playbackInfo), repeatMode, shuffleMode,
                 itemList, sessionActivity);
     }
 
@@ -293,7 +319,7 @@
         }
         List<CommandButton> layout = new ArrayList<>();
         for (int i = 0; i < commandButtonList.size(); i++) {
-            CommandButton button = ParcelUtils.fromParcelable(commandButtonList.get(i));
+            CommandButton button = MediaUtils2.fromParcelable(commandButtonList.get(i));
             if (button != null) {
                 layout.add(button);
             }
@@ -303,6 +329,9 @@
 
     @Override
     public void onAllowedCommandsChanged(ParcelImpl commands) {
+        if (commands == null) {
+            return;
+        }
         final MediaController2ImplBase controller;
         try {
             controller = getController();
@@ -314,7 +343,7 @@
             // TODO(jaewan): Revisit here. Could be a bug
             return;
         }
-        SessionCommandGroup2 commandGroup = ParcelUtils.fromParcelable(commands);
+        SessionCommandGroup2 commandGroup = MediaUtils2.fromParcelable(commands);
         if (commandGroup == null) {
             Log.w(TAG, "onAllowedCommandsChanged(): Ignoring null commands");
             return;
@@ -324,6 +353,9 @@
 
     @Override
     public void onCustomCommand(int seq, ParcelImpl commandParcel, Bundle args) {
+        if (commandParcel == null) {
+            return;
+        }
         final MediaController2ImplBase controller;
         try {
             controller = getController();
@@ -331,7 +363,7 @@
             Log.w(TAG, "Don't fail silently here. Highly likely a bug");
             return;
         }
-        SessionCommand2 command = ParcelUtils.fromParcelable(commandParcel);
+        SessionCommand2 command = MediaUtils2.fromParcelable(commandParcel);
         if (command == null) {
             Log.w(TAG, "sendCustomCommand(): Ignoring null command");
             return;
@@ -345,6 +377,9 @@
     @Override
     public void onSearchResultChanged(final String query, final int itemCount,
             final ParcelImpl libraryParams) throws RuntimeException {
+        if (libraryParams == null) {
+            return;
+        }
         if (TextUtils.isEmpty(query)) {
             Log.w(TAG, "onSearchResultChanged(): Ignoring empty query");
             return;
@@ -367,7 +402,7 @@
             @Override
             public void run() {
                 browser.getCallback().onSearchResultChanged(browser, query, itemCount,
-                        (LibraryParams) ParcelUtils.fromParcelable(libraryParams));
+                        (LibraryParams) MediaUtils2.fromParcelable(libraryParams));
             }
         });
     }
@@ -375,8 +410,11 @@
     @Override
     public void onChildrenChanged(final String parentId, final int itemCount,
             final ParcelImpl libraryParams) {
-        if (parentId == null) {
-            Log.w(TAG, "onChildrenChanged(): Ignoring null parentId");
+        if (libraryParams == null) {
+            return;
+        }
+        if (TextUtils.isEmpty(parentId)) {
+            Log.w(TAG, "onChildrenChanged(): Ignoring empty parentId");
             return;
         }
         if (itemCount < 0) {
@@ -396,8 +434,10 @@
         browser.getCallbackExecutor().execute(new Runnable() {
             @Override
             public void run() {
+                // TODO (b/118472216): Find all ParcelUtils.fromParcelable usages, and null check
+                // before calling it.
                 browser.getCallback().onChildrenChanged(browser, parentId, itemCount,
-                        (LibraryParams) ParcelUtils.fromParcelable(libraryParams));
+                        (LibraryParams) MediaUtils2.fromParcelable(libraryParams));
             }
         });
     }
diff --git a/media2/src/main/java/androidx/media2/MediaInterface2.java b/media2/src/main/java/androidx/media2/MediaInterface2.java
index 2208c85..7b705fc 100644
--- a/media2/src/main/java/androidx/media2/MediaInterface2.java
+++ b/media2/src/main/java/androidx/media2/MediaInterface2.java
@@ -28,7 +28,7 @@
 
     // TODO: relocate methods among different interfaces and classes.
     interface SessionPlaybackControl {
-        ListenableFuture<PlayerResult> prefetch();
+        ListenableFuture<PlayerResult> prepare();
         ListenableFuture<PlayerResult> play();
         ListenableFuture<PlayerResult> pause();
 
@@ -48,18 +48,17 @@
     interface SessionPlaylistControl {
         List<MediaItem2> getPlaylist();
         MediaMetadata2 getPlaylistMetadata();
-        ListenableFuture<PlayerResult> setPlaylist(List<MediaItem2> list,
-                MediaMetadata2 metadata);
+        ListenableFuture<PlayerResult> setPlaylist(List<MediaItem2> list, MediaMetadata2 metadata);
         ListenableFuture<PlayerResult> setMediaItem(MediaItem2 item);
         ListenableFuture<PlayerResult> updatePlaylistMetadata(MediaMetadata2 metadata);
 
         MediaItem2 getCurrentMediaItem();
-        ListenableFuture<PlayerResult> skipToPlaylistItem(MediaItem2 item);
+        ListenableFuture<PlayerResult> skipToPlaylistItem(int index);
         ListenableFuture<PlayerResult> skipToPreviousItem();
         ListenableFuture<PlayerResult> skipToNextItem();
 
         ListenableFuture<PlayerResult> addPlaylistItem(int index, MediaItem2 item);
-        ListenableFuture<PlayerResult> removePlaylistItem(MediaItem2 item);
+        ListenableFuture<PlayerResult> removePlaylistItem(int index);
         ListenableFuture<PlayerResult> replacePlaylistItem(int index, MediaItem2 item);
 
         int getRepeatMode();
@@ -69,9 +68,6 @@
     }
 
     // Common interface for session2 and controller2
-    // TODO: consider to add fastForward, rewind.
     interface SessionPlayer extends SessionPlaybackControl, SessionPlaylistControl {
-        ListenableFuture<PlayerResult> skipForward();
-        ListenableFuture<PlayerResult> skipBackward();
     }
 }
diff --git a/media2/src/main/java/androidx/media2/MediaItem2.java b/media2/src/main/java/androidx/media2/MediaItem2.java
index 289b320..a65db4f 100644
--- a/media2/src/main/java/androidx/media2/MediaItem2.java
+++ b/media2/src/main/java/androidx/media2/MediaItem2.java
@@ -16,23 +16,25 @@
 
 package androidx.media2;
 
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
 
 import android.os.Bundle;
-import android.os.ParcelUuid;
 import android.text.TextUtils;
 
-import androidx.annotation.IntDef;
+import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
+import androidx.core.util.Pair;
+import androidx.versionedparcelable.CustomVersionedParcelable;
+import androidx.versionedparcelable.NonParcelField;
 import androidx.versionedparcelable.ParcelField;
-import androidx.versionedparcelable.VersionedParcelable;
 import androidx.versionedparcelable.VersionedParcelize;
 
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.UUID;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
 
 /**
  * A class with information on a single media item with the metadata information. Here are use
@@ -48,35 +50,21 @@
  * When it's shared across the processes, we cannot guarantee that they contain the right values
  * because media items are application dependent especially for the metadata.
  * <p>
- * When its subclass is sent between {@link MediaSession2}/{@link MediaController2} or
+ * When an object of the {@link MediaItem2}'s subclass is sent across the process between
+ * {@link MediaSession2}/{@link MediaController2} or
  * {@link androidx.media2.MediaLibraryService2.MediaLibrarySession}/{@link MediaBrowser2}, the
- * subclass' data will be anonymized. The recipient need to translate if it's interested in playback
- * with it. See {@link MediaSession2.SessionCallback#onCreateMediaItem} for the detail.
+ * object will sent as if it's {@link MediaItem2}. The recipient cannot get the object with the
+ * subclasses' type. This will sanitize process specific information (e.g.
+ * {@link java.io.FileDescriptor}, {@link android.content.Context}, etc).
  * <p>
  * This object isn't a thread safe.
  */
-@VersionedParcelize
-public class MediaItem2 implements VersionedParcelable {
+@VersionedParcelize(isCustom = true)
+public class MediaItem2 extends CustomVersionedParcelable {
     // intentionally less than long.MAX_VALUE.
     // Declare this first to avoid 'illegal forward reference'.
     static final long LONG_MAX = 0x7ffffffffffffffL;
 
-    /** @hide */
-    @RestrictTo(LIBRARY_GROUP)
-    @Retention(RetentionPolicy.SOURCE)
-    @IntDef(flag = true, value = { FLAG_BROWSABLE, FLAG_PLAYABLE })
-    public @interface Flags { }
-
-    /**
-     * Flag: Indicates that the item has children of its own.
-     */
-    public static final int FLAG_BROWSABLE = 1 << 0;
-
-    /**
-     * Flag: Indicates that the item is playable.
-     */
-    public static final int FLAG_PLAYABLE = 1 << 1;
-
     /**
      * Used when a position is unknown.
      *
@@ -84,25 +72,20 @@
      */
     public static final long POSITION_UNKNOWN = LONG_MAX;
 
-    private static final String KEY_ID = "android.media.mediaitem2.id";
-    private static final String KEY_FLAGS = "android.media.mediaitem2.flags";
     private static final String KEY_METADATA = "android.media.mediaitem2.metadata";
-    private static final String KEY_UUID = "android.media.mediaitem2.uuid";
 
     @ParcelField(1)
-    String mMediaId;
-    @ParcelField(2)
-    int mFlags;
-    @ParcelField(3)
-    ParcelUuid mParcelUuid;
-    @ParcelField(4)
     MediaMetadata2 mMetadata;
-    @ParcelField(5)
+    @ParcelField(2)
     long mStartPositionMs = 0;
-    @ParcelField(6)
+    @ParcelField(3)
     long mEndPositionMs = POSITION_UNKNOWN;
-    @ParcelField(7)
-    long mDurationMs = SessionPlayer2.UNKNOWN_TIME;
+
+    @NonParcelField
+    private final Object mLock = new Object();
+    @GuardedBy("mLock")
+    @NonParcelField
+    private final List<Pair<OnMetadataChangedListener, Executor>> mListeners = new ArrayList<>();
 
     /**
      * Used for VersionedParcelable
@@ -117,50 +100,31 @@
     //       MediaItem2.
     @SuppressWarnings("WeakerAccess") /* synthetic access */
     MediaItem2(BuilderBase builder) {
-        this(builder.mUuid, builder.mMediaId, builder.mFlags, builder.mMetadata,
-                builder.mStartPositionMs, builder.mEndPositionMs, builder.mDurationMs);
+        this(builder.mMetadata, builder.mStartPositionMs, builder.mEndPositionMs);
+    }
+
+    MediaItem2(MediaItem2 item) {
+        this(item.mMetadata, item.mStartPositionMs, item.mEndPositionMs);
     }
 
     @SuppressWarnings("WeakerAccess") /* synthetic access */
-    MediaItem2(@Nullable UUID uuid, @Nullable String mediaId, Integer flags,
-            @Nullable MediaMetadata2 metadata, long startPositionMs, long endPositionMs,
-            long durationMs) {
+    MediaItem2(@Nullable MediaMetadata2 metadata, long startPositionMs, long endPositionMs) {
         if (startPositionMs > endPositionMs) {
             throw new IllegalStateException("Illegal start/end position: "
                     + startPositionMs + " : " + endPositionMs);
         }
-        if (durationMs != SessionPlayer2.UNKNOWN_TIME && endPositionMs != POSITION_UNKNOWN
-                && endPositionMs > durationMs) {
-            throw new IllegalStateException("endPositionMs shouldn't be greater than durationMs: "
-                    + " endPositionMs=" + endPositionMs + ", durationMs=" + durationMs);
-        }
-
-        mParcelUuid = new ParcelUuid((uuid != null) ? uuid : UUID.randomUUID());
-        if (mediaId != null || durationMs > 0 || flags != null) {
-            MediaMetadata2.Builder builder = metadata != null
-                    ? new MediaMetadata2.Builder(metadata) : new MediaMetadata2.Builder();
-            if (mediaId != null) {
-                builder.putString(MediaMetadata2.METADATA_KEY_MEDIA_ID, mediaId);
+        if (metadata != null && metadata.containsKey(MediaMetadata2.METADATA_KEY_DURATION)) {
+            long durationMs = metadata.getLong(MediaMetadata2.METADATA_KEY_DURATION);
+            if (durationMs != SessionPlayer2.UNKNOWN_TIME && endPositionMs != POSITION_UNKNOWN
+                    && endPositionMs > durationMs) {
+                throw new IllegalStateException("endPositionMs shouldn't be greater than"
+                        + " duration in the metdata, endPositionMs=" + endPositionMs
+                        + ", durationMs=" + durationMs);
             }
-            if (durationMs > 0) {
-                builder.putLong(MediaMetadata2.METADATA_KEY_DURATION, durationMs);
-            }
-            if (flags != null) {
-                builder.putLong(MediaMetadata2.METADATA_KEY_FLAGS, flags);
-            }
-            metadata = builder.build();
         }
-        if (metadata != null) {
-            mediaId = metadata.getString(MediaMetadata2.METADATA_KEY_MEDIA_ID);
-            durationMs = metadata.getLong(MediaMetadata2.METADATA_KEY_DURATION);
-            flags = (int) metadata.getLong(MediaMetadata2.METADATA_KEY_FLAGS);
-        }
-        mMediaId = mediaId;
-        mFlags = flags != null ? flags : 0;
         mMetadata = metadata;
         mStartPositionMs = startPositionMs;
         mEndPositionMs = endPositionMs;
-        mDurationMs = durationMs;
     }
 
     /**
@@ -172,12 +136,9 @@
     // TODO(jaewan): Remove
     public @NonNull Bundle toBundle() {
         Bundle bundle = new Bundle();
-        bundle.putString(KEY_ID, mMediaId);
-        bundle.putInt(KEY_FLAGS, mFlags);
         if (mMetadata != null) {
             bundle.putBundle(KEY_METADATA, mMetadata.toBundle());
         }
-        bundle.putParcelable(KEY_UUID, mParcelUuid);
         return bundle;
     }
 
@@ -193,38 +154,16 @@
         if (bundle == null) {
             return null;
         }
-        final ParcelUuid parcelUuid = bundle.getParcelable(KEY_UUID);
-        return fromBundle(bundle, parcelUuid);
-    }
-
-    /**
-     * Create a MediaItem2 from the {@link Bundle} with the specified {@link UUID} string.
-     * <p>
-     * {@link UUID} string can be null if it want to generate new one.
-     *
-     * @param bundle The bundle which was published by {@link MediaItem2#toBundle()}.
-     * @param parcelUuid A {@link ParcelUuid} string to override. Can be {@link null} for override.
-     * @return The newly created MediaItem2
-     */
-    static MediaItem2 fromBundle(@NonNull Bundle bundle, @Nullable ParcelUuid parcelUuid) {
-        if (bundle == null) {
-            return null;
-        }
-        final UUID uuid = (parcelUuid != null) ? parcelUuid.getUuid() : null;
-        final String id = bundle.getString(KEY_ID);
         final Bundle metadataBundle = bundle.getBundle(KEY_METADATA);
         final MediaMetadata2 metadata = metadataBundle != null
                 ? MediaMetadata2.fromBundle(metadataBundle) : null;
-        final int flags = bundle.getInt(KEY_FLAGS);
-        return new MediaItem2(uuid, id, flags, metadata, 0, 0, 0);
+        return new MediaItem2(metadata, 0, 0);
     }
 
     @Override
     public String toString() {
-        final StringBuilder sb = new StringBuilder("MediaItem2{");
-        sb.append("mMediaId=").append(mMediaId);
-        sb.append(", mFlags=").append(mFlags);
-        sb.append(", mMetadata=").append(mMetadata);
+        final StringBuilder sb = new StringBuilder(getClass().getSimpleName());
+        sb.append("{mMetadata=").append(mMetadata);
         sb.append(", mStartPositionMs=").append(mStartPositionMs);
         sb.append(", mEndPositionMs=").append(mEndPositionMs);
         sb.append('}');
@@ -232,41 +171,30 @@
     }
 
     /**
-     * Gets the flags of the item.
-     */
-    public @Flags int getFlags() {
-        return mFlags;
-    }
-
-    /**
-     * Checks whether this item is browsable.
-     * @see #FLAG_BROWSABLE
-     */
-    public boolean isBrowsable() {
-        return (mFlags & FLAG_BROWSABLE) != 0;
-    }
-
-    /**
-     * Checks whether this item is playable.
-     * @see #FLAG_PLAYABLE
-     */
-    public boolean isPlayable() {
-        return (mFlags & FLAG_PLAYABLE) != 0;
-    }
-
-    /**
-     * Sets a metadata. If the metadata is not {@code null}, its id should be matched with this
+     * Sets metadata. If the metadata is not {@code null}, its id should be matched with this
      * instance's media id.
      *
      * @param metadata metadata to update
+     * @see MediaMetadata2#METADATA_KEY_MEDIA_ID
      */
     public void setMetadata(@Nullable MediaMetadata2 metadata) {
-        if (metadata != null && !TextUtils.equals(mMediaId, metadata.getMediaId())) {
+        if (metadata != null && !TextUtils.equals(getMediaId(), metadata.getMediaId())) {
             throw new IllegalArgumentException("metadata's id should be matched with the mediaId");
         }
         mMetadata = metadata;
-        if (metadata != null) {
-            mDurationMs = metadata.getLong(MediaMetadata2.METADATA_KEY_DURATION);
+        List<Pair<OnMetadataChangedListener, Executor>> listeners = new ArrayList<>();
+        synchronized (mLock) {
+            listeners.addAll(mListeners);
+        }
+
+        for (Pair<OnMetadataChangedListener, Executor> pair : listeners) {
+            final OnMetadataChangedListener listener = pair.first;
+            pair.second.execute(new Runnable() {
+                @Override
+                public void run() {
+                    listener.onMetadataChanged(MediaItem2.this);
+                }
+            });
         }
     }
 
@@ -301,27 +229,34 @@
      * for the underlying media content.
      *
      * @return media Id from the session
+     * @hide
      */
+    // TODO: Remove
+    @RestrictTo(LIBRARY)
     public @Nullable String getMediaId() {
-        return mMediaId;
+        return mMetadata != null ? mMetadata.getString(MediaMetadata2.METADATA_KEY_MEDIA_ID) : null;
     }
 
-    @Override
-    public int hashCode() {
-        return mParcelUuid.hashCode();
-    }
-
-    @Override
-    public boolean equals(@Nullable Object obj) {
-        if (!(obj instanceof MediaItem2)) {
-            return false;
+    void addOnMetadataChangedListener(Executor executor, OnMetadataChangedListener listener) {
+        synchronized (mLock) {
+            for (Pair<OnMetadataChangedListener, Executor> pair : mListeners) {
+                if (pair.first == listener) {
+                    return;
+                }
+            }
+            mListeners.add(new Pair<>(listener, executor));
         }
-        MediaItem2 other = (MediaItem2) obj;
-        return mParcelUuid.equals(other.mParcelUuid);
     }
 
-    UUID getUuid() {
-        return mParcelUuid.getUuid();
+    void removeOnMetadataChangedListener(OnMetadataChangedListener listener) {
+        synchronized (mLock) {
+            for (int i = mListeners.size() - 1; i >= 0; i--) {
+                if (mListeners.get(i).first == listener) {
+                    mListeners.remove(i);
+                    return;
+                }
+            }
+        }
     }
 
     /**
@@ -333,55 +268,14 @@
     @RestrictTo(LIBRARY_GROUP)
     public static class BuilderBase<T extends BuilderBase> {
         @SuppressWarnings("WeakerAccess") /* synthetic access */
-        @Flags int mFlags;
-        @SuppressWarnings("WeakerAccess") /* synthetic access */
-        String mMediaId;
-        @SuppressWarnings("WeakerAccess") /* synthetic access */
         MediaMetadata2 mMetadata;
         @SuppressWarnings("WeakerAccess") /* synthetic access */
-        UUID mUuid;
-        @SuppressWarnings("WeakerAccess") /* synthetic access */
         long mStartPositionMs = 0;
         @SuppressWarnings("WeakerAccess") /* synthetic access */
         long mEndPositionMs = POSITION_UNKNOWN;
-        @SuppressWarnings("WeakerAccess") /* synthetic access */
-        long mDurationMs = SessionPlayer2.UNKNOWN_TIME;
-
-        /**
-         * Constructs a new {@link T}.
-         *
-         * @param flags flags whether it's playable and/or browsable.
-         * @see #FLAG_BROWSABLE
-         * @see #FLAG_PLAYABLE
-         */
-        public BuilderBase(@Flags int flags) {
-            mFlags = flags;
-        }
-
-        /**
-         * Set the media id of this instance. {@code null} for unset.
-         * <p>
-         * If used, this should be a persistent unique key for the underlying content so session
-         * and controller can uniquely identify a media content.
-         * <p>
-         * If the metadata is set with the {@link #setMetadata(MediaMetadata2)} and it has
-         * media id, id from {@link #setMediaId(String)} will be ignored and metadata's id will be
-         * used instead.
-         *
-         * @param mediaId media id
-         * @return this instance for chaining
-         */
-        public @NonNull T setMediaId(@Nullable String mediaId) {
-            mMediaId = mediaId;
-            return (T) this;
-        }
 
         /**
          * Set the metadata of this instance. {@code null} for unset.
-         * <p>
-         * If the metadata is set with the {@link #setMetadata(MediaMetadata2)} and it has
-         * media id, id from {@link #setMediaId(String)} will be ignored and metadata's id will be
-         * used instead.
          *
          * @param metadata metadata
          * @return this instance for chaining
@@ -421,11 +315,6 @@
             return (T) this;
         }
 
-        T setUuid(UUID uuid) {
-            mUuid = uuid;
-            return (T) this;
-        }
-
         /**
          * Build {@link MediaItem2}.
          *
@@ -440,8 +329,29 @@
      * Builder for {@link MediaItem2}.
      */
     public static class Builder extends BuilderBase<BuilderBase> {
-        public Builder(@Flags int flags) {
-            super(flags);
+        /**
+         * Default constructor
+         */
+        public Builder() {
+            super();
         }
     }
+
+    interface OnMetadataChangedListener {
+        void onMetadataChanged(MediaItem2 item);
+    }
+
+    /**
+     * @hide
+     * @param isStream
+     */
+    @RestrictTo(LIBRARY)
+    @Override
+    public void onPreParceling(boolean isStream) {
+        if (getClass() != MediaItem2.class) {
+            throw new RuntimeException("MediaItem2's subclasses shouldn't be parcelized."
+                    + " Use instead");
+        }
+        super.onPreParceling(isStream);
+    }
 }
diff --git a/media2/src/main/java/androidx/media2/MediaLibraryService2.java b/media2/src/main/java/androidx/media2/MediaLibraryService2.java
index ad4da59..350e48a 100644
--- a/media2/src/main/java/androidx/media2/MediaLibraryService2.java
+++ b/media2/src/main/java/androidx/media2/MediaLibraryService2.java
@@ -83,6 +83,11 @@
     public static final class MediaLibrarySession extends MediaSession2 {
         /**
          * Callback for the {@link MediaLibrarySession}.
+         * <p>
+         * When you return {@link LibraryResult} with media items,
+         * items must have valid {@link MediaMetadata2#METADATA_KEY_MEDIA_ID} and
+         * specify {@link MediaMetadata2#METADATA_KEY_BROWSABLE} and
+         * {@link MediaMetadata2#METADATA_KEY_PLAYABLE}.
          */
         public static class MediaLibrarySessionCallback extends MediaSession2.SessionCallback {
             /**
@@ -120,7 +125,7 @@
              *
              * @param session the session for this event
              * @param controller controller
-             * @param mediaId item id to get media item.
+             * @param mediaId non-empty media id of the requested item
              * @return a library result with a media item with the id. A runtime exception
              *         will be thrown if an invalid result is returned.
              * @see SessionCommand2#COMMAND_CODE_LIBRARY_GET_ITEM
@@ -139,7 +144,7 @@
              *
              * @param session the session for this event
              * @param controller controller
-             * @param parentId parent id to get children
+             * @param parentId non-empty parent id to get children
              * @param page page number. Starts from {@code 0}.
              * @param pageSize page size. Should be greater or equal to {@code 1}.
              * @param params library params
@@ -149,8 +154,9 @@
              * @see LibraryParams
              */
             public @NonNull LibraryResult onGetChildren(@NonNull MediaLibrarySession session,
-                    @NonNull ControllerInfo controller, @NonNull String parentId, int page,
-                    int pageSize, @Nullable LibraryParams params) {
+                    @NonNull ControllerInfo controller, @NonNull String parentId,
+                    @IntRange(from = 0) int page, @IntRange(from = 1) int pageSize,
+                    @Nullable LibraryParams params) {
                 return new LibraryResult(RESULT_CODE_NOT_SUPPORTED);
             }
 
@@ -168,7 +174,7 @@
              *
              * @param session the session for this event
              * @param controller controller
-             * @param parentId parent id
+             * @param parentId non-empty parent id
              * @param params library params
              * @return result code
              * @see SessionCommand2#COMMAND_CODE_LIBRARY_SUBSCRIBE
@@ -189,7 +195,7 @@
              *
              * @param session the session for this event
              * @param controller controller
-             * @param parentId parent id
+             * @param parentId non-empty parent id
              * @return result code
              * @see SessionCommand2#COMMAND_CODE_LIBRARY_UNSUBSCRIBE
              */
@@ -208,8 +214,8 @@
              *
              * @param session the session for this event
              * @param controller controller
-             * @param query The search query sent from the media browser. It contains keywords
-             *              separated by space.
+             * @param query The non-empty search query sent from the media browser.
+             *              It contains keywords separated by space.
              * @param params library params
              * @return result code
              * @see SessionCommand2#COMMAND_CODE_LIBRARY_SEARCH
@@ -234,7 +240,8 @@
              *
              * @param session the session for this event
              * @param controller controller
-             * @param query The search query which was previously sent through {@link #onSearch}.
+             * @param query The non-empty search query which was previously sent through
+             *              {@link #onSearch}.
              * @param page page number. Starts from {@code 0}.
              * @param pageSize page size. Should be greater or equal to {@code 1}.
              * @param params library params
@@ -245,7 +252,8 @@
              */
             public @NonNull LibraryResult onGetSearchResult(
                     @NonNull MediaLibrarySession session, @NonNull ControllerInfo controller,
-                    @NonNull String query, int page, int pageSize, @Nullable LibraryParams params) {
+                    @NonNull String query, @IntRange(from = 0) int page,
+                    @IntRange(from = 1) int pageSize, @Nullable LibraryParams params) {
                 return new LibraryResult(RESULT_CODE_NOT_SUPPORTED);
             }
         }
@@ -322,7 +330,7 @@
          * to get the list of children.
          *
          * @param controller controller to notify
-         * @param parentId parent id with changes in its children
+         * @param parentId non-empty parent id with changes in its children
          * @param itemCount number of children.
          * @param params library params
          */
@@ -345,7 +353,7 @@
          * Notify all controllers that subscribed to the parent about change in the parent's
          * children, regardless of the library params supplied by
          * {@link MediaBrowser2#subscribe(String, LibraryParams)}.
-         *  @param parentId parent id
+         *  @param parentId non-empty parent id
          * @param itemCount number of children
          * @param params library params
          */
@@ -365,7 +373,7 @@
          * Notify controller about change in the search result.
          *
          * @param controller controller to notify
-         * @param query previously sent search query from the controller.
+         * @param query previously sent non-empty search query from the controller.
          * @param itemCount the number of items that have been found in the search.
          * @param params library params
          */
@@ -573,7 +581,7 @@
              * @param recent {@code true} for recent items. {@code false} otherwise.
              * @return this builder
              */
-            public Builder setRecent(boolean recent) {
+            public @NonNull Builder setRecent(boolean recent) {
                 mRecent = recent;
                 return this;
             }
@@ -588,7 +596,7 @@
              * @param offline {@code true} for offline items. {@code false} otherwise.
              * @return this builder
              */
-            public Builder setOffline(boolean offline) {
+            public @NonNull Builder setOffline(boolean offline) {
                 mOffline = offline;
                 return this;
             }
@@ -604,7 +612,7 @@
              * @param suggested {@code true} for suggested items. {@code false} otherwise
              * @return this builder
              */
-            public Builder setSuggested(boolean suggested) {
+            public @NonNull Builder setSuggested(boolean suggested) {
                 mSuggested = suggested;
                 return this;
             }
@@ -615,7 +623,7 @@
              * @param extras The extras or null.
              * @return this builder
              */
-            public Builder setExtras(@Nullable Bundle extras) {
+            public @NonNull Builder setExtras(@Nullable Bundle extras) {
                 mBundle = extras;
                 return this;
             }
@@ -625,7 +633,7 @@
              *
              * @return new LibraryParams
              */
-            public LibraryParams build() {
+            public @NonNull LibraryParams build() {
                 return new LibraryParams(mBundle, mRecent, mOffline, mSuggested);
             }
         }
diff --git a/media2/src/main/java/androidx/media2/MediaLibraryService2LegacyStub.java b/media2/src/main/java/androidx/media2/MediaLibraryService2LegacyStub.java
index 6e7f24c..a6adcc3 100644
--- a/media2/src/main/java/androidx/media2/MediaLibraryService2LegacyStub.java
+++ b/media2/src/main/java/androidx/media2/MediaLibraryService2LegacyStub.java
@@ -81,6 +81,9 @@
             return null;
         }
         final ControllerInfo controller = getCurrentController();
+        if (controller == null) {
+            return null;
+        }
         if (getConnectedControllersManager().isAllowedCommand(controller,
                 SessionCommand2.COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT)) {
             // Call callbacks directly instead of execute on the executor. Here's the reason.
@@ -533,7 +536,7 @@
                     for (int i = 0; i < searchRequests.size(); i++) {
                         SearchRequest request = searchRequests.get(i);
                         int page = 0;
-                        int pageSize = 0;
+                        int pageSize = Integer.MAX_VALUE;
                         if (request.mExtras != null) {
                             try {
                                 request.mExtras.setClassLoader(
diff --git a/media2/src/main/java/androidx/media2/MediaLibrarySessionImplBase.java b/media2/src/main/java/androidx/media2/MediaLibrarySessionImplBase.java
index 3bf7f2b..97fae38 100644
--- a/media2/src/main/java/androidx/media2/MediaLibrarySessionImplBase.java
+++ b/media2/src/main/java/androidx/media2/MediaLibrarySessionImplBase.java
@@ -23,6 +23,7 @@
 import android.content.Context;
 import android.os.RemoteException;
 import android.support.v4.media.session.MediaSessionCompat.Token;
+import android.text.TextUtils;
 import android.util.Log;
 
 import androidx.annotation.GuardedBy;
@@ -154,13 +155,15 @@
             int pageSize) {
         returnedResult = ensureNonNullResult(returnedResult);
         if (returnedResult.getResultCode() == RESULT_CODE_SUCCESS) {
-            if (returnedResult.getMediaItems() == null) {
+            List<MediaItem2> items = returnedResult.getMediaItems();
+
+            if (items == null) {
                 if (THROW_EXCEPTION_FOR_INVALID_RETURN) {
                     throw new RuntimeException("List shouldn't be null for the success");
                 }
                 return new LibraryResult(RESULT_CODE_UNKNOWN_ERROR);
             }
-            if (returnedResult.getMediaItems().size() > pageSize) {
+            if (items.size() > pageSize) {
                 if (THROW_EXCEPTION_FOR_INVALID_RETURN) {
                     throw new RuntimeException("List shouldn't contain items more than pageSize"
                             + ", size=" + returnedResult.getMediaItems().size()
@@ -168,6 +171,11 @@
                 }
                 return new LibraryResult(RESULT_CODE_UNKNOWN_ERROR);
             }
+            for (MediaItem2 item : items) {
+                if (!isValidItem(item)) {
+                    return new LibraryResult(RESULT_CODE_UNKNOWN_ERROR);
+                }
+            }
         }
         return returnedResult;
     }
@@ -175,15 +183,52 @@
     private LibraryResult ensureNonNullResultWithValidItem(LibraryResult returnedResult) {
         returnedResult = ensureNonNullResult(returnedResult);
         if (returnedResult.getResultCode() == RESULT_CODE_SUCCESS) {
-            if (returnedResult.getMediaItem() == null) {
-                if (THROW_EXCEPTION_FOR_INVALID_RETURN) {
-                    throw new RuntimeException("Item shouldn't be null for the success");
-                }
+            if (!isValidItem(returnedResult.getMediaItem())) {
                 return new LibraryResult(RESULT_CODE_UNKNOWN_ERROR);
             }
         }
         return returnedResult;
     }
+
+    private boolean isValidItem(MediaItem2 item) {
+        if (item == null) {
+            if (THROW_EXCEPTION_FOR_INVALID_RETURN) {
+                throw new RuntimeException("Item shouldn't be null for the success");
+            }
+            return false;
+        }
+        if (TextUtils.isEmpty(item.getMediaId())) {
+            if (THROW_EXCEPTION_FOR_INVALID_RETURN) {
+                throw new RuntimeException(
+                        "Media ID of an item shouldn't be empty for the success");
+            }
+            return false;
+        }
+        MediaMetadata2 metadata = item.getMetadata();
+        if (metadata == null) {
+            if (THROW_EXCEPTION_FOR_INVALID_RETURN) {
+                throw new RuntimeException(
+                        "Metadata of an item shouldn't be null for the success");
+            }
+            return false;
+        }
+        if (!metadata.containsKey(MediaMetadata2.METADATA_KEY_BROWSABLE)) {
+            if (THROW_EXCEPTION_FOR_INVALID_RETURN) {
+                throw new RuntimeException(
+                        "METADATA_KEY_BROWSABLE should be specified in metadata of an item");
+            }
+            return false;
+        }
+        if (!metadata.containsKey(MediaMetadata2.METADATA_KEY_PLAYABLE)) {
+            if (THROW_EXCEPTION_FOR_INVALID_RETURN) {
+                throw new RuntimeException(
+                        "METADATA_KEY_PLAYABLE should be specified in metadata of an item");
+            }
+            return false;
+        }
+        return true;
+    }
+
     /**
      * Called by {@link MediaSession2Stub#getLibraryRoot(IMediaController2, int, ParcelImpl)}.
      *
diff --git a/media2/src/main/java/androidx/media2/MediaMetadata2.java b/media2/src/main/java/androidx/media2/MediaMetadata2.java
index 68303d4..49f0a45 100644
--- a/media2/src/main/java/androidx/media2/MediaMetadata2.java
+++ b/media2/src/main/java/androidx/media2/MediaMetadata2.java
@@ -16,7 +16,6 @@
 
 package androidx.media2;
 
-import static androidx.annotation.RestrictTo.Scope.LIBRARY;
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
 
 import android.graphics.Bitmap;
@@ -29,6 +28,7 @@
 import androidx.annotation.RestrictTo;
 import androidx.annotation.StringDef;
 import androidx.collection.ArrayMap;
+import androidx.media2.MediaLibraryService2.MediaLibrarySession;
 import androidx.versionedparcelable.ParcelField;
 import androidx.versionedparcelable.ParcelUtils;
 import androidx.versionedparcelable.VersionedParcelable;
@@ -39,12 +39,77 @@
 import java.util.Set;
 
 /**
- * Contains metadata about an item, such as the title, artist, etc.
+ * Contains metadata about an item, such as the title, artist, etc. This is optional, but you'd
+ * better to provide this as much as possible when you're using media widget and/or session APIs.
+ * <p>
+ * The media widget components build its UI based on the metadata here. For an example,
+ * {@link androidx.media.widget.MediaControlView2} will show title from the metadata.
+ * <p>
+ * The {@link MediaLibrarySession} would require some metadata values when it provides
+ * {@link MediaItem2}s to {@link MediaBrowser2}.
+ * <p>
+ * Topics covered here:
+ * <ol>
+ * <li><a href="#MediaId">Media ID</a>
+ * <li><a href="#Browsable">Browsable type</a>
+ * <li><a href="#Playable">Playable</a>
+ * <li><a href="#Duration">Duration</a>
+ * <li><a href="#UserRating">User rating</a>
+ * </ol>
+ * <a name="MediaId"></a>
+ * <h3>{@link MediaMetadata2#METADATA_KEY_MEDIA_ID Media ID}</h3>
+ * <p>
+ * If set, the media ID must be the persistent key for the underlying media contents, so
+ * {@link MediaController2} and {@link MediaBrowser2} can store the information and reuse it later.
+ * Some APIs requires a media ID (e.g. {@link MediaController2#setRating}, so you'd better specify
+ * one.
+ * <p>
+ * Typical example of using media ID is the URI of the contents, but use it with the caution because
+ * the metadata is shared across the process in plain text.
+ * <p>
+ * The {@link MediaLibrarySession} would require it for the library root, so {@link MediaBrowser2}
+ * can call subsequent {@link MediaBrowser2#getChildren} with the ID.
+ * <p>
+ * <a name="Browsable"></a>
+ * <h3>{@link MediaMetadata2#METADATA_KEY_BROWSABLE Browsable type}</h3>
+ * <p>
+ * Browsable defines whether the media item has children and type of children if any. With this,
+ * {@link MediaBrowser2} can know whether the subsequent {@link MediaBrowser2#getChildren} would
+ * successfuly run.
+ * <p>
+ * The {@link MediaLibrarySession} would require the explicit browsable type for the the media items
+ * returned by the
+ * {@link androidx.media2.MediaLibraryService2.MediaLibrarySession.MediaLibrarySessionCallback}.
+ * <p>
+ * <a name="Playable"></a>
+ * <h3>{@link MediaMetadata2#METADATA_KEY_PLAYABLE Playable type}</h3>
+ * <p>
+ * Playable defines whether the media item can be played or not. It may be possible for a playlist
+ * to contain a media item which isn't playable in order to show a disabled media item.
+ * <p>
+ * The {@link MediaLibrarySession} would require the explicit playable value for the the media items
+ * returned by the
+ * {@link androidx.media2.MediaLibraryService2.MediaLibrarySession.MediaLibrarySessionCallback}.
+ * <p>
+ * <a name="Duration"></a>
+ * <li><a href="#Duration">{@link MediaMetadata2#METADATA_KEY_DURATION Duration}</a>
+ * The duration is the length of the contents. The {@link MediaController2} can only get the
+ * duration through the metadata. This tells when would the playback ends, and also tells about the
+ * allowed range of {@link MediaController2#seekTo(long)}.
+ * <p>
+ * If it's not set by developer, {@link MediaSession2} would update the duration in the metadata
+ * with the {@link SessionPlayer2#getDuration()}.
+ * <p>
+ * <a name="UserRating"></a>
+ * <li><a href="#UserRating">{@link MediaMetadata2#METADATA_KEY_USER_RATING User rating}</a>
+ * <p>
+ * Prefer to have unrated user rating instead of {@code null}, so {@link MediaController2} can know
+ * the possible user rating type for calling {@link MediaController2#setRating(String, Rating2)}.
  */
 // New version of MediaMetadata with following changes
 //   - Don't implement Parcelable for updatable support.
 //   - Also support MediaDescription features. MediaDescription is deprecated instead because
-//     it was insufficient for controller to display media contents.
+//     it was insufficient for controller to display media contents. (e.g. duration is missing)
 @VersionedParcelize
 public final class MediaMetadata2 implements VersionedParcelable {
     private static final String TAG = "MediaMetadata2";
@@ -252,10 +317,12 @@
 
     /**
      * The metadata key for a {@link Rating2} typed value to retrieve the information about the
-     * user's rating for the media.
+     * user's rating for the media. Prefer to have unrated user rating instead of {@code null}, so
+     * {@link MediaController2} can know the possible user rating type.
      *
      * @see Builder#putRating(String, Rating2)
      * @see #getRating(String)
+     * @see <a href="#UserRating">User rating</a>
      */
     public static final String METADATA_KEY_USER_RATING = "android.media.metadata.USER_RATING";
 
@@ -340,14 +407,14 @@
     /**
      * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
      * information about the media ID of the content. This value is specific to the
-     * service providing the content. If used, this should be a persistent
-     * unique key for the underlying content. This ID is used by {@link MediaController2} and
-     * {@link MediaBrowser2}.
+     * service providing the content. If used, this should be a persistent key for the underlying
+     * content. This ID is used by {@link MediaController2} and {@link MediaBrowser2}.
      *
      * @see Builder#putText(String, CharSequence)
      * @see Builder#putString(String, String)
      * @see #getText(String)
      * @see #getString(String)
+     * @see <a href="#MediaID">Media ID</a>
      */
     public static final String METADATA_KEY_MEDIA_ID = "android.media.metadata.MEDIA_ID";
 
@@ -390,66 +457,102 @@
             "android.media.metadata.RADIO_PROGRAM_NAME";
 
     /**
-     * The metadata key for a {@link Long} typed value to retrieve the information about the
-     * bluetooth folder type of the media specified in the section 6.10.2.2 of the Bluetooth
-     * AVRCP 1.5. It should be one of the following:
+     * The metadata key for a {@link Long} typed value to retrieve the information about the type
+     * of browsable. It should be one of the following:
      * <ul>
-     * <li>{@link #BT_FOLDER_TYPE_MIXED}</li>
-     * <li>{@link #BT_FOLDER_TYPE_TITLES}</li>
-     * <li>{@link #BT_FOLDER_TYPE_ALBUMS}</li>
-     * <li>{@link #BT_FOLDER_TYPE_ARTISTS}</li>
-     * <li>{@link #BT_FOLDER_TYPE_GENRES}</li>
-     * <li>{@link #BT_FOLDER_TYPE_PLAYLISTS}</li>
-     * <li>{@link #BT_FOLDER_TYPE_YEARS}</li>
+     * <li>{@link #BROWSABLE_TYPE_NONE}</li>
+     * <li>{@link #BROWSABLE_TYPE_MIXED}</li>
+     * <li>{@link #BROWSABLE_TYPE_TITLES}</li>
+     * <li>{@link #BROWSABLE_TYPE_ALBUMS}</li>
+     * <li>{@link #BROWSABLE_TYPE_ARTISTS}</li>
+     * <li>{@link #BROWSABLE_TYPE_GENRES}</li>
+     * <li>{@link #BROWSABLE_TYPE_PLAYLISTS}</li>
+     * <li>{@link #BROWSABLE_TYPE_YEARS}</li>
      * </ul>
+     * <p>
+     * The values other than {@link #BROWSABLE_TYPE_NONE} mean that the media item has children.[
+     * <p>
+     * This matches with the bluetooth folder type of the media specified in the section 6.10.2.2 of
+     * the Bluetooth AVRCP 1.5.
      *
      * @see Builder#putLong(String, long)
      * @see #getLong(String)
+     * @see <a href="#Browsable">Browsable</a>
      */
-    public static final String METADATA_KEY_BT_FOLDER_TYPE =
+    public static final String METADATA_KEY_BROWSABLE =
             "android.media.metadata.BT_FOLDER_TYPE";
 
     /**
-     * The type of folder that is unknown or contains media elements of mixed types as specified in
-     * the section 6.10.2.2 of the Bluetooth AVRCP 1.5.
+     * The type of browsable for non-browsable media item.
      */
-    public static final long BT_FOLDER_TYPE_MIXED = 0;
+    public static final long BROWSABLE_TYPE_NONE = -1;
 
     /**
-     * The type of folder that contains media elements only as specified in the section 6.10.2.2 of
+     * The type of browsable that is unknown or contains media items of mixed types.
+     * <p>
+     * This value matches with the folder type 'Mixed' as specified in the section 6.10.2.2 of the
+     * Bluetooth AVRCP 1.5.
+     */
+    public static final long BROWSABLE_TYPE_MIXED = 0;
+
+    /**
+     * The type of browsable that only contains playable media items.
+     * <p>
+     * This value matches with the folder type 'Titles' as specified in the section 6.10.2.2 of the
+     * Bluetooth AVRCP 1.5.
+     */
+    public static final long BROWSABLE_TYPE_TITLES = 1;
+
+    /**
+     * The type of browsable that contains browsable items categorized by album.
+     * <p>
+     * This value matches with the folder type 'Albums' as specified in the section 6.10.2.2 of the
+     * Bluetooth AVRCP 1.5.
+     */
+    public static final long BROWSABLE_TYPE_ALBUMS = 2;
+
+    /**
+     * The type of browsable that contains browsable items categorized by artist.
+     * <p>
+     * This value matches with the folder type 'Artists' as specified in the section 6.10.2.2 of the
+     * Bluetooth AVRCP 1.5.
+     */
+    public static final long BROWSABLE_TYPE_ARTISTS = 3;
+
+    /**
+     * The type of browsable that contains browsable items categorized by genre.
+     * <p>
+     * This value matches with the folder type 'Genres' as specified in the section 6.10.2.2 of the
+     * Bluetooth AVRCP 1.5.
+     */
+    public static final long BROWSABLE_TYPE_GENRES = 4;
+
+    /**
+     * The type of browsable that contains browsable items categorized by playlist.
+     * <p>
+     * This value matches with the folder type 'Playlists' as specified in the section 6.10.2.2 of
      * the Bluetooth AVRCP 1.5.
      */
-    public static final long BT_FOLDER_TYPE_TITLES = 1;
+    public static final long BROWSABLE_TYPE_PLAYLISTS = 5;
 
     /**
-     * The type of folder that contains folders categorized by album as specified in the section
-     * 6.10.2.2 of the Bluetooth AVRCP 1.5.
+     * The type of browsable that contains browsable items categorized by year.
+     * <p>
+     * This value matches with the folder type 'Years' as specified in the section 6.10.2.2 of the
+     * Bluetooth AVRCP 1.5.
      */
-    public static final long BT_FOLDER_TYPE_ALBUMS = 2;
+    public static final long BROWSABLE_TYPE_YEARS = 6;
 
     /**
-     * The type of folder that contains folders categorized by artist as specified in the section
-     * 6.10.2.2 of the Bluetooth AVRCP 1.5.
+     * The metadata key for a {@link Long} typed value to retrieve the information about whether
+     * the media is playable. A value of 0 indicates it is not a playable item.
+     * A value of 1 or non-zero indicates it is playable.
+     *
+     * @see Builder#putLong(String, long)
+     * @see #getLong(String)
+     * @see <a href="#Playable">Playable</a>
      */
-    public static final long BT_FOLDER_TYPE_ARTISTS = 3;
-
-    /**
-     * The type of folder that contains folders categorized by genre as specified in the section
-     * 6.10.2.2 of the Bluetooth AVRCP 1.5.
-     */
-    public static final long BT_FOLDER_TYPE_GENRES = 4;
-
-    /**
-     * The type of folder that contains folders categorized by playlist as specified in the section
-     * 6.10.2.2 of the Bluetooth AVRCP 1.5.
-     */
-    public static final long BT_FOLDER_TYPE_PLAYLISTS = 5;
-
-    /**
-     * The type of folder that contains folders categorized by year as specified in the section
-     * 6.10.2.2 of the Bluetooth AVRCP 1.5.
-     */
-    public static final long BT_FOLDER_TYPE_YEARS = 6;
+    public static final String METADATA_KEY_PLAYABLE = "android.media.metadata.playable";
 
     /**
      * The metadata key for a {@link Long} typed value to retrieve the information about whether
@@ -508,29 +611,6 @@
     /**
      * @hide
      */
-    // TODO(jaewan): Unhide this, and revisit documentation of putLong()
-    @RestrictTo(LIBRARY)
-    public static final String METADATA_KEY_FLAGS = "android.media.metadata.FLAGS";
-
-    /**
-     * Flag: Indicates that the item has children of its own.
-     * @hide
-     */
-    // TODO(jaewan): Unhide this
-    @RestrictTo(LIBRARY)
-    public static final int FLAG_BROWSABLE = 1 << 0;
-
-    /**
-     * Flag: Indicates that the item is playable.
-     * @hide
-     */
-    // TODO(jaewan): Unhide this
-    @RestrictTo(LIBRARY)
-    public static final int FLAG_PLAYABLE = 1 << 1;
-
-    /**
-     * @hide
-     */
     @RestrictTo(LIBRARY_GROUP)
     @StringDef({METADATA_KEY_TITLE, METADATA_KEY_ARTIST, METADATA_KEY_ALBUM, METADATA_KEY_AUTHOR,
             METADATA_KEY_WRITER, METADATA_KEY_COMPOSER, METADATA_KEY_COMPILATION,
@@ -546,8 +626,8 @@
      */
     @RestrictTo(LIBRARY_GROUP)
     @StringDef({METADATA_KEY_DURATION, METADATA_KEY_YEAR, METADATA_KEY_TRACK_NUMBER,
-            METADATA_KEY_NUM_TRACKS, METADATA_KEY_DISC_NUMBER, METADATA_KEY_BT_FOLDER_TYPE,
-            METADATA_KEY_ADVERTISEMENT, METADATA_KEY_DOWNLOAD_STATUS, METADATA_KEY_FLAGS})
+            METADATA_KEY_NUM_TRACKS, METADATA_KEY_DISC_NUMBER, METADATA_KEY_BROWSABLE,
+            METADATA_KEY_PLAYABLE, METADATA_KEY_ADVERTISEMENT, METADATA_KEY_DOWNLOAD_STATUS})
     @Retention(RetentionPolicy.SOURCE)
     public @interface LongKey {}
 
@@ -575,11 +655,20 @@
     @Retention(RetentionPolicy.SOURCE)
     public @interface FloatKey {}
 
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @StringDef({METADATA_KEY_EXTRAS})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface BundleKey {}
+
     static final int METADATA_TYPE_LONG = 0;
     static final int METADATA_TYPE_TEXT = 1;
     static final int METADATA_TYPE_BITMAP = 2;
     static final int METADATA_TYPE_RATING = 3;
     static final int METADATA_TYPE_FLOAT = 4;
+    static final int METADATA_TYPE_BUNDLE = 5;
     static final ArrayMap<String, Integer> METADATA_KEYS_TYPE;
 
     static {
@@ -614,10 +703,11 @@
         METADATA_KEYS_TYPE.put(METADATA_KEY_MEDIA_URI, METADATA_TYPE_TEXT);
         METADATA_KEYS_TYPE.put(METADATA_KEY_RADIO_FREQUENCY, METADATA_TYPE_FLOAT);
         METADATA_KEYS_TYPE.put(METADATA_KEY_RADIO_PROGRAM_NAME, METADATA_TYPE_TEXT);
-        METADATA_KEYS_TYPE.put(METADATA_KEY_BT_FOLDER_TYPE, METADATA_TYPE_LONG);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_BROWSABLE, METADATA_TYPE_LONG);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_PLAYABLE, METADATA_TYPE_LONG);
         METADATA_KEYS_TYPE.put(METADATA_KEY_ADVERTISEMENT, METADATA_TYPE_LONG);
         METADATA_KEYS_TYPE.put(METADATA_KEY_DOWNLOAD_STATUS, METADATA_TYPE_LONG);
-        METADATA_KEYS_TYPE.put(METADATA_KEY_FLAGS, METADATA_TYPE_LONG);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_EXTRAS, METADATA_TYPE_BUNDLE);
     }
 
     private static final @MediaMetadata2.TextKey
@@ -749,7 +839,7 @@
         }
         Rating2 rating = null;
         try {
-            rating = ParcelUtils.fromParcelable(mBundle.getParcelable(key));
+            rating = ParcelUtils.getVersionedParcelable(mBundle, key);
         } catch (Exception e) {
             // ignore, value was not a rating
             Log.w(TAG, "Failed to retrieve a key as Rating.", e);
@@ -826,8 +916,9 @@
     }
 
     /**
-     * Gets the bundle backing the metadata object. This is available to support
-     * backwards compatibility. Apps should not modify the bundle directly.
+     * Gets the bundle backing the metadata object. This is available to support backwards
+     * compatibility. Apps shouldn't modify the bundle directly, nor share the metadata across the
+     * process with the bundle here.
      *
      * @return The Bundle backing this metadata.
      */
@@ -835,6 +926,11 @@
         return mBundle;
     }
 
+    @Override
+    public String toString() {
+        return mBundle.toString();
+    }
+
     /**
      * Creates the {@link MediaMetadata2} from the bundle that previously returned by
      * {@link #toBundle()}.
@@ -897,6 +993,15 @@
         }
 
         /**
+         * Only for the backward compatibility.
+         *
+         * @param bundle
+         */
+        Builder(Bundle bundle) {
+            mBundle = new Bundle(bundle);
+        }
+
+        /**
          * Put a CharSequence value into the metadata. Custom keys may be used,
          * but if the METADATA_KEYs defined in this class are used they may only
          * be one of the following:
@@ -994,7 +1099,8 @@
          * <li>{@link #METADATA_KEY_NUM_TRACKS}</li>
          * <li>{@link #METADATA_KEY_DISC_NUMBER}</li>
          * <li>{@link #METADATA_KEY_YEAR}</li>
-         * <li>{@link #METADATA_KEY_BT_FOLDER_TYPE}</li>
+         * <li>{@link #METADATA_KEY_BROWSABLE}</li>
+         * <li>{@link #METADATA_KEY_PLAYABLE}</li>
          * <li>{@link #METADATA_KEY_ADVERTISEMENT}</li>
          * <li>{@link #METADATA_KEY_DOWNLOAD_STATUS}</li>
          * </ul>
@@ -1041,7 +1147,7 @@
                             + " key cannot be used to put a Rating");
                 }
             }
-            mBundle.putParcelable(key, ParcelUtils.toParcelable(value));
+            ParcelUtils.putVersionedParcelable(mBundle, key, value);
             return this;
         }
 
@@ -1105,7 +1211,7 @@
          * @param extras The extras to include with this description or null.
          * @return The Builder to allow chaining
          */
-        public Builder setExtras(@Nullable Bundle extras) {
+        public @NonNull Builder setExtras(@Nullable Bundle extras) {
             mBundle.putBundle(METADATA_KEY_EXTRAS, extras);
             return this;
         }
diff --git a/media2/src/main/java/androidx/media2/XMediaPlayer.java b/media2/src/main/java/androidx/media2/MediaPlayer.java
similarity index 65%
rename from media2/src/main/java/androidx/media2/XMediaPlayer.java
rename to media2/src/main/java/androidx/media2/MediaPlayer.java
index 202aac7..9a3440b 100644
--- a/media2/src/main/java/androidx/media2/XMediaPlayer.java
+++ b/media2/src/main/java/androidx/media2/MediaPlayer.java
@@ -57,6 +57,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -64,7 +65,6 @@
 import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
-import java.util.concurrent.atomic.AtomicInteger;
 
 /**
  * A media player which plays {@link MediaItem2}s. The details on playback control and player states
@@ -77,7 +77,7 @@
  * <a name="AudioFocusAndNoisyIntent"></a>
  * <h3>Audio focus and noisy intent</h3>
  * <p>
- * By default, {@link XMediaPlayer} handles audio focus and noisy intent with
+ * By default, {@link MediaPlayer} handles audio focus and noisy intent with
  * {@link AudioAttributesCompat} set to this player. You need to call
  * {@link #setAudioAttributes(AudioAttributesCompat)} set the audio attribute while in the
  * {@link #PLAYER_STATE_IDLE}.
@@ -129,8 +129,8 @@
  * <p>
  */
 @TargetApi(Build.VERSION_CODES.P)
-public class XMediaPlayer extends SessionPlayer2 {
-    private static final String TAG = "XMediaPlayer";
+public class MediaPlayer extends SessionPlayer2 {
+    private static final String TAG = "MediaPlayer";
 
     /**
      * Unspecified player error.
@@ -230,11 +230,11 @@
     public static final int MEDIA_INFO_MEDIA_ITEM_REPEAT = 7;
 
     /**
-     * The player just finished prefetching a media item for playback.
+     * The player just finished preparing a media item for playback.
      * @see #prepare()
      * @see PlayerCallback#onInfo
      */
-    public static final int MEDIA_INFO_PREFETCHED = 100;
+    public static final int MEDIA_INFO_PREPARED = 100;
 
     /**
      * The video is too complex for the decoder: it can't decode frames fast
@@ -360,7 +360,7 @@
             MEDIA_INFO_MEDIA_ITEM_END,
             MEDIA_INFO_MEDIA_ITEM_LIST_END,
             MEDIA_INFO_MEDIA_ITEM_REPEAT,
-            MEDIA_INFO_PREFETCHED,
+            MEDIA_INFO_PREPARED,
             MEDIA_INFO_VIDEO_TRACK_LAGGING,
             MEDIA_INFO_BUFFERING_START,
             MEDIA_INFO_BUFFERING_END,
@@ -500,26 +500,110 @@
     MediaPlayer2 mPlayer;
     private ExecutorService mExecutor;
 
-    /* A list for tracking the commands submitted to MediaPlayer2.*/
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    final ArrayDeque<PendingCommand> mPendingCommands = new ArrayDeque<>();
-    @GuardedBy("mPendingCommands")
-    boolean mPreviewEnabled;
-
     @SuppressWarnings("WeakerAccess") /* synthetic access */
     static final class PendingCommand {
         @SuppressWarnings("WeakerAccess") /* synthetic access */
-        final @MediaPlayer2.CallCompleted int mCallType;
+        @MediaPlayer2.CallCompleted final int mCallType;
         @SuppressWarnings("WeakerAccess") /* synthetic access */
         final ResolvableFuture mFuture;
 
         @SuppressWarnings("WeakerAccess") /* synthetic access */
-        PendingCommand(int mCallType, ResolvableFuture mFuture) {
-            this.mCallType = mCallType;
-            this.mFuture = mFuture;
+        PendingCommand(int callType, ResolvableFuture future) {
+            mCallType = callType;
+            mFuture = future;
         }
     }
 
+    /* A list for tracking the commands submitted to MediaPlayer2.*/
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    @GuardedBy("mPendingCommands")
+    final ArrayDeque<PendingCommand> mPendingCommands = new ArrayDeque<>();
+
+    /**
+     * PendingFuture is a future for the result of execution which will be executed later via
+     * the onExecute() method.
+     */
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    abstract static class PendingFuture<V extends PlayerResult>
+            extends AbstractResolvableFuture<V> {
+        @SuppressWarnings("WeakerAccess") /* synthetic access */
+        final boolean mIsSeekTo;
+        @SuppressWarnings("WeakerAccess") /* synthetic access */
+        boolean mExecuted = false;
+        @SuppressWarnings("WeakerAccess") /* synthetic access */
+        List<ResolvableFuture<V>> mFutures;
+
+        PendingFuture() {
+            mIsSeekTo = false;
+        }
+
+        PendingFuture(boolean isSeekTo) {
+            mIsSeekTo = isSeekTo;
+        }
+
+        @Override
+        public boolean set(@Nullable V value) {
+            return super.set(value);
+        }
+
+        @Override
+        public boolean setException(Throwable throwable) {
+            return super.setException(throwable);
+        }
+
+        public void execute() {
+            if (!mExecuted && !isCancelled()) {
+                mExecuted = true;
+                mFutures = onExecute();
+                setResultIfFinished();
+            }
+        }
+
+        public boolean setResultIfFinished() {
+            V result = null;
+            for (int i = 0; i < mFutures.size(); ++i) {
+                ResolvableFuture<V> future = mFutures.get(i);
+                if (!future.isDone() && !future.isCancelled()) {
+                    return false;
+                }
+                try {
+                    result = future.get();
+                    int resultCode = result.getResultCode();
+                    if (resultCode != RESULT_CODE_SUCCESS && resultCode != RESULT_CODE_SKIPPED) {
+                        cancelFutures();
+                        set(result);
+                        return true;
+                    }
+                } catch (Exception e) {
+                    cancelFutures();
+                    setException(e);
+                    return true;
+                }
+            }
+            try {
+                set(result);
+            } catch (Exception e) {
+                setException(e);
+            }
+            return true;
+        }
+
+        abstract List<ResolvableFuture<V>> onExecute();
+
+        private void cancelFutures() {
+            for (ResolvableFuture<V> future : mFutures) {
+                if (!future.isCancelled() && !future.isDone()) {
+                    future.cancel(true);
+                }
+            }
+        }
+    }
+
+    /* A list of pending operations within this MediaPlayer that will be executed sequentially. */
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    @GuardedBy("mPendingFutures")
+    final ArrayDeque<PendingFuture<? super PlayerResult>> mPendingFutures = new ArrayDeque<>();
+
     private final Object mStateLock = new Object();
     @GuardedBy("mStateLock")
     private @PlayerState int mState;
@@ -528,27 +612,36 @@
     @SuppressWarnings("WeakerAccess") /* synthetic access */
     final AudioFocusHandler mAudioFocusHandler;
 
-    private final Object mPlaylistLock = new Object();
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    final Object mPlaylistLock = new Object();
     @GuardedBy("mPlaylistLock")
-    private ArrayList<MediaItem2> mPlaylist = new ArrayList<>();
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    ArrayList<MediaItem2> mPlaylist = new ArrayList<>();
     @GuardedBy("mPlaylistLock")
-    private ArrayList<MediaItem2> mShuffledList = new ArrayList<>();
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    ArrayList<MediaItem2> mShuffledList = new ArrayList<>();
     @GuardedBy("mPlaylistLock")
-    private MediaMetadata2 mPlaylistMetadata;
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    MediaMetadata2 mPlaylistMetadata;
     @GuardedBy("mPlaylistLock")
-    private int mRepeatMode;
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    int mRepeatMode;
     @GuardedBy("mPlaylistLock")
-    private int mShuffleMode;
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    int mShuffleMode;
     @GuardedBy("mPlaylistLock")
-    private int mCurrentShuffleIdx;
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    int mCurrentShuffleIdx;
     @GuardedBy("mPlaylistLock")
-    private MediaItem2 mCurPlaylistItem;
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    MediaItem2 mCurPlaylistItem;
     @GuardedBy("mPlaylistLock")
-    private MediaItem2 mNextPlaylistItem;
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    MediaItem2 mNextPlaylistItem;
     @GuardedBy("mPlaylistLock")
     private boolean mSetMediaItemCalled;
 
-    public XMediaPlayer(Context context) {
+    public MediaPlayer(@NonNull Context context) {
         mState = PLAYER_STATE_IDLE;
         mPlayer = MediaPlayer2.create(context);
         mExecutor = Executors.newFixedThreadPool(1);
@@ -559,7 +652,8 @@
     }
 
     @GuardedBy("mPendingCommands")
-    private void addPendingCommandLocked(
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    void addPendingCommandLocked(
             int callType, final ResolvableFuture future, final Object token) {
         final PendingCommand pendingCommand = new PendingCommand(callType, future);
         mPendingCommands.add(pendingCommand);
@@ -578,31 +672,76 @@
         }, mExecutor);
     }
 
-    @Override
-    public ListenableFuture<PlayerResult> play() {
-        // TODO: Make commands be executed sequentially
-        if (mAudioFocusHandler.onPlayRequested()) {
-            final ResolvableFuture<PlayerResult> future = ResolvableFuture.create();
-            synchronized (mPendingCommands) {
-                Object token = mPlayer.play();
-                addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_PLAY, future, token);
-            }
-            return future;
-        } else {
-            return createFutureForResultCodeInternal(RESULT_CODE_UNKNOWN_ERROR);
+    private void addPendingFuture(final PendingFuture pendingFuture) {
+        synchronized (mPendingFutures) {
+            pendingFuture.addListener(new Runnable() {
+                @Override
+                public void run() {
+                    if (pendingFuture.isCancelled() && pendingFuture.mExecuted) {
+                        for (Object f : pendingFuture.mFutures) {
+                            ResolvableFuture future = (ResolvableFuture) f;
+                            if (!future.isDone() && !future.isCancelled()) {
+                                future.cancel(true);
+                            }
+                        }
+                        pendingFuture.mFutures.clear();
+                    }
+                }
+            }, mExecutor);
+            mPendingFutures.add(pendingFuture);
+            executePendingFuturesIfNeeded();
         }
     }
 
     @Override
+    @NonNull
+    public ListenableFuture<PlayerResult> play() {
+        PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>() {
+            @Override
+            List<ResolvableFuture<PlayerResult>> onExecute() {
+                ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>();
+                // TODO: Make commands be executed sequentially
+                final ResolvableFuture<PlayerResult> future;
+                if (mAudioFocusHandler.onPlay()) {
+                    if (mPlayer.getAudioAttributes() == null) {
+                        futures.add(setPlayerVolumeInternal(0f));
+                    }
+                    future = ResolvableFuture.create();
+                    synchronized (mPendingCommands) {
+                        Object token = mPlayer.play();
+                        addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_PLAY, future, token);
+                    }
+                } else {
+                    future = createFutureForResultCode(RESULT_CODE_UNKNOWN_ERROR);
+                }
+                futures.add(future);
+                return futures;
+            }
+        };
+        addPendingFuture(pendingFuture);
+        return pendingFuture;
+    }
+
+    @Override
+    @NonNull
     public ListenableFuture<PlayerResult> pause() {
-        ResolvableFuture<PlayerResult> future = ResolvableFuture.create();
-        // TODO: Make commands be executed sequentially
-        mAudioFocusHandler.onPauseRequested();
-        synchronized (mPendingCommands) {
-            Object token = mPlayer.pause();
-            addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_PAUSE, future, token);
-        }
-        return future;
+        PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>() {
+            @Override
+            List<ResolvableFuture<PlayerResult>> onExecute() {
+                ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>();
+                ResolvableFuture<PlayerResult> future = ResolvableFuture.create();
+                // TODO: Make commands be executed sequentially
+                mAudioFocusHandler.onPause();
+                synchronized (mPendingCommands) {
+                    Object token = mPlayer.pause();
+                    addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_PAUSE, future, token);
+                }
+                futures.add(future);
+                return futures;
+            }
+        };
+        addPendingFuture(pendingFuture);
+        return pendingFuture;
     }
 
     /**
@@ -612,59 +751,92 @@
      * {@link PlayerResult} will be delivered when the command completes.
      */
     @Override
+    @NonNull
     public ListenableFuture<PlayerResult> prepare() {
-        ListenableFuture ret;
-        synchronized (mPendingCommands) {
-            ResolvableFuture<PlayerResult> future = ResolvableFuture.create();
-            Object token = mPlayer.prepare();
-            addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_PREPARE, future, token);
-            if (mPreviewEnabled) {
-                ResolvableFuture<PlayerResult> future2 = ResolvableFuture.create();
-                Object token2 = mPlayer.pause();
-                addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_PAUSE, future2, token2);
-                ret = CombindedCommandResultFuture.create(mExecutor, future, future2);
-            } else {
-                ret = future;
+        PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>() {
+            @Override
+            List<ResolvableFuture<PlayerResult>> onExecute() {
+                ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>();
+                ResolvableFuture<PlayerResult> future = ResolvableFuture.create();
+                synchronized (mPendingCommands) {
+                    Object token = mPlayer.prepare();
+                    addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_PREPARE, future, token);
+                }
+                // TODO: Changing buffering state is not correct. Think about changing MP2 event
+                // APIs for the initial buffering for prepare case.
+                setBufferingState(mPlayer.getCurrentMediaItem(),
+                        BUFFERING_STATE_BUFFERING_AND_STARVED);
+                futures.add(future);
+                return futures;
             }
-        }
-        // TODO: Changing buffering state is not correct. Think about changing MP2 event APIs for
-        // the initial buffering for prepare case.
-        setBufferingState(mPlayer.getCurrentMediaItem(), BUFFERING_STATE_BUFFERING_AND_STARVED);
-        return ret;
+        };
+        addPendingFuture(pendingFuture);
+        return pendingFuture;
     }
 
     @Override
-    public ListenableFuture<PlayerResult> seekTo(long position) {
-        ResolvableFuture<PlayerResult> future = ResolvableFuture.create();
-        synchronized (mPendingCommands) {
-            Object token = mPlayer.seekTo(position);
-            addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_SEEK_TO, future, token);
-        }
-        return future;
+    @NonNull
+    public ListenableFuture<PlayerResult> seekTo(final long position) {
+        PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>(true) {
+            @Override
+            List<ResolvableFuture<PlayerResult>> onExecute() {
+                ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>();
+                ResolvableFuture<PlayerResult> future = ResolvableFuture.create();
+                synchronized (mPendingCommands) {
+                    Object token = mPlayer.seekTo(position);
+                    addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_SEEK_TO, future, token);
+                }
+                futures.add(future);
+                return futures;
+            }
+        };
+        addPendingFuture(pendingFuture);
+        return pendingFuture;
     }
 
     @Override
-    public ListenableFuture<PlayerResult> setPlaybackSpeed(float playbackSpeed) {
-        ResolvableFuture<PlayerResult> future = ResolvableFuture.create();
-        synchronized (mPendingCommands) {
-            Object token = mPlayer.setPlaybackParams(new PlaybackParams2.Builder(
-                    mPlayer.getPlaybackParams().getPlaybackParams())
-                    .setSpeed(playbackSpeed).build());
-            addPendingCommandLocked(
-                    MediaPlayer2.CALL_COMPLETED_SET_PLAYBACK_PARAMS, future, token);
-        }
-        return future;
+    @NonNull
+    public ListenableFuture<PlayerResult> setPlaybackSpeed(final float playbackSpeed) {
+        PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>() {
+            @Override
+            List<ResolvableFuture<PlayerResult>> onExecute() {
+                ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>();
+                ResolvableFuture<PlayerResult> future = ResolvableFuture.create();
+                synchronized (mPendingCommands) {
+                    Object token = mPlayer.setPlaybackParams(new PlaybackParams2.Builder(
+                            mPlayer.getPlaybackParams().getPlaybackParams())
+                            .setSpeed(playbackSpeed).build());
+                    addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_SET_PLAYBACK_PARAMS,
+                            future, token);
+                }
+                futures.add(future);
+                return futures;
+            }
+        };
+        addPendingFuture(pendingFuture);
+        return pendingFuture;
     }
 
+    @NonNull
     @Override
-    public ListenableFuture<PlayerResult> setAudioAttributes(AudioAttributesCompat attr) {
-        ResolvableFuture<PlayerResult> future = ResolvableFuture.create();
-        synchronized (mPendingCommands) {
-            Object token = mPlayer.setAudioAttributes(attr);
-            addPendingCommandLocked(
-                    MediaPlayer2.CALL_COMPLETED_SET_AUDIO_ATTRIBUTES, future, token);
-        }
-        return future;
+    public ListenableFuture<PlayerResult> setAudioAttributes(
+            @NonNull final AudioAttributesCompat attr) {
+        PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>() {
+            @Override
+            List<ResolvableFuture<PlayerResult>> onExecute() {
+                ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>();
+                ResolvableFuture<PlayerResult> future = ResolvableFuture.create();
+                synchronized (mPendingCommands) {
+                    Object token = mPlayer.setAudioAttributes(attr);
+                    addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_SET_AUDIO_ATTRIBUTES,
+                            future, token);
+                }
+                futures.add(future);
+                return futures;
+            }
+        };
+        addPendingFuture(pendingFuture);
+        return pendingFuture;
     }
 
     @Override
@@ -720,6 +892,7 @@
     }
 
     @Override
+    @Nullable
     public AudioAttributesCompat getAudioAttributes() {
         try {
             return mPlayer.getAudioAttributes();
@@ -729,54 +902,80 @@
     }
 
     @Override
-    public ListenableFuture<PlayerResult> setMediaItem(@NonNull MediaItem2 item) {
+    @NonNull
+    public ListenableFuture<PlayerResult> setMediaItem(@NonNull final MediaItem2 item) {
         if (item == null) {
             throw new IllegalArgumentException("item shouldn't be null");
         }
-        synchronized (mPlaylistLock) {
-            mPlaylist.clear();
-            mShuffledList.clear();
-            mCurPlaylistItem = item;
-            mNextPlaylistItem = null;
-            mCurrentShuffleIdx = END_OF_PLAYLIST;
-        }
-        return setPlayerMediaItemsInternal(item, null);
+        PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>() {
+            @Override
+            List<ResolvableFuture<PlayerResult>> onExecute() {
+                ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>();
+                synchronized (mPlaylistLock) {
+                    mPlaylist.clear();
+                    mShuffledList.clear();
+                    mCurPlaylistItem = item;
+                    mNextPlaylistItem = null;
+                    mCurrentShuffleIdx = END_OF_PLAYLIST;
+                }
+                futures.addAll(setMediaItemsInternal(item, null));
+                return futures;
+            }
+        };
+        addPendingFuture(pendingFuture);
+        return pendingFuture;
     }
 
+    @NonNull
     @Override
     public ListenableFuture<PlayerResult> setPlaylist(
             @NonNull final List<MediaItem2> playlist, @Nullable final MediaMetadata2 metadata) {
         if (playlist == null || playlist.isEmpty()) {
             throw new IllegalArgumentException("playlist shouldn't be null or empty");
         }
-        MediaItem2 curItem;
-        MediaItem2 nextItem;
-        synchronized (mPlaylistLock) {
-            mPlaylistMetadata = metadata;
-            mPlaylist.clear();
-            mShuffledList.clear();
-            mPlaylist.addAll(playlist);
-            applyShuffleModeLocked();
-            mCurrentShuffleIdx = 0;
-            updateAndGetCurrentNextItemIfNeededLocked();
-            curItem = mCurPlaylistItem;
-            nextItem = mNextPlaylistItem;
-        }
-        notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() {
-            @Override
-            public void callCallback(
-                    SessionPlayer2.PlayerCallback callback) {
-                callback.onPlaylistChanged(XMediaPlayer.this, playlist, metadata);
+        for (MediaItem2 item : playlist) {
+            if (item == null) {
+                throw new IllegalArgumentException("playlist shouldn't contain null item");
             }
-        });
-        if (curItem != null) {
-            return setPlayerMediaItemsInternal(curItem, nextItem);
         }
-        return createFutureForResultCodeInternal(RESULT_CODE_SUCCESS);
+
+        PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>() {
+            @Override
+            List<ResolvableFuture<PlayerResult>> onExecute() {
+                MediaItem2 curItem;
+                MediaItem2 nextItem;
+                synchronized (mPlaylistLock) {
+                    mPlaylistMetadata = metadata;
+                    mPlaylist.clear();
+                    mShuffledList.clear();
+                    mPlaylist.addAll(playlist);
+                    applyShuffleModeLocked();
+                    mCurrentShuffleIdx = 0;
+                    updateAndGetCurrentNextItemIfNeededLocked();
+                    curItem = mCurPlaylistItem;
+                    nextItem = mNextPlaylistItem;
+                }
+                notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() {
+                    @Override
+                    public void callCallback(
+                            SessionPlayer2.PlayerCallback callback) {
+                        callback.onPlaylistChanged(MediaPlayer.this, playlist, metadata);
+                    }
+                });
+                if (curItem != null) {
+                    return setMediaItemsInternal(curItem, nextItem);
+                }
+                return createFuturesForResultCode(RESULT_CODE_SUCCESS);
+            }
+        };
+        addPendingFuture(pendingFuture);
+        return pendingFuture;
     }
 
+    @NonNull
     @Override
-    public ListenableFuture<PlayerResult> addPlaylistItem(int index, MediaItem2 item) {
+    public ListenableFuture<PlayerResult> addPlaylistItem(
+            final int index, @NonNull final MediaItem2 item) {
         if (item == null) {
             throw new IllegalArgumentException("item shouldn't be null");
         }
@@ -788,85 +987,109 @@
                 throw new IllegalStateException("The item is already in the playlist: " + item);
             }
         }
-        Pair<MediaItem2, MediaItem2> updatedCurNextItem;
-        synchronized (mPlaylistLock) {
-            index = clamp(index, mPlaylist.size());
-            int addedShuffleIdx = index;
-            mPlaylist.add(index, item);
-            if (mShuffleMode == SessionPlayer2.SHUFFLE_MODE_NONE) {
-                mShuffledList.add(index, item);
-            } else {
-                // Add the item in random position of mShuffledList.
-                addedShuffleIdx = (int) (Math.random() * (mShuffledList.size() + 1));
-                mShuffledList.add(addedShuffleIdx, item);
-            }
-            if (addedShuffleIdx <= mCurrentShuffleIdx) {
-                mCurrentShuffleIdx++;
-            }
-            updatedCurNextItem = updateAndGetCurrentNextItemIfNeededLocked();
-        }
-        final List<MediaItem2> playlist = getPlaylist();
-        final MediaMetadata2 metadata = getPlaylistMetadata();
-        notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() {
-            @Override
-            public void callCallback(
-                    SessionPlayer2.PlayerCallback callback) {
-                callback.onPlaylistChanged(XMediaPlayer.this, playlist, metadata);
-            }
-        });
 
-        if (updatedCurNextItem.second == null) {
-            return createFutureForResultCodeInternal(RESULT_CODE_SUCCESS);
-        }
-        return setNextMediaItemInternal(updatedCurNextItem.second);
+        PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>() {
+            @Override
+            List<ResolvableFuture<PlayerResult>> onExecute() {
+                Pair<MediaItem2, MediaItem2> updatedCurNextItem;
+                synchronized (mPlaylistLock) {
+                    int clampedIndex = clamp(index, mPlaylist.size());
+                    int addedShuffleIdx = clampedIndex;
+                    mPlaylist.add(clampedIndex, item);
+                    if (mShuffleMode == SessionPlayer2.SHUFFLE_MODE_NONE) {
+                        mShuffledList.add(clampedIndex, item);
+                    } else {
+                        // Add the item in random position of mShuffledList.
+                        addedShuffleIdx = (int) (Math.random() * (mShuffledList.size() + 1));
+                        mShuffledList.add(addedShuffleIdx, item);
+                    }
+                    if (addedShuffleIdx <= mCurrentShuffleIdx) {
+                        mCurrentShuffleIdx++;
+                    }
+                    updatedCurNextItem = updateAndGetCurrentNextItemIfNeededLocked();
+                }
+                final List<MediaItem2> playlist = getPlaylist();
+                final MediaMetadata2 metadata = getPlaylistMetadata();
+                notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() {
+                    @Override
+                    public void callCallback(
+                            SessionPlayer2.PlayerCallback callback) {
+                        callback.onPlaylistChanged(MediaPlayer.this, playlist, metadata);
+                    }
+                });
+
+                if (updatedCurNextItem.second == null) {
+                    return createFuturesForResultCode(RESULT_CODE_SUCCESS);
+                }
+                ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>();
+                futures.add(setNextMediaItemInternal(updatedCurNextItem.second));
+                return futures;
+            }
+        };
+        addPendingFuture(pendingFuture);
+        return pendingFuture;
     }
 
     @Override
-    public ListenableFuture<PlayerResult> removePlaylistItem(MediaItem2 item) {
+    @NonNull
+    public ListenableFuture<PlayerResult> removePlaylistItem(@NonNull final MediaItem2 item) {
         if (item == null) {
             throw new IllegalArgumentException("item shouldn't be null");
         }
-        int removedItemShuffleIdx;
-        MediaItem2 curItem;
-        MediaItem2 nextItem;
-        Pair<MediaItem2, MediaItem2> updatedCurNextItem = null;
-        synchronized (mPlaylistLock) {
-            removedItemShuffleIdx = mShuffledList.indexOf(item);
-            if (removedItemShuffleIdx >= 0) {
-                mPlaylist.remove(item);
-                mShuffledList.remove(removedItemShuffleIdx);
-                if (removedItemShuffleIdx < mCurrentShuffleIdx) {
-                    mCurrentShuffleIdx--;
-                }
-                updatedCurNextItem = updateAndGetCurrentNextItemIfNeededLocked();
-            }
-            curItem = mCurPlaylistItem;
-            nextItem = mNextPlaylistItem;
-        }
-        if (removedItemShuffleIdx >= 0) {
-            final List<MediaItem2> playlist = getPlaylist();
-            final MediaMetadata2 metadata = getPlaylistMetadata();
-            notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() {
-                @Override
-                public void callCallback(
-                        SessionPlayer2.PlayerCallback callback) {
-                    callback.onPlaylistChanged(XMediaPlayer.this, playlist, metadata);
-                }
-            });
-        }
 
-        if (updatedCurNextItem != null) {
-            if (updatedCurNextItem.first != null) {
-                return setPlayerMediaItemsInternal(curItem, nextItem);
-            } else if (updatedCurNextItem.second != null) {
-                return setNextMediaItemInternal(nextItem);
+        PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>() {
+            @Override
+            List<ResolvableFuture<PlayerResult>> onExecute() {
+                int removedItemShuffleIdx;
+                MediaItem2 curItem;
+                MediaItem2 nextItem;
+                Pair<MediaItem2, MediaItem2> updatedCurNextItem = null;
+                synchronized (mPlaylistLock) {
+                    removedItemShuffleIdx = mShuffledList.indexOf(item);
+                    if (removedItemShuffleIdx >= 0) {
+                        mPlaylist.remove(item);
+                        mShuffledList.remove(removedItemShuffleIdx);
+                        if (removedItemShuffleIdx < mCurrentShuffleIdx) {
+                            mCurrentShuffleIdx--;
+                        }
+                        updatedCurNextItem = updateAndGetCurrentNextItemIfNeededLocked();
+                    }
+                    curItem = mCurPlaylistItem;
+                    nextItem = mNextPlaylistItem;
+                }
+                if (removedItemShuffleIdx >= 0) {
+                    final List<MediaItem2> playlist = getPlaylist();
+                    final MediaMetadata2 metadata = getPlaylistMetadata();
+                    notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() {
+                        @Override
+                        public void callCallback(
+                                SessionPlayer2.PlayerCallback callback) {
+                            callback.onPlaylistChanged(MediaPlayer.this, playlist, metadata);
+                        }
+                    });
+                }
+
+                ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>();
+                if (updatedCurNextItem != null) {
+                    if (updatedCurNextItem.first != null) {
+                        futures.addAll(setMediaItemsInternal(curItem, nextItem));
+                    } else if (updatedCurNextItem.second != null) {
+                        futures.add(setNextMediaItemInternal(nextItem));
+                    }
+                } else {
+                    futures.add(createFutureForResultCode(RESULT_CODE_SUCCESS));
+                }
+                return futures;
             }
-        }
-        return createFutureForResultCodeInternal(RESULT_CODE_SUCCESS);
+        };
+        addPendingFuture(pendingFuture);
+        return pendingFuture;
     }
 
+    @NonNull
     @Override
-    public ListenableFuture<PlayerResult> replacePlaylistItem(int index, MediaItem2 item) {
+    public ListenableFuture<PlayerResult> replacePlaylistItem(
+            final int index, @NonNull final MediaItem2 item) {
         if (item == null) {
             throw new IllegalArgumentException("item shouldn't be null");
         }
@@ -879,167 +1102,230 @@
                 throw new IllegalStateException("The item is already in the playlist: " + item);
             }
         }
-        MediaItem2 curItem;
-        MediaItem2 nextItem;
-        Pair<MediaItem2, MediaItem2> updatedCurNextItem = null;
-        synchronized (mPlaylistLock) {
-            if (index >= mPlaylist.size()) {
-                return createFutureForResultCodeInternal(RESULT_CODE_BAD_VALUE);
-            }
-            int shuffleIdx = mShuffledList.indexOf(mPlaylist.get(index));
-            mShuffledList.set(shuffleIdx, item);
-            mPlaylist.set(index, item);
-            updatedCurNextItem = updateAndGetCurrentNextItemIfNeededLocked();
-            curItem = mCurPlaylistItem;
-            nextItem = mNextPlaylistItem;
-        }
-        // TODO: Should we notify current media item changed if it is replaced?
-        final List<MediaItem2> playlist = getPlaylist();
-        final MediaMetadata2 metadata = getPlaylistMetadata();
-        notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() {
+
+        PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>() {
             @Override
-            public void callCallback(
-                    SessionPlayer2.PlayerCallback callback) {
-                callback.onPlaylistChanged(XMediaPlayer.this, playlist, metadata);
-            }
-        });
+            List<ResolvableFuture<PlayerResult>> onExecute() {
+                MediaItem2 curItem;
+                MediaItem2 nextItem;
+                Pair<MediaItem2, MediaItem2> updatedCurNextItem = null;
+                synchronized (mPlaylistLock) {
+                    if (index >= mPlaylist.size()) {
+                        return createFuturesForResultCode(RESULT_CODE_BAD_VALUE);
+                    }
+                    int shuffleIdx = mShuffledList.indexOf(mPlaylist.get(index));
+                    mShuffledList.set(shuffleIdx, item);
+                    mPlaylist.set(index, item);
+                    updatedCurNextItem = updateAndGetCurrentNextItemIfNeededLocked();
+                    curItem = mCurPlaylistItem;
+                    nextItem = mNextPlaylistItem;
+                }
+                // TODO: Should we notify current media item changed if it is replaced?
+                final List<MediaItem2> playlist = getPlaylist();
+                final MediaMetadata2 metadata = getPlaylistMetadata();
+                notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() {
+                    @Override
+                    public void callCallback(
+                            SessionPlayer2.PlayerCallback callback) {
+                        callback.onPlaylistChanged(MediaPlayer.this, playlist, metadata);
+                    }
+                });
 
-        if (updatedCurNextItem != null) {
-            if (updatedCurNextItem.first != null) {
-                return setPlayerMediaItemsInternal(curItem, nextItem);
-            } else if (updatedCurNextItem.second != null) {
-                return setNextMediaItemInternal(nextItem);
+                ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>();
+                if (updatedCurNextItem != null) {
+                    if (updatedCurNextItem.first != null) {
+                        futures.addAll(setMediaItemsInternal(curItem, nextItem));
+                    } else if (updatedCurNextItem.second != null) {
+                        futures.add(setNextMediaItemInternal(nextItem));
+                    }
+                } else {
+                    futures.add(createFutureForResultCode(RESULT_CODE_SUCCESS));
+                }
+                return futures;
             }
-        }
-
-        return createFutureForResultCodeInternal(RESULT_CODE_SUCCESS);
+        };
+        addPendingFuture(pendingFuture);
+        return pendingFuture;
     }
 
     @Override
+    @NonNull
     public ListenableFuture<PlayerResult> skipToPreviousPlaylistItem() {
-        MediaItem2 curItem;
-        MediaItem2 nextItem;
-        synchronized (mPlaylistLock) {
-            int prevShuffleIdx = mCurrentShuffleIdx - 1;
-            if (prevShuffleIdx < 0) {
-                if (mRepeatMode == REPEAT_MODE_ALL || mRepeatMode == REPEAT_MODE_GROUP) {
-                    prevShuffleIdx = mShuffledList.size() - 1;
-                } else {
-                    return createFutureForResultCodeInternal(RESULT_CODE_INVALID_STATE);
-                }
-            }
-            mCurrentShuffleIdx = prevShuffleIdx;
-            updateAndGetCurrentNextItemIfNeededLocked();
-            curItem = mCurPlaylistItem;
-            nextItem = mNextPlaylistItem;
-        }
-        return setPlayerMediaItemsInternal(curItem, nextItem);
-    }
-
-    @Override
-    public ListenableFuture<PlayerResult> skipToNextPlaylistItem() {
-        MediaItem2 curItem;
-        MediaItem2 nextItem;
-        synchronized (mPlaylistLock) {
-            int nextShuffleIdx = mCurrentShuffleIdx + 1;
-            if (nextShuffleIdx >= mShuffledList.size()) {
-                if (mRepeatMode == REPEAT_MODE_ALL || mRepeatMode == REPEAT_MODE_GROUP) {
-                    nextShuffleIdx = 0;
-                } else {
-                    return createFutureForResultCodeInternal(RESULT_CODE_INVALID_STATE);
-                }
-            }
-            mCurrentShuffleIdx = nextShuffleIdx;
-            updateAndGetCurrentNextItemIfNeededLocked();
-            curItem = mCurPlaylistItem;
-            nextItem = mNextPlaylistItem;
-        }
-        return curItem != null ? setPlayerMediaItemsInternal(curItem, nextItem)
-                : skipToNextInternal();
-    }
-
-    @Override
-    public ListenableFuture<PlayerResult> skipToPlaylistItem(MediaItem2 item) {
-        MediaItem2 curItem;
-        MediaItem2 nextItem;
-        synchronized (mPlaylistLock) {
-            int newShuffleIdx = mShuffledList.indexOf(item);
-            if (newShuffleIdx < 0) {
-                return createFutureForResultCodeInternal(RESULT_CODE_BAD_VALUE);
-            }
-            mCurrentShuffleIdx = newShuffleIdx;
-            updateAndGetCurrentNextItemIfNeededLocked();
-            curItem = mCurPlaylistItem;
-            nextItem = mNextPlaylistItem;
-        }
-        return setPlayerMediaItemsInternal(curItem, nextItem);
-    }
-
-    @Override
-    public ListenableFuture<PlayerResult> updatePlaylistMetadata(final MediaMetadata2 metadata) {
-        synchronized (mPlaylistLock) {
-            mPlaylistMetadata = metadata;
-        }
-        notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() {
+        PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>() {
             @Override
-            public void callCallback(
-                    SessionPlayer2.PlayerCallback callback) {
-                callback.onPlaylistMetadataChanged(XMediaPlayer.this, metadata);
+            List<ResolvableFuture<PlayerResult>> onExecute() {
+                MediaItem2 curItem;
+                MediaItem2 nextItem;
+                synchronized (mPlaylistLock) {
+                    int prevShuffleIdx = mCurrentShuffleIdx - 1;
+                    if (prevShuffleIdx < 0) {
+                        if (mRepeatMode == REPEAT_MODE_ALL || mRepeatMode == REPEAT_MODE_GROUP) {
+                            prevShuffleIdx = mShuffledList.size() - 1;
+                        } else {
+                            return createFuturesForResultCode(RESULT_CODE_INVALID_STATE);
+                        }
+                    }
+                    mCurrentShuffleIdx = prevShuffleIdx;
+                    updateAndGetCurrentNextItemIfNeededLocked();
+                    curItem = mCurPlaylistItem;
+                    nextItem = mNextPlaylistItem;
+                }
+                return setMediaItemsInternal(curItem, nextItem);
             }
-        });
-
-        return createFutureForResultCodeInternal(RESULT_CODE_SUCCESS);
+        };
+        addPendingFuture(pendingFuture);
+        return pendingFuture;
     }
 
     @Override
+    @NonNull
+    public ListenableFuture<PlayerResult> skipToNextPlaylistItem() {
+        PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>() {
+            @Override
+            List<ResolvableFuture<PlayerResult>> onExecute() {
+                MediaItem2 curItem;
+                MediaItem2 nextItem;
+                synchronized (mPlaylistLock) {
+                    int nextShuffleIdx = mCurrentShuffleIdx + 1;
+                    if (nextShuffleIdx >= mShuffledList.size()) {
+                        if (mRepeatMode == REPEAT_MODE_ALL || mRepeatMode == REPEAT_MODE_GROUP) {
+                            nextShuffleIdx = 0;
+                        } else {
+                            return createFuturesForResultCode(RESULT_CODE_INVALID_STATE);
+                        }
+                    }
+                    mCurrentShuffleIdx = nextShuffleIdx;
+                    updateAndGetCurrentNextItemIfNeededLocked();
+                    curItem = mCurPlaylistItem;
+                    nextItem = mNextPlaylistItem;
+                }
+                if (curItem != null) {
+                    return setMediaItemsInternal(curItem, nextItem);
+                }
+                ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>();
+                futures.add(skipToNextInternal());
+                return futures;
+            }
+        };
+        addPendingFuture(pendingFuture);
+        return pendingFuture;
+    }
+
+    @Override
+    @NonNull
+    public ListenableFuture<PlayerResult> skipToPlaylistItem(@NonNull final MediaItem2 item) {
+        PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>() {
+            @Override
+            List<ResolvableFuture<PlayerResult>> onExecute() {
+                MediaItem2 curItem;
+                MediaItem2 nextItem;
+                synchronized (mPlaylistLock) {
+                    int newShuffleIdx = mShuffledList.indexOf(item);
+                    if (newShuffleIdx < 0) {
+                        return createFuturesForResultCode(RESULT_CODE_BAD_VALUE);
+                    }
+                    mCurrentShuffleIdx = newShuffleIdx;
+                    updateAndGetCurrentNextItemIfNeededLocked();
+                    curItem = mCurPlaylistItem;
+                    nextItem = mNextPlaylistItem;
+                }
+                return setMediaItemsInternal(curItem, nextItem);
+            }
+        };
+        addPendingFuture(pendingFuture);
+        return pendingFuture;
+    }
+
+    @NonNull
+    @Override
+    public ListenableFuture<PlayerResult> updatePlaylistMetadata(
+            @Nullable final MediaMetadata2 metadata) {
+        PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>() {
+            @Override
+            List<ResolvableFuture<PlayerResult>> onExecute() {
+                synchronized (mPlaylistLock) {
+                    mPlaylistMetadata = metadata;
+                }
+                notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() {
+                    @Override
+                    public void callCallback(
+                            SessionPlayer2.PlayerCallback callback) {
+                        callback.onPlaylistMetadataChanged(MediaPlayer.this, metadata);
+                    }
+                });
+                return createFuturesForResultCode(RESULT_CODE_SUCCESS);
+            }
+        };
+        addPendingFuture(pendingFuture);
+        return pendingFuture;
+    }
+
+    @Override
+    @NonNull
     public ListenableFuture<PlayerResult> setRepeatMode(final int repeatMode) {
-        if (repeatMode < SessionPlayer2.REPEAT_MODE_NONE
-                || repeatMode > SessionPlayer2.REPEAT_MODE_GROUP) {
-            return createFutureForResultCodeInternal(RESULT_CODE_BAD_VALUE);
-        }
-
-        boolean changed;
-        synchronized (mPlaylistLock) {
-            changed = mRepeatMode != repeatMode;
-            mRepeatMode = repeatMode;
-        }
-        if (changed) {
-            notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() {
-                @Override
-                public void callCallback(
-                        SessionPlayer2.PlayerCallback callback) {
-                    callback.onRepeatModeChanged(XMediaPlayer.this, repeatMode);
+        PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>() {
+            @Override
+            List<ResolvableFuture<PlayerResult>> onExecute() {
+                if (repeatMode < SessionPlayer2.REPEAT_MODE_NONE
+                        || repeatMode > SessionPlayer2.REPEAT_MODE_GROUP) {
+                    return createFuturesForResultCode(RESULT_CODE_BAD_VALUE);
                 }
-            });
-        }
-        return createFutureForResultCodeInternal(RESULT_CODE_SUCCESS);
+
+                boolean changed;
+                synchronized (mPlaylistLock) {
+                    changed = mRepeatMode != repeatMode;
+                    mRepeatMode = repeatMode;
+                }
+                if (changed) {
+                    notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() {
+                        @Override
+                        public void callCallback(
+                                SessionPlayer2.PlayerCallback callback) {
+                            callback.onRepeatModeChanged(MediaPlayer.this, repeatMode);
+                        }
+                    });
+                }
+                return createFuturesForResultCode(RESULT_CODE_SUCCESS);
+            }
+        };
+        addPendingFuture(pendingFuture);
+        return pendingFuture;
     }
 
     @Override
+    @NonNull
     public ListenableFuture<PlayerResult> setShuffleMode(final int shuffleMode) {
-        if (shuffleMode < SessionPlayer2.SHUFFLE_MODE_NONE
-                || shuffleMode > SessionPlayer2.SHUFFLE_MODE_GROUP) {
-            return createFutureForResultCodeInternal(RESULT_CODE_BAD_VALUE);
-        }
-
-        boolean changed;
-        synchronized (mPlaylistLock) {
-            changed = mShuffleMode != shuffleMode;
-            mShuffleMode = shuffleMode;
-        }
-        if (changed) {
-            notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() {
-                @Override
-                public void callCallback(
-                        SessionPlayer2.PlayerCallback callback) {
-                    callback.onShuffleModeChanged(XMediaPlayer.this, shuffleMode);
+        PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>() {
+            @Override
+            List<ResolvableFuture<PlayerResult>> onExecute() {
+                if (shuffleMode < SessionPlayer2.SHUFFLE_MODE_NONE
+                        || shuffleMode > SessionPlayer2.SHUFFLE_MODE_GROUP) {
+                    return createFuturesForResultCode(RESULT_CODE_BAD_VALUE);
                 }
-            });
-        }
-        return createFutureForResultCodeInternal(RESULT_CODE_SUCCESS);
+
+                boolean changed;
+                synchronized (mPlaylistLock) {
+                    changed = mShuffleMode != shuffleMode;
+                    mShuffleMode = shuffleMode;
+                }
+                if (changed) {
+                    notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() {
+                        @Override
+                        public void callCallback(
+                                SessionPlayer2.PlayerCallback callback) {
+                            callback.onShuffleModeChanged(MediaPlayer.this, shuffleMode);
+                        }
+                    });
+                }
+                return createFuturesForResultCode(RESULT_CODE_SUCCESS);
+            }
+        };
+        addPendingFuture(pendingFuture);
+        return pendingFuture;
     }
 
     @Override
+    @Nullable
     public List<MediaItem2> getPlaylist() {
         synchronized (mPlaylistLock) {
             return mPlaylist.isEmpty() ? null : new ArrayList<>(mPlaylist);
@@ -1047,6 +1333,7 @@
     }
 
     @Override
+    @Nullable
     public MediaMetadata2 getPlaylistMetadata() {
         synchronized (mPlaylistLock) {
             return mPlaylistMetadata;
@@ -1068,6 +1355,7 @@
     }
 
     @Override
+    @Nullable
     public MediaItem2 getCurrentMediaItem() {
         return mPlayer.getCurrentMediaItem();
     }
@@ -1089,19 +1377,26 @@
     }
 
     /**
-     * Resets {@link XMediaPlayer} to its uninitialized state. After calling
+     * Resets {@link MediaPlayer} to its uninitialized state. After calling
      * this method, you will have to initialize it again by setting the
      * media item and calling {@link #prepare()}.
      */
     public void reset() {
-        mPlayer.reset();
+        // Cancel the pending futures.
+        synchronized (mPendingFutures) {
+            for (PendingFuture f : mPendingFutures) {
+                if (f.mExecuted && !f.isDone() && !f.isCancelled()) {
+                    f.cancel(true);
+                }
+            }
+            mPendingFutures.clear();
+        }
+        // Cancel the pending commands.
         synchronized (mPendingCommands) {
-            // Cancel the pending futures.
             for (PendingCommand c : mPendingCommands) {
                 c.mFuture.cancel(true);
             }
             mPendingCommands.clear();
-            mPreviewEnabled = false;
         }
         synchronized (mStateLock) {
             mState = PLAYER_STATE_IDLE;
@@ -1115,6 +1410,8 @@
             mCurrentShuffleIdx = END_OF_PLAYLIST;
             mSetMediaItemCalled = false;
         }
+        mAudioFocusHandler.onReset();
+        mPlayer.reset();
     }
 
     /**
@@ -1136,13 +1433,23 @@
      * @return a {@link ListenableFuture} which represents the pending completion of the command.
      * {@link PlayerResult} will be delivered when the command completes.
      */
-    public ListenableFuture<PlayerResult> setSurface(Surface surface) {
-        ResolvableFuture<PlayerResult> future = ResolvableFuture.create();
-        synchronized (mPendingCommands) {
-            Object token = mPlayer.setSurface(surface);
-            addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_SET_SURFACE, future, token);
-        }
-        return future;
+    @NonNull
+    public ListenableFuture<PlayerResult> setSurface(@Nullable final Surface surface) {
+        PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>() {
+            @Override
+            List<ResolvableFuture<PlayerResult>> onExecute() {
+                ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>();
+                ResolvableFuture<PlayerResult> future = ResolvableFuture.create();
+                synchronized (mPendingCommands) {
+                    Object token = mPlayer.setSurface(surface);
+                    addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_SET_SURFACE, future, token);
+                }
+                futures.add(future);
+                return futures;
+            }
+        };
+        addPendingFuture(pendingFuture);
+        return pendingFuture;
     }
 
     /**
@@ -1157,14 +1464,18 @@
      * @return a {@link ListenableFuture} which represents the pending completion of the command.
      * {@link PlayerResult} will be delivered when the command completes.
      */
-    public ListenableFuture<PlayerResult> setPlayerVolume(float volume) {
-        ResolvableFuture<PlayerResult> future = ResolvableFuture.create();
-        synchronized (mPendingCommands) {
-            Object token = mPlayer.setPlayerVolume(volume);
-            addPendingCommandLocked(
-                    MediaPlayer2.CALL_COMPLETED_SET_PLAYER_VOLUME, future, token);
-        }
-        return future;
+    @NonNull
+    public ListenableFuture<PlayerResult> setPlayerVolume(final float volume) {
+        PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>() {
+            @Override
+            List<ResolvableFuture<PlayerResult>> onExecute() {
+                ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>();
+                futures.add(setPlayerVolumeInternal(volume));
+                return futures;
+            }
+        };
+        addPendingFuture(pendingFuture);
+        return pendingFuture;
     }
 
     /**
@@ -1229,14 +1540,24 @@
      * @return a {@link ListenableFuture} which represents the pending completion of the command.
      * {@link PlayerResult} will be delivered when the command completes.
      */
-    public ListenableFuture<PlayerResult> setPlaybackParams(@NonNull PlaybackParams2 params) {
-        ResolvableFuture<PlayerResult> future = ResolvableFuture.create();
-        synchronized (mPendingCommands) {
-            Object token = mPlayer.setPlaybackParams(params);
-            addPendingCommandLocked(
-                    MediaPlayer2.CALL_COMPLETED_SET_PLAYBACK_PARAMS, future, token);
-        }
-        return future;
+    @NonNull
+    public ListenableFuture<PlayerResult> setPlaybackParams(@NonNull final PlaybackParams2 params) {
+        PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>() {
+            @Override
+            List<ResolvableFuture<PlayerResult>> onExecute() {
+                ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>();
+                ResolvableFuture<PlayerResult> future = ResolvableFuture.create();
+                synchronized (mPendingCommands) {
+                    Object token = mPlayer.setPlaybackParams(params);
+                    addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_SET_PLAYBACK_PARAMS,
+                            future, token);
+                }
+                futures.add(future);
+                return futures;
+            }
+        };
+        addPendingFuture(pendingFuture);
+        return pendingFuture;
     }
 
     /**
@@ -1244,6 +1565,7 @@
      *
      * @return the playback params.
      */
+    @NonNull
     public PlaybackParams2 getPlaybackParams() {
         return mPlayer.getPlaybackParams();
     }
@@ -1265,14 +1587,24 @@
      * @return a {@link ListenableFuture} which represents the pending completion of the command.
      * {@link PlayerResult} will be delivered when the command completes.
      */
-    public ListenableFuture<PlayerResult> seekTo(long msec, @SeekMode int mode) {
-        ResolvableFuture<PlayerResult> future = ResolvableFuture.create();
-        int mp2SeekMode = sSeekModeMap.getOrDefault(mode, SEEK_NEXT_SYNC);
-        synchronized (mPendingCommands) {
-            Object token = mPlayer.seekTo(msec, mp2SeekMode);
-            addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_SEEK_TO, future, token);
-        }
-        return future;
+    @NonNull
+    public ListenableFuture<PlayerResult> seekTo(final long msec, @SeekMode final int mode) {
+        PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>(true) {
+            @Override
+            List<ResolvableFuture<PlayerResult>> onExecute() {
+                ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>();
+                ResolvableFuture<PlayerResult> future = ResolvableFuture.create();
+                int mp2SeekMode = sSeekModeMap.getOrDefault(mode, SEEK_NEXT_SYNC);
+                synchronized (mPendingCommands) {
+                    Object token = mPlayer.seekTo(msec, mp2SeekMode);
+                    addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_SEEK_TO, future, token);
+                }
+                futures.add(future);
+                return futures;
+            }
+        };
+        addPendingFuture(pendingFuture);
+        return pendingFuture;
     }
 
     /**
@@ -1315,14 +1647,24 @@
      * @return a {@link ListenableFuture} which represents the pending completion of the command.
      * {@link PlayerResult} will be delivered when the command completes.
      */
-    public ListenableFuture<PlayerResult> setAudioSessionId(int sessionId) {
-        ResolvableFuture<PlayerResult> future = ResolvableFuture.create();
-        synchronized (mPendingCommands) {
-            Object token = mPlayer.setAudioSessionId(sessionId);
-            addPendingCommandLocked(
-                    MediaPlayer2.CALL_COMPLETED_SET_AUDIO_SESSION_ID, future, token);
-        }
-        return future;
+    @NonNull
+    public ListenableFuture<PlayerResult> setAudioSessionId(final int sessionId) {
+        PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>() {
+            @Override
+            List<ResolvableFuture<PlayerResult>> onExecute() {
+                ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>();
+                ResolvableFuture<PlayerResult> future = ResolvableFuture.create();
+                synchronized (mPendingCommands) {
+                    Object token = mPlayer.setAudioSessionId(sessionId);
+                    addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_SET_AUDIO_SESSION_ID,
+                            future, token);
+                }
+                futures.add(future);
+                return futures;
+            }
+        };
+        addPendingFuture(pendingFuture);
+        return pendingFuture;
     }
 
     /**
@@ -1351,14 +1693,24 @@
      * @return a {@link ListenableFuture} which represents the pending completion of the command.
      * {@link PlayerResult} will be delivered when the command completes.
      */
-    public ListenableFuture<PlayerResult> attachAuxEffect(int effectId) {
-        ResolvableFuture<PlayerResult> future = ResolvableFuture.create();
-        synchronized (mPendingCommands) {
-            Object token = mPlayer.attachAuxEffect(effectId);
-            addPendingCommandLocked(
-                    MediaPlayer2.CALL_COMPLETED_ATTACH_AUX_EFFECT, future, token);
-        }
-        return future;
+    @NonNull
+    public ListenableFuture<PlayerResult> attachAuxEffect(final int effectId) {
+        PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>() {
+            @Override
+            List<ResolvableFuture<PlayerResult>> onExecute() {
+                ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>();
+                ResolvableFuture<PlayerResult> future = ResolvableFuture.create();
+                synchronized (mPendingCommands) {
+                    Object token = mPlayer.attachAuxEffect(effectId);
+                    addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_ATTACH_AUX_EFFECT,
+                            future, token);
+                }
+                futures.add(future);
+                return futures;
+            }
+        };
+        addPendingFuture(pendingFuture);
+        return pendingFuture;
     }
 
 
@@ -1376,14 +1728,24 @@
      * @return a {@link ListenableFuture} which represents the pending completion of the command.
      * {@link PlayerResult} will be delivered when the command completes.
      */
-    public ListenableFuture<PlayerResult> setAuxEffectSendLevel(float level) {
-        ResolvableFuture<PlayerResult> future = ResolvableFuture.create();
-        synchronized (mPendingCommands) {
-            Object token = mPlayer.setAuxEffectSendLevel(level);
-            addPendingCommandLocked(
-                    MediaPlayer2.CALL_COMPLETED_SET_AUX_EFFECT_SEND_LEVEL, future, token);
-        }
-        return future;
+    @NonNull
+    public ListenableFuture<PlayerResult> setAuxEffectSendLevel(final float level) {
+        PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>() {
+            @Override
+            List<ResolvableFuture<PlayerResult>> onExecute() {
+                ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>();
+                ResolvableFuture<PlayerResult> future = ResolvableFuture.create();
+                synchronized (mPendingCommands) {
+                    Object token = mPlayer.setAuxEffectSendLevel(level);
+                    addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_SET_AUX_EFFECT_SEND_LEVEL,
+                            future, token);
+                }
+                futures.add(future);
+                return futures;
+            }
+        };
+        addPendingFuture(pendingFuture);
+        return pendingFuture;
     }
 
     /**
@@ -1391,6 +1753,7 @@
      *
      * @return List of track info. The total number of tracks is the size of the list.
      */
+    @NonNull
     public List<TrackInfo> getTrackInfo() {
         List<MediaPlayer2.TrackInfo> list = mPlayer.getTrackInfo();
         List<TrackInfo> trackList = new ArrayList<>();
@@ -1449,13 +1812,24 @@
      * @return a {@link ListenableFuture} which represents the pending completion of the command.
      * {@link PlayerResult} will be delivered when the command completes.
      */
-    public ListenableFuture<PlayerResult> selectTrack(int index) {
-        ResolvableFuture<PlayerResult> future = ResolvableFuture.create();
-        synchronized (mPendingCommands) {
-            Object token = mPlayer.selectTrack(index);
-            addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_SELECT_TRACK, future, token);
-        }
-        return future;
+    @NonNull
+    public ListenableFuture<PlayerResult> selectTrack(final int index) {
+        PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>() {
+            @Override
+            List<ResolvableFuture<PlayerResult>> onExecute() {
+                ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>();
+                ResolvableFuture<PlayerResult> future = ResolvableFuture.create();
+                synchronized (mPendingCommands) {
+                    Object token = mPlayer.selectTrack(index);
+                    addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_SELECT_TRACK,
+                            future, token);
+                }
+                futures.add(future);
+                return futures;
+            }
+        };
+        addPendingFuture(pendingFuture);
+        return pendingFuture;
     }
 
     /**
@@ -1472,20 +1846,34 @@
      * @return a {@link ListenableFuture} which represents the pending completion of the command.
      * {@link PlayerResult} will be delivered when the command completes.
      */
-    public ListenableFuture<PlayerResult> deselectTrack(int index) {
-        ResolvableFuture<PlayerResult> future = ResolvableFuture.create();
-        synchronized (mPendingCommands) {
-            Object token = mPlayer.deselectTrack(index);
-            addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_DESELECT_TRACK, future, token);
-        }
-        return future;
+    @NonNull
+    public ListenableFuture<PlayerResult> deselectTrack(final int index) {
+        PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>() {
+            @Override
+            List<ResolvableFuture<PlayerResult>> onExecute() {
+                ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>();
+                ResolvableFuture<PlayerResult> future = ResolvableFuture.create();
+                synchronized (mPendingCommands) {
+                    Object token = mPlayer.deselectTrack(index);
+                    addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_DESELECT_TRACK,
+                            future, token);
+                }
+                futures.add(future);
+                return futures;
+            }
+        };
+        addPendingFuture(pendingFuture);
+        return pendingFuture;
     }
 
     /**
      * Retrieves the DRM Info associated with the current media item.
      *
      * @throws IllegalStateException if called before being prepared
+     * @hide
      */
+    @Nullable
+    @RestrictTo(LIBRARY_GROUP)
     public DrmInfo getDrmInfo() {
         MediaPlayer2.DrmInfo info = mPlayer.getDrmInfo();
         return info == null ? null : new DrmInfo(info);
@@ -1513,15 +1901,28 @@
      * {@link PlayerCallback#onDrmInfo}.
      * @return a {@link ListenableFuture} which represents the pending completion of the command.
      * {@link DrmResult} will be delivered when the command completes.
+     * @hide
      */
+    @RestrictTo(LIBRARY_GROUP)
     // This is an asynchronous call.
-    public ResolvableFuture<DrmResult> prepareDrm(@NonNull UUID uuid) {
-        ResolvableFuture<DrmResult> future = ResolvableFuture.create();
-        synchronized (mPendingCommands) {
-            Object token = mPlayer.prepareDrm(uuid);
-            addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_PREPARE_DRM, future, token);
-        }
-        return future;
+    @NonNull
+    public ListenableFuture<DrmResult> prepareDrm(@NonNull final UUID uuid) {
+        PendingFuture<DrmResult> pendingFuture = new PendingFuture<DrmResult>() {
+            @Override
+            List<ResolvableFuture<DrmResult>> onExecute() {
+                ArrayList<ResolvableFuture<DrmResult>> futures = new ArrayList<>();
+                ResolvableFuture<DrmResult> future = ResolvableFuture.create();
+                synchronized (mPendingCommands) {
+                    Object token = mPlayer.prepareDrm(uuid);
+                    addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_PREPARE_DRM, future, token);
+                }
+                futures.add(future);
+                return futures;
+            }
+        };
+
+        addPendingFuture(pendingFuture);
+        return pendingFuture;
     }
 
     /**
@@ -1532,7 +1933,9 @@
      * A {@code reset()} call will release the DRM session implicitly.
      *
      * @throws NoDrmSchemeException if there is no active DRM session to release
+     * @hide
      */
+    @RestrictTo(LIBRARY_GROUP)
     public void releaseDrm() throws NoDrmSchemeException {
         try {
             mPlayer.releaseDrm();
@@ -1577,7 +1980,9 @@
      * This may be {@code null} if no additional parameters are to be sent.
      *
      * @throws NoDrmSchemeException if there is no active DRM session
+     * @hide
      */
+    @RestrictTo(LIBRARY_GROUP)
     @NonNull
     public MediaDrm.KeyRequest getDrmKeyRequest(
             @Nullable byte[] keySetId, @Nullable byte[] initData,
@@ -1610,7 +2015,10 @@
      * @throws NoDrmSchemeException if there is no active DRM session
      * @throws DeniedByServerException if the response indicates that the
      * server rejected the request
+     * @hide
      */
+    @Nullable
+    @RestrictTo(LIBRARY_GROUP)
     public byte[] provideDrmKeyResponse(
             @Nullable byte[] keySetId, @NonNull byte[] response)
             throws NoDrmSchemeException, DeniedByServerException {
@@ -1626,7 +2034,9 @@
      * keys to load, obtained from a prior call to {@link #provideDrmKeyResponse}.
      *
      * @param keySetId identifies the saved key set to restore
+     * @hide
      */
+    @RestrictTo(LIBRARY_GROUP)
     public void restoreDrmKeys(@NonNull byte[] keySetId) throws NoDrmSchemeException {
         try {
             mPlayer.restoreDrmKeys(keySetId);
@@ -1643,7 +2053,9 @@
      * Standard fields names are:
      * {@link MediaDrm#PROPERTY_VENDOR}, {@link MediaDrm#PROPERTY_VERSION},
      * {@link MediaDrm#PROPERTY_DESCRIPTION}, {@link MediaDrm#PROPERTY_ALGORITHMS}
+     * @hide
      */
+    @RestrictTo(LIBRARY_GROUP)
     @NonNull
     public String getDrmPropertyString(@NonNull String propertyName) throws NoDrmSchemeException {
         try {
@@ -1662,7 +2074,9 @@
      * Standard fields names are:
      * {@link MediaDrm#PROPERTY_VENDOR}, {@link MediaDrm#PROPERTY_VERSION},
      * {@link MediaDrm#PROPERTY_DESCRIPTION}, {@link MediaDrm#PROPERTY_ALGORITHMS}
+     * @hide
      */
+    @RestrictTo(LIBRARY_GROUP)
     public void setDrmPropertyString(@NonNull String propertyName, @NonNull String value)
             throws NoDrmSchemeException {
         try {
@@ -1679,13 +2093,15 @@
      * of {@link #prepareDrm(UUID uuid)}.
      *
      * @param listener the callback that will be run
+     * @hide
      */
-    public void setOnDrmConfigHelper(final OnDrmConfigHelper listener) {
+    @RestrictTo(LIBRARY_GROUP)
+    public void setOnDrmConfigHelper(@Nullable final OnDrmConfigHelper listener) {
         mPlayer.setOnDrmConfigHelper(listener == null ? null :
                 new MediaPlayer2.OnDrmConfigHelper() {
                     @Override
                     public void onDrmConfig(MediaPlayer2 mp, MediaItem2 item) {
-                        listener.onDrmConfig(XMediaPlayer.this, item);
+                        listener.onDrmConfig(MediaPlayer.this, item);
                     }
                 });
     }
@@ -1703,7 +2119,7 @@
             notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() {
                 @Override
                 public void callCallback(SessionPlayer2.PlayerCallback callback) {
-                    callback.onPlayerStateChanged(XMediaPlayer.this, state);
+                    callback.onPlayerStateChanged(MediaPlayer.this, state);
                 }
             });
         }
@@ -1719,7 +2135,7 @@
             notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() {
                 @Override
                 public void callCallback(SessionPlayer2.PlayerCallback callback) {
-                    callback.onBufferingStateChanged(XMediaPlayer.this, item, state);
+                    callback.onBufferingStateChanged(MediaPlayer.this, item, state);
                 }
             });
         }
@@ -1740,7 +2156,7 @@
     }
 
     @SuppressWarnings("WeakerAccess") /* synthetic access */
-    void notifyXMediaPlayerCallback(final XMediaPlayerCallbackNotifier notifier) {
+    void notifyMediaPlayerCallback(final MediaPlayerCallbackNotifier notifier) {
         List<Pair<SessionPlayer2.PlayerCallback, Executor>> callbacks = getCallbacks();
         for (Pair<SessionPlayer2.PlayerCallback, Executor> pair : callbacks) {
             if (pair.first instanceof PlayerCallback) {
@@ -1759,26 +2175,33 @@
         void callCallback(SessionPlayer2.PlayerCallback callback);
     }
 
-    private interface XMediaPlayerCallbackNotifier {
+    private interface MediaPlayerCallbackNotifier {
         void callCallback(PlayerCallback callback);
     }
 
-    private ListenableFuture<PlayerResult> setPlayerMediaItemsInternal(
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    List<ResolvableFuture<PlayerResult>> setMediaItemsInternal(
             @NonNull MediaItem2 curItem, @Nullable MediaItem2 nextItem) {
         boolean setMediaItemCalled;
         synchronized (mPlaylistLock) {
             setMediaItemCalled = mSetMediaItemCalled;
         }
-        ListenableFuture<PlayerResult> future = !setMediaItemCalled
-                ? setMediaItemInternal(curItem)
-                : CombindedCommandResultFuture.create(mExecutor,
-                        setNextMediaItemInternal(curItem), skipToNextInternal());
 
-        return nextItem == null ? future : CombindedCommandResultFuture.create(
-                mExecutor, future, setNextMediaItemInternal(nextItem));
+        ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>();
+        if (setMediaItemCalled) {
+            futures.add(setNextMediaItemInternal(curItem));
+            futures.add(skipToNextInternal());
+        } else {
+            futures.add(setMediaItemInternal(curItem));
+        }
+
+        if (nextItem != null) {
+            futures.add(setNextMediaItemInternal(nextItem));
+        }
+        return futures;
     }
 
-    private ListenableFuture<PlayerResult> setMediaItemInternal(MediaItem2 item) {
+    private ResolvableFuture<PlayerResult> setMediaItemInternal(MediaItem2 item) {
         ResolvableFuture<PlayerResult> future = ResolvableFuture.create();
         synchronized (mPendingCommands) {
             Object token = mPlayer.setMediaItem(item);
@@ -1790,7 +2213,8 @@
         return future;
     }
 
-    private ListenableFuture<PlayerResult> setNextMediaItemInternal(MediaItem2 item) {
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    ResolvableFuture<PlayerResult> setNextMediaItemInternal(MediaItem2 item) {
         ResolvableFuture<PlayerResult> future = ResolvableFuture.create();
         synchronized (mPendingCommands) {
             Object token = mPlayer.setNextMediaItem(item);
@@ -1800,7 +2224,8 @@
         return future;
     }
 
-    private ListenableFuture<PlayerResult> skipToNextInternal() {
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    ResolvableFuture<PlayerResult> skipToNextInternal() {
         ResolvableFuture<PlayerResult> future = ResolvableFuture.create();
         synchronized (mPendingCommands) {
             Object token = mPlayer.skipToNext();
@@ -1810,21 +2235,33 @@
         return future;
     }
 
-    private ListenableFuture<PlayerResult> createFutureForResultCodeInternal(int resultCode) {
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    ResolvableFuture<PlayerResult> setPlayerVolumeInternal(float volume) {
         ResolvableFuture<PlayerResult> future = ResolvableFuture.create();
         synchronized (mPendingCommands) {
-            if (mPendingCommands.size() > 0) {
-                // TODO: Find a better way to set the call type.
-                addPendingCommandLocked(CALL_COMPLETE_PLAYLIST_BASE - resultCode, future, null);
-            } else {
-                future.set(new PlayerResult(resultCode, mPlayer.getCurrentMediaItem()));
-            }
+            Object token = mPlayer.setPlayerVolume(volume);
+            addPendingCommandLocked(
+                    MediaPlayer2.CALL_COMPLETED_SET_PLAYER_VOLUME, future, token);
         }
         return future;
     }
 
-    @SuppressWarnings("GuardedBy")
-    private void applyShuffleModeLocked() {
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    ResolvableFuture<PlayerResult> createFutureForResultCode(int resultCode) {
+        ResolvableFuture<PlayerResult> future = ResolvableFuture.create();
+        future.set(new PlayerResult(resultCode, mPlayer.getCurrentMediaItem()));
+        return future;
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    List<ResolvableFuture<PlayerResult>> createFuturesForResultCode(int resultCode) {
+        ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>();
+        futures.add(createFutureForResultCode(resultCode));
+        return futures;
+    }
+
+    @SuppressWarnings({"GuardedBy", "WeakerAccess"}) /* synthetic access */
+    void applyShuffleModeLocked() {
         mShuffledList.clear();
         mShuffledList.addAll(mPlaylist);
         if (mShuffleMode == SessionPlayer2.SHUFFLE_MODE_ALL
@@ -1841,8 +2278,8 @@
      * same, it will return null Pair. If non null Pair which contains two nulls, that means one of
      * current and next item or both are changed to null.
      */
-    @SuppressWarnings("GuardedBy")
-    private Pair<MediaItem2, MediaItem2> updateAndGetCurrentNextItemIfNeededLocked() {
+    @SuppressWarnings({"GuardedBy", "WeakerAccess"}) /* synthetic access */
+    Pair<MediaItem2, MediaItem2> updateAndGetCurrentNextItemIfNeededLocked() {
         MediaItem2 changedCurItem = null;
         MediaItem2 changedNextItem = null;
         if (mCurrentShuffleIdx < 0) {
@@ -1876,7 +2313,8 @@
     }
 
     // Clamps value to [0, maxValue]
-    private static int clamp(int value, int maxValue) {
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    static int clamp(int value, int maxValue) {
         if (value < 0) {
             return 0;
         }
@@ -1914,7 +2352,7 @@
                         @Override
                         public void callCallback(
                                 SessionPlayer2.PlayerCallback callback) {
-                            callback.onSeekCompleted(XMediaPlayer.this, pos);
+                            callback.onSeekCompleted(MediaPlayer.this, pos);
                         }
                     });
                     break;
@@ -1923,7 +2361,7 @@
                         @Override
                         public void callCallback(
                                 SessionPlayer2.PlayerCallback callback) {
-                            callback.onCurrentMediaItemChanged(XMediaPlayer.this, item);
+                            callback.onCurrentMediaItemChanged(MediaPlayer.this, item);
                         }
                     });
                     break;
@@ -1934,7 +2372,7 @@
                         @Override
                         public void callCallback(
                                 SessionPlayer2.PlayerCallback callback) {
-                            callback.onPlaybackSpeedChanged(XMediaPlayer.this, speed);
+                            callback.onPlaybackSpeedChanged(MediaPlayer.this, speed);
                         }
                     });
                     break;
@@ -1943,7 +2381,7 @@
                     notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() {
                         @Override
                         public void callCallback(SessionPlayer2.PlayerCallback callback) {
-                            callback.onAudioAttributesChanged(XMediaPlayer.this, attr);
+                            callback.onAudioAttributesChanged(MediaPlayer.this, attr);
                         }
                     });
                     break;
@@ -1957,32 +2395,54 @@
                     status, DrmResult.RESULT_CODE_PREPARATION_ERROR);
             expected.mFuture.set(new DrmResult(resultCode, item));
         }
+        executePendingFuturesIfNeeded();
+    }
 
-        PendingCommand command;
-        // TODO: Make commands be executed sequentially
-        while (true) {
-            synchronized (mPendingCommands) {
-                command = mPendingCommands.peekFirst();
+    private void executePendingFuturesIfNeeded() {
+        synchronized (mPendingFutures) {
+            PendingFuture<? super PlayerResult> pendingFuture;
+            while ((pendingFuture = mPendingFutures.peekFirst()) != null) {
+                if (pendingFuture.isCancelled()) {
+                    mPendingFutures.removeFirst();
+                } else if (pendingFuture.mExecuted) {
+                    // Set result and remove pending futures that are finished.
+                    if (pendingFuture.setResultIfFinished()) {
+                        mPendingFutures.removeFirst();
+                    } else {
+                        break;
+                    }
+                } else {
+                    pendingFuture.execute();
+                    if (!pendingFuture.isDone()) {
+                        break;
+                    }
+                }
             }
-            if (command == null || command.mCallType > CALL_COMPLETE_PLAYLIST_BASE) {
-                break;
+            // Execute skip futures earlier for making them be skipped.
+            Iterator<PendingFuture<? super PlayerResult>> it = mPendingFutures.iterator();
+            if (it.hasNext()) {
+                // The first future is being handled.
+                it.next();
             }
-            command.mFuture.set(new PlayerResult(
-                    CALL_COMPLETE_PLAYLIST_BASE - command.mCallType, item));
-            synchronized (mPendingCommands) {
-                mPendingCommands.removeFirst();
+            while (it.hasNext()) {
+                PendingFuture f = it.next();
+                if (!f.mIsSeekTo) {
+                    break;
+                }
+                f.execute();
             }
         }
     }
+
     @SuppressWarnings("WeakerAccess") /* synthetic access */
     class Mp2DrmCallback extends MediaPlayer2.DrmEventCallback {
         @Override
         public void onDrmInfo(
                 MediaPlayer2 mp, final MediaItem2 item, final MediaPlayer2.DrmInfo drmInfo) {
-            notifyXMediaPlayerCallback(new XMediaPlayerCallbackNotifier() {
+            notifyMediaPlayerCallback(new MediaPlayerCallbackNotifier() {
                 @Override
                 public void callCallback(PlayerCallback callback) {
-                    callback.onDrmInfo(XMediaPlayer.this, item,
+                    callback.onDrmInfo(MediaPlayer.this, item,
                             drmInfo == null ? null : new DrmInfo(drmInfo));
                 }
             });
@@ -1999,10 +2459,10 @@
         @Override
         public void onVideoSizeChanged(
                 MediaPlayer2 mp, final MediaItem2 item, final int width, final int height) {
-            notifyXMediaPlayerCallback(new XMediaPlayerCallbackNotifier() {
+            notifyMediaPlayerCallback(new MediaPlayerCallbackNotifier() {
                 @Override
                 public void callCallback(PlayerCallback callback) {
-                    callback.onVideoSizeChanged(XMediaPlayer.this, item, width, height);
+                    callback.onVideoSizeChanged(MediaPlayer.this, item, width, height);
                 }
             });
         }
@@ -2010,10 +2470,10 @@
         @Override
         public void onTimedMetaDataAvailable(
                 MediaPlayer2 mp, final MediaItem2 item, final TimedMetaData2 data) {
-            notifyXMediaPlayerCallback(new XMediaPlayerCallbackNotifier() {
+            notifyMediaPlayerCallback(new MediaPlayerCallbackNotifier() {
                 @Override
                 public void callCallback(PlayerCallback callback) {
-                    callback.onTimedMetaDataAvailable(XMediaPlayer.this, item, data);
+                    callback.onTimedMetaDataAvailable(MediaPlayer.this, item, data);
                 }
             });
         }
@@ -2023,10 +2483,10 @@
                 MediaPlayer2 mp, final MediaItem2 item, final int what, final int extra) {
             setState(PLAYER_STATE_ERROR);
             setBufferingState(item, BUFFERING_STATE_UNKNOWN);
-            notifyXMediaPlayerCallback(new XMediaPlayerCallbackNotifier() {
+            notifyMediaPlayerCallback(new MediaPlayerCallbackNotifier() {
                 @Override
                 public void callCallback(PlayerCallback callback) {
-                    callback.onError(XMediaPlayer.this, item, what, extra);
+                    callback.onError(MediaPlayer.this, item, what, extra);
                 }
             });
         }
@@ -2052,16 +2512,16 @@
                     notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() {
                         @Override
                         public void callCallback(SessionPlayer2.PlayerCallback callback) {
-                            callback.onPlaybackCompleted(XMediaPlayer.this);
+                            callback.onPlaybackCompleted(MediaPlayer.this);
                         }
                     });
                     break;
             }
             final int what = sInfoCodeMap.getOrDefault(mp2What, MEDIA_INFO_UNKNOWN);
-            notifyXMediaPlayerCallback(new XMediaPlayerCallbackNotifier() {
+            notifyMediaPlayerCallback(new MediaPlayerCallbackNotifier() {
                 @Override
                 public void callCallback(PlayerCallback callback) {
-                    callback.onInfo(XMediaPlayer.this, item, what, extra);
+                    callback.onInfo(MediaPlayer.this, item, what, extra);
                 }
             });
         }
@@ -2075,26 +2535,26 @@
         @Override
         public void onMediaTimeDiscontinuity(
                 MediaPlayer2 mp, final MediaItem2 item, final MediaTimestamp2 timestamp) {
-            notifyXMediaPlayerCallback(new XMediaPlayerCallbackNotifier() {
+            notifyMediaPlayerCallback(new MediaPlayerCallbackNotifier() {
                 @Override
                 public void callCallback(PlayerCallback callback) {
-                    callback.onMediaTimeDiscontinuity(XMediaPlayer.this, item, timestamp);
+                    callback.onMediaTimeDiscontinuity(MediaPlayer.this, item, timestamp);
                 }
             });
         }
 
         @Override
         public void onCommandLabelReached(MediaPlayer2 mp, Object label) {
-            // Ignore. XMediaPlayer does not use MediaPlayer2.notifyWhenCommandLabelReached().
+            // Ignore. MediaPlayer does not use MediaPlayer2.notifyWhenCommandLabelReached().
         }
 
         @Override
         public void onSubtitleData(
                 MediaPlayer2 mp, final MediaItem2 item, final SubtitleData2 data) {
-            notifyXMediaPlayerCallback(new XMediaPlayerCallbackNotifier() {
+            notifyMediaPlayerCallback(new MediaPlayerCallbackNotifier() {
                 @Override
                 public void callCallback(PlayerCallback callback) {
-                    callback.onSubtitleData(XMediaPlayer.this, item, data);
+                    callback.onSubtitleData(MediaPlayer.this, item, data);
                 }
             });
         }
@@ -2117,7 +2577,7 @@
          * @param height the height of the video
          */
         public void onVideoSizeChanged(
-                XMediaPlayer mp, MediaItem2 item, int width, int height) { }
+                @NonNull MediaPlayer mp, @NonNull MediaItem2 item, int width, int height) { }
 
         /**
          * Called to indicate available timed metadata
@@ -2135,8 +2595,8 @@
          * @param item the MediaItem2 of this media item
          * @param data the timed metadata sample associated with this event
          */
-        public void onTimedMetaDataAvailable(
-                XMediaPlayer mp, MediaItem2 item, TimedMetaData2 data) { }
+        public void onTimedMetaDataAvailable(@NonNull MediaPlayer mp,
+                @NonNull MediaItem2 item, @NonNull TimedMetaData2 data) { }
 
         /**
          * Called to indicate an error.
@@ -2147,8 +2607,8 @@
          * @param extra an extra code, specific to the error. Typically
          * implementation dependent.
          */
-        public void onError(
-                XMediaPlayer mp, MediaItem2 item, @MediaError int what, int extra) { }
+        public void onError(@NonNull MediaPlayer mp,
+                @NonNull MediaItem2 item, @MediaError int what, int extra) { }
 
         /**
          * Called to indicate an info or a warning.
@@ -2159,7 +2619,8 @@
          * @param extra an extra code, specific to the info. Typically
          * implementation dependent.
          */
-        public void onInfo(XMediaPlayer mp, MediaItem2 item, @MediaInfo int what, int extra) { }
+        public void onInfo(@NonNull MediaPlayer mp,
+                @NonNull MediaItem2 item, @MediaInfo int what, int extra) { }
 
         /**
          * Called when a discontinuity in the normal progression of the media time is detected.
@@ -2181,8 +2642,8 @@
          * @param timestamp the timestamp that correlates media time, system time and clock rate,
          *     or {@link MediaTimestamp2#TIMESTAMP_UNKNOWN} in an error case.
          */
-        public void onMediaTimeDiscontinuity(
-                XMediaPlayer mp, MediaItem2 item, MediaTimestamp2 timestamp) { }
+        public void onMediaTimeDiscontinuity(@NonNull MediaPlayer mp,
+                @NonNull MediaItem2 item, @NonNull MediaTimestamp2 timestamp) { }
 
         /**
          * Called when when a player subtitle track has new subtitle data available.
@@ -2190,8 +2651,8 @@
          * @param item the MediaItem2 of this media item
          * @param data the subtitle data
          */
-        public void onSubtitleData(
-                XMediaPlayer mp, MediaItem2 item, @NonNull SubtitleData2 data) { }
+        public void onSubtitleData(@NonNull MediaPlayer mp,
+                @NonNull MediaItem2 item, @NonNull SubtitleData2 data) { }
 
         /**
          * Called to indicate DRM info is available
@@ -2200,8 +2661,11 @@
          * @param item the MediaItem2 of this media item
          * @param drmInfo DRM info of the source including PSSH, and subset
          *                of crypto schemes supported by this device
+         * @hide
          */
-        public void onDrmInfo(XMediaPlayer mp, MediaItem2 item, DrmInfo drmInfo) { }
+        @RestrictTo(LIBRARY_GROUP)
+        public void onDrmInfo(@NonNull MediaPlayer mp,
+                @NonNull MediaItem2 item, @NonNull DrmInfo drmInfo) { }
     }
 
     /**
@@ -2236,6 +2700,7 @@
          * When the language is unknown or could not be determined,
          * ISO-639-2 language code, "und", is returned.
          */
+        @NonNull
         public String getLanguage() {
             String language = mFormat.getString(MediaFormat.KEY_LANGUAGE);
             return language == null ? "und" : language;
@@ -2245,6 +2710,7 @@
          * Gets the {@link MediaFormat} of the track.  If the format is
          * unknown or could not be determined, null is returned.
          */
+        @Nullable
         public MediaFormat getFormat() {
             if (mTrackType == MEDIA_TRACK_TYPE_TIMEDTEXT
                     || mTrackType == MEDIA_TRACK_TYPE_SUBTITLE) {
@@ -2288,13 +2754,16 @@
 
     /**
      * Encapsulates the DRM properties of the source.
+     * @hide
      */
+    @RestrictTo(LIBRARY_GROUP)
     public static final class DrmInfo {
         private final MediaPlayer2.DrmInfo mMp2DrmInfo;
 
         /**
          * Returns the PSSH info of the media item for each supported DRM scheme.
          */
+        @NonNull
         public Map<UUID, byte[]> getPssh() {
             return mMp2DrmInfo.getPssh();
         }
@@ -2304,6 +2773,7 @@
          * It effectively identifies the subset of the source's DRM schemes which
          * are supported by the device too.
          */
+        @NonNull
         public List<UUID> getSupportedSchemes() {
             return mMp2DrmInfo.getSupportedSchemes();
         }
@@ -2322,23 +2792,27 @@
      *
      * The only allowed DRM calls in this listener are {@link #getDrmPropertyString}
      * and {@link #setDrmPropertyString}.
+     * @hide
      */
+    @RestrictTo(LIBRARY_GROUP)
     public interface OnDrmConfigHelper {
         /**
          * Called to give the app the opportunity to configure DRM before the session is created
          *
-         * @param mp the {@code XMediaPlayer} associated with this callback
+         * @param mp the {@code MediaPlayer} associated with this callback
          * @param item the MediaItem2 of this media item
          */
-        void onDrmConfig(XMediaPlayer mp, MediaItem2 item);
+        void onDrmConfig(@NonNull MediaPlayer mp, @NonNull MediaItem2 item);
     }
 
     /**
      * Thrown when a DRM method is called before preparing a DRM scheme through prepareDrm().
      * Extends MediaDrm.MediaDrmException
+     * @hide
      */
+    @RestrictTo(LIBRARY_GROUP)
     public static class NoDrmSchemeException extends MediaDrmException {
-        public NoDrmSchemeException(String detailMessage) {
+        public NoDrmSchemeException(@Nullable String detailMessage) {
             super(detailMessage);
         }
     }
@@ -2440,59 +2914,11 @@
         public static final String ERROR_CODE = "android.media.mediaplayer.errcode";
     }
 
-    static final class CombindedCommandResultFuture extends AbstractResolvableFuture<PlayerResult> {
-        final ListenableFuture<PlayerResult>[] mFutures;
-        AtomicInteger mSuccessCount = new AtomicInteger(0);
-
-        public static CombindedCommandResultFuture create(Executor executor,
-                ListenableFuture<PlayerResult>... futures) {
-            return new CombindedCommandResultFuture(executor, futures);
-        }
-
-        private CombindedCommandResultFuture(Executor executor,
-                ListenableFuture<PlayerResult>[] futures) {
-            mFutures = futures;
-            for (int i = 0; i < mFutures.length; ++i) {
-                final int cur = i;
-                mFutures[i].addListener(new Runnable() {
-                    @Override
-                    public void run() {
-                        try {
-                            PlayerResult result = mFutures[cur].get();
-                            int resultCode = result.getResultCode();
-                            if (resultCode != RESULT_CODE_SUCCESS
-                                    && resultCode != RESULT_CODE_SKIPPED) {
-                                for (int j = 0; j < mFutures.length; ++j) {
-                                    if (!mFutures[j].isCancelled() && !mFutures[j].isDone()
-                                            && cur != j) {
-                                        mFutures[j].cancel(true);
-                                    }
-                                }
-                                set(result);
-                            } else {
-                                int cnt = mSuccessCount.incrementAndGet();
-                                if (cnt == mFutures.length) {
-                                    set(result);
-                                }
-                            }
-                        } catch (Exception e) {
-                            for (int j = 0; j < mFutures.length; ++j) {
-                                if (!mFutures[j].isCancelled() && !mFutures[j].isDone()
-                                        && cur != j) {
-                                    mFutures[j].cancel(true);
-                                }
-                            }
-                            setException(e);
-                        }
-                    }
-                }, executor);
-            }
-        }
-    }
-
     /**
      * Result class of the asynchronous DRM APIs.
+     * @hide
      */
+    @RestrictTo(LIBRARY_GROUP)
     public static class DrmResult extends PlayerResult {
         /**
          * The device required DRM provisioning but couldn't reach the provisioning server.
@@ -2538,7 +2964,7 @@
          * @param resultCode result code. Recommends to use the standard code defined here.
          * @param item media item when the operation is completed
          */
-        public DrmResult(@DrmResultCode int resultCode, @Nullable MediaItem2 item) {
+        public DrmResult(@DrmResultCode int resultCode, @NonNull MediaItem2 item) {
             super(resultCode, item);
         }
 
@@ -2548,7 +2974,8 @@
          * @return result code.
          */
         @Override
-        public @DrmResultCode int getResultCode() {
+        @DrmResultCode
+        public int getResultCode() {
             return super.getResultCode();
         }
     }
diff --git a/media2/src/main/java/androidx/media2/MediaPlayer2.java b/media2/src/main/java/androidx/media2/MediaPlayer2.java
index 2052654..4512c53 100644
--- a/media2/src/main/java/androidx/media2/MediaPlayer2.java
+++ b/media2/src/main/java/androidx/media2/MediaPlayer2.java
@@ -491,7 +491,7 @@
      * @return a token which can be used to cancel the operation later with {@link #cancel}.
      */
     // This is an asynchronous call.
-    public abstract Object setSurface(Surface surface);
+    public abstract Object setSurface(@Nullable Surface surface);
 
     /* Do not change these video scaling mode values below without updating
      * their counterparts in system/window.h! Please do not forget to update
diff --git a/media2/src/main/java/androidx/media2/MediaPlayer2Impl.java b/media2/src/main/java/androidx/media2/MediaPlayer2Impl.java
index 7a0f799..fac87a7 100644
--- a/media2/src/main/java/androidx/media2/MediaPlayer2Impl.java
+++ b/media2/src/main/java/androidx/media2/MediaPlayer2Impl.java
@@ -1760,11 +1760,19 @@
         }
 
         synchronized int getVideoWidth() {
-            return getCurrentPlayer().getVideoWidth();
+            try {
+                return getCurrentPlayer().getVideoWidth();
+            } catch (IllegalStateException e) {
+                return 0;
+            }
         }
 
         synchronized int getVideoHeight() {
-            return getCurrentPlayer().getVideoHeight();
+            try {
+                return getCurrentPlayer().getVideoHeight();
+            } catch (IllegalStateException e) {
+                return 0;
+            }
         }
 
         synchronized PersistableBundle getMetrics() {
diff --git a/media2/src/main/java/androidx/media2/MediaSession2.java b/media2/src/main/java/androidx/media2/MediaSession2.java
index 20b3162..7cc076f 100644
--- a/media2/src/main/java/androidx/media2/MediaSession2.java
+++ b/media2/src/main/java/androidx/media2/MediaSession2.java
@@ -335,22 +335,6 @@
     }
 
     /**
-     * @hide
-     */
-    @RestrictTo(LIBRARY_GROUP)
-    public void skipForward() {
-        mImpl.skipForward();
-    }
-
-    /**
-     * @hide
-     */
-    @RestrictTo(LIBRARY_GROUP)
-    public void skipBackward() {
-        mImpl.skipBackward();
-    }
-
-    /**
      * Notify routes information to a connected controller
      *
      * @param controller controller information
@@ -453,7 +437,7 @@
          * @see SessionCommand2#COMMAND_CODE_PLAYER_PAUSE
          * @see SessionCommand2#COMMAND_CODE_PLAYER_SKIP_TO_NEXT_PLAYLIST_ITEM
          * @see SessionCommand2#COMMAND_CODE_PLAYER_SKIP_TO_PREVIOUS_PLAYLIST_ITEM
-         * @see SessionCommand2#COMMAND_CODE_PLAYER_PREFETCH
+         * @see SessionCommand2#COMMAND_CODE_PLAYER_PREPARE
          * @see SessionCommand2#COMMAND_CODE_PLAYER_SEEK_TO
          * @see SessionCommand2#COMMAND_CODE_PLAYER_SKIP_TO_PLAYLIST_ITEM
          * @see SessionCommand2#COMMAND_CODE_PLAYER_SET_SHUFFLE_MODE
@@ -477,20 +461,18 @@
          * Called when a controller has sent a command with a {@link MediaItem2} to add a new media
          * item to this session. Being specific, this will be called for following APIs.
          * <ol>
-         * <li>{@link MediaController2#addPlaylistItem(int, MediaItem2)}
-         * <li>{@link MediaController2#replacePlaylistItem(int, MediaItem2)}
+         * <li>{@link MediaController2#addPlaylistItem(int, String)}
+         * <li>{@link MediaController2#replacePlaylistItem(int, String)}
          * <li>{@link MediaController2#setPlaylist(List, MediaMetadata2)}
-         * <li>{@link MediaController2#setMediaItem(MediaItem2)}
+         * <li>{@link MediaController2#setMediaItem(String)}
          * </ol>
-         * Override this to translate incoming media items to be understood by your player. It's
-         * highly recommended to create and return a new media item here. Otherwise session player
-         * may reject the command.
+         * Override this to translate incoming {@code mediaId} to a {@link MediaItem2} to be
+         * understood by your player. For example, a player may only understand
+         * {@link FileMediaItem2}, {@link UriMediaItem2}, and {@link CallbackMediaItem2}. Check the
+         * documentation of the player that you're using.
          * <p>
-         * For example, a player may only understand {@link FileMediaItem2}, {@link UriMediaItem2},
-         * and {@link CallbackMediaItem2} while the media item from the controller would be
-         * sanitized not to contain subclass information. In that case you need to override this
-         * method to create a media item for player from given media item.
-         * <p>
+         * If the given media ID is valid, you should return the media item with the given media ID.
+         * If the ID doesn't match, an {@link RuntimeException} will be thrown.
          * You may return {@code null} if the given item is invalid. Here's the behavior when it
          * happens.
          * <table border="0" cellspacing="0" cellpadding="0">
@@ -506,17 +488,17 @@
          * This will be called on the same thread where {@link #onCommandRequest} and commands with
          * the media controller will be executed.
          * <p>
-         * Default implementation simply returns the media item from the controller although the
-         * player may ignore or throw an exception.
+         * Default implementation returns the {@code null}.
          *
          * @param session the session for this event
          * @param controller controller information
-         * @param item incoming media item from the controller
-         * @return translated media item for player. Can be {@code null} to ignore
+         * @param mediaId non-empty media id for creating item with
+         * @return translated media item for player with the mediaId. Can be {@code null} to ignore.
+         * @see MediaMetadata2#METADATA_KEY_MEDIA_ID
          */
         public @Nullable MediaItem2 onCreateMediaItem(@NonNull MediaSession2 session,
-                @NonNull ControllerInfo controller, @NonNull MediaItem2 item) {
-            return item;
+                @NonNull ControllerInfo controller, @NonNull String mediaId) {
+            return null;
         }
 
         /**
@@ -530,7 +512,7 @@
          *
          * @param session the session for this event
          * @param controller controller information
-         * @param mediaId media id from the controller
+         * @param mediaId non-empty media id
          * @param rating new rating from the controller
          * @see SessionCommand2#COMMAND_CODE_SESSION_SET_RATING
          */
@@ -569,7 +551,7 @@
          *
          * @param session the session for this event
          * @param controller controller information
-         * @param mediaId media id
+         * @param mediaId non-empty media id
          * @param extras optional extra bundle
          * @see SessionCommand2#COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID
          * @hide
@@ -584,13 +566,10 @@
         /**
          * Called when a controller requested to begin playback from a search query through
          * {@link MediaController2#playFromSearch(String, Bundle)}
-         * <p>
-         * An empty query indicates that the app may play any music. The implementation should
-         * attempt to make a smart choice about what to play.
          *
          * @param session the session for this event
          * @param controller controller information
-         * @param query query string. Can be empty to indicate any suggested media
+         * @param query non-empty search query.
          * @param extras optional extra bundle
          * @see SessionCommand2#COMMAND_CODE_SESSION_PLAY_FROM_SEARCH
          * @hide
@@ -621,14 +600,14 @@
         }
 
         /**
-         * Called when a controller requested to prefetch for playing a specific mediaId through
-         * {@link MediaController2#prefetchFromMediaId(String, Bundle)}.
+         * Called when a controller requested to prepare for playing a specific mediaId through
+         * {@link MediaController2#prepareFromMediaId(String, Bundle)}.
          * <p>
-         * During the prefetch, a session should not hold audio focus in order to allow
+         * During the prepare, a session should not hold audio focus in order to allow
          * other sessions play seamlessly. The state of playback should be updated to
-         * {@link SessionPlayer2#PLAYER_STATE_PAUSED} after the prefetch is done.
+         * {@link SessionPlayer2#PLAYER_STATE_PAUSED} after the prepare is done.
          * <p>
-         * The playback of the prefetched content should start in the later calls of
+         * The playback of the prepared content should start in the later calls of
          * {@link SessionPlayer2#play()}.
          * <p>
          * Override {@link #onPlayFromMediaId} to handle requests for starting
@@ -636,30 +615,27 @@
          *
          * @param session the session for this event
          * @param controller controller information
-         * @param mediaId media id to prefetch
+         * @param mediaId non-empty media id
          * @param extras optional extra bundle
-         * @see SessionCommand2#COMMAND_CODE_SESSION_PREFETCH_FROM_MEDIA_ID
+         * @see SessionCommand2#COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID
          * @hide
          */
         @RestrictTo(LIBRARY_GROUP)
-        public @ResultCode int onPrefetchFromMediaId(@NonNull MediaSession2 session,
+        public @ResultCode int onPrepareFromMediaId(@NonNull MediaSession2 session,
                 @NonNull ControllerInfo controller, @NonNull String mediaId,
                 @Nullable Bundle extras) {
             return RESULT_CODE_NOT_SUPPORTED;
         }
 
         /**
-         * Called when a controller requested to prefetch playback from a search query through
-         * {@link MediaController2#prefetchFromSearch(String, Bundle)}.
+         * Called when a controller requested to prepare playback from a search query through
+         * {@link MediaController2#prepareFromSearch(String, Bundle)}.
          * <p>
-         * An empty query indicates that the app may prefetch any music. The implementation should
-         * attempt to make a smart choice about what to play.
-         * <p>
-         * During the prefetch, a session should not hold audio focus in order to allow
+         * During the prepare, a session should not hold audio focus in order to allow
          * other sessions play seamlessly. The state of playback should be updated to
-         * {@link SessionPlayer2#PLAYER_STATE_PAUSED} after the prefetch is done.
+         * {@link SessionPlayer2#PLAYER_STATE_PAUSED} after the prepare is done.
          * <p>
-         * The playback of the prefetched content should start in the later calls of
+         * The playback of the prepared content should start in the later calls of
          * {@link SessionPlayer2#play()}.
          * <p>
          * Override {@link #onPlayFromSearch} to handle requests for starting playback without
@@ -667,27 +643,27 @@
          *
          * @param session the session for this event
          * @param controller controller information
-         * @param query query string. Can be empty to indicate any suggested media
+         * @param query non-empty search query
          * @param extras optional extra bundle
-         * @see SessionCommand2#COMMAND_CODE_SESSION_PREFETCH_FROM_SEARCH
+         * @see SessionCommand2#COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH
          * @hide
          */
         @RestrictTo(LIBRARY_GROUP)
-        public @ResultCode int onPrefetchFromSearch(@NonNull MediaSession2 session,
+        public @ResultCode int onPrepareFromSearch(@NonNull MediaSession2 session,
                 @NonNull ControllerInfo controller, @NonNull String query,
                 @Nullable Bundle extras) {
             return RESULT_CODE_NOT_SUPPORTED;
         }
 
         /**
-         * Called when a controller requested to prefetch a specific media item represented by a URI
-         * through {@link MediaController2#prefetchFromUri(Uri, Bundle)}.
+         * Called when a controller requested to prepare a specific media item represented by a URI
+         * through {@link MediaController2#prepareFromUri(Uri, Bundle)}.
          * <p>
-         * During the prefetch, a session should not hold audio focus in order to allow
+         * During the prepare, a session should not hold audio focus in order to allow
          * other sessions play seamlessly. The state of playback should be updated to
-         * {@link SessionPlayer2#PLAYER_STATE_PAUSED} after the prefetch is done.
+         * {@link SessionPlayer2#PLAYER_STATE_PAUSED} after the prepare is done.
          * <p>
-         * The playback of the prefetched content should start in the later calls of
+         * The playback of the prepared content should start in the later calls of
          * {@link SessionPlayer2#play()}.
          * <p>
          * Override {@link #onPlayFromUri} to handle requests for starting playback without
@@ -697,17 +673,19 @@
          * @param controller controller information
          * @param uri uri
          * @param extras optional extra bundle
-         * @see SessionCommand2#COMMAND_CODE_SESSION_PREFETCH_FROM_URI
+         * @see SessionCommand2#COMMAND_CODE_SESSION_PREPARE_FROM_URI
          * @hide
          */
         @RestrictTo(LIBRARY_GROUP)
-        public @ResultCode int onPrefetchFromUri(@NonNull MediaSession2 session,
+        public @ResultCode int onPrepareFromUri(@NonNull MediaSession2 session,
                 @NonNull ControllerInfo controller, @NonNull Uri uri, @Nullable Bundle extras) {
             return RESULT_CODE_NOT_SUPPORTED;
         }
 
         /**
-         * Called when a controller called {@link MediaController2#fastForward()}
+         * Called when a controller called {@link MediaController2#fastForward()}.
+         * <p>
+         * It's recommended to increase the playback speed when this method is called.
          *
          * @param session the session for this event
          * @param controller controller information
@@ -719,7 +697,9 @@
         }
 
         /**
-         * Called when a controller called {@link MediaController2#rewind()}
+         * Called when a controller called {@link MediaController2#rewind()}.
+         * <p>
+         * It's recommended to decrease the playback speed when this method is called.
          *
          * @param session the session for this event
          * @param controller controller information
@@ -731,6 +711,36 @@
         }
 
         /**
+         * Called when a controller called {@link MediaController2#skipForward()}.
+         * <p>
+         * It's recommended to seek forward within the current media item when this method
+         * is called.
+         *
+         * @param session the session for this event
+         * @param controller controller information
+         * @see SessionCommand2#COMMAND_CODE_SESSION_SKIP_FORWARD
+         */
+        public @ResultCode int onSkipForward(@NonNull MediaSession2 session,
+                @NonNull ControllerInfo controller) {
+            return RESULT_CODE_NOT_SUPPORTED;
+        }
+
+        /**
+         * Called when a controller called {@link MediaController2#skipBackward()}.
+         * <p>
+         * It's recommended to seek backward within the current media item when this method
+         * is called.
+         *
+         * @param session the session for this event
+         * @param controller controller information
+         * @see SessionCommand2#COMMAND_CODE_SESSION_SKIP_BACKWARD
+         */
+        public @ResultCode int onSkipBackward(@NonNull MediaSession2 session,
+                @NonNull ControllerInfo controller) {
+            return RESULT_CODE_NOT_SUPPORTED;
+        }
+
+        /**
          * Called when a controller called {@link MediaController2#subscribeRoutesInfo()}
          * Session app should notify the routes information by calling
          * {@link MediaSession2#notifyRoutesInfoChanged(ControllerInfo, List)}.
diff --git a/media2/src/main/java/androidx/media2/MediaSession2ImplBase.java b/media2/src/main/java/androidx/media2/MediaSession2ImplBase.java
index 1dc73ad..e02d881 100644
--- a/media2/src/main/java/androidx/media2/MediaSession2ImplBase.java
+++ b/media2/src/main/java/androidx/media2/MediaSession2ImplBase.java
@@ -16,6 +16,7 @@
 
 package androidx.media2;
 
+import static androidx.media2.BaseResult2.RESULT_CODE_BAD_VALUE;
 import static androidx.media2.MediaSession2.ControllerCb;
 import static androidx.media2.MediaSession2.ControllerInfo;
 import static androidx.media2.MediaSession2.SessionCallback;
@@ -52,6 +53,7 @@
 import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.concurrent.futures.AbstractResolvableFuture;
 import androidx.concurrent.futures.ResolvableFuture;
 import androidx.core.util.ObjectsCompat;
 import androidx.media.AudioAttributesCompat;
@@ -69,13 +71,14 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicInteger;
 
 class MediaSession2ImplBase implements MediaSession2Impl {
     private static final String DEFAULT_MEDIA_SESSION_TAG_PREFIX = "android.media.session2.id";
     private static final String DEFAULT_MEDIA_SESSION_TAG_DELIM = ".";
 
     static final String TAG = "MS2ImplBase";
-    static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+    static final boolean DEBUG = true; //Log.isLoggable(TAG, Log.DEBUG);
 
     // Note: This checks the uniqueness of a session ID only in single process.
     // When the framework becomes able to check the uniqueness, this logic should be removed.
@@ -90,7 +93,8 @@
     private final MediaSessionCompat mSessionCompat;
     private final MediaSession2Stub mSession2Stub;
     private final MediaSessionLegacyStub mSessionLegacyStub;
-    private final Executor mCallbackExecutor;
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    final Executor mCallbackExecutor;
     @SuppressWarnings("WeakerAccess") /* synthetic access */
     final SessionCallback mCallback;
     private final String mSessionId;
@@ -280,6 +284,9 @@
             if (isClosed()) {
                 return;
             }
+            if (DEBUG) {
+                Log.d(TAG, "Closing session, id=" + getId() + ", token=" + getToken());
+            }
             synchronized (MediaSession2ImplBase.class) {
                 SESSION_ID_LIST.remove(mSessionId);
             }
@@ -408,8 +415,8 @@
                     // Let dispatchPlayerTask() handle such cases.
                     return null;
                 }
-                return XMediaPlayer.CombindedCommandResultFuture.create(DIRECT_EXECUTOR,
-                        prepareFuture, playFuture);
+                return CombinedCommandResultFuture.create(
+                        DIRECT_EXECUTOR, prepareFuture, playFuture);
             }
         });
     }
@@ -425,7 +432,7 @@
     }
 
     @Override
-    public ListenableFuture<PlayerResult> prefetch() {
+    public ListenableFuture<PlayerResult> prepare() {
         return dispatchPlayerTask(new PlayerTask<ListenableFuture<PlayerResult>>() {
             @Override
             public ListenableFuture<PlayerResult> run(SessionPlayer2 player) throws Exception {
@@ -445,18 +452,6 @@
     }
 
     @Override
-    public ListenableFuture<PlayerResult> skipForward() {
-        // To match with KEYCODE_MEDIA_SKIP_FORWARD
-        return null;
-    }
-
-    @Override
-    public ListenableFuture<PlayerResult> skipBackward() {
-        // To match with KEYCODE_MEDIA_SKIP_BACKWARD
-        return null;
-    }
-
-    @Override
     public void notifyRoutesInfoChanged(@NonNull ControllerInfo controller,
             @Nullable final List<Bundle> routes) {
         dispatchRemoteControllerCallbackTask(controller, new RemoteControllerCallbackTask() {
@@ -586,14 +581,18 @@
     }
 
     @Override
-    public ListenableFuture<PlayerResult> skipToPlaylistItem(final @NonNull MediaItem2 item) {
+    public ListenableFuture<PlayerResult> skipToPlaylistItem(final int index) {
         return dispatchPlayerTask(new PlayerTask<ListenableFuture<PlayerResult>>() {
             @Override
             public ListenableFuture<PlayerResult> run(SessionPlayer2 player) throws Exception {
-                if (item == null) {
-                    throw new IllegalArgumentException("item shouldn't be null");
+                if (index < 0) {
+                    throw new IllegalArgumentException("index shouldn't be negative");
                 }
-                return player.skipToPlaylistItem(item);
+                final List<MediaItem2> list = player.getPlaylist();
+                if (index >= list.size()) {
+                    return PlayerResult.createFuture(RESULT_CODE_BAD_VALUE);
+                }
+                return player.skipToPlaylistItem(list.get(index));
             }
         });
     }
@@ -646,14 +645,18 @@
     }
 
     @Override
-    public ListenableFuture<PlayerResult> removePlaylistItem(final @NonNull MediaItem2 item) {
+    public ListenableFuture<PlayerResult> removePlaylistItem(final int index) {
         return dispatchPlayerTask(new PlayerTask<ListenableFuture<PlayerResult>>() {
             @Override
             public ListenableFuture<PlayerResult> run(SessionPlayer2 player) throws Exception {
-                if (item == null) {
-                    throw new IllegalArgumentException("item shouldn't be null");
+                if (index < 0) {
+                    throw new IllegalArgumentException("index shouldn't be negative");
                 }
-                return player.removePlaylistItem(item);
+                final List<MediaItem2> list = player.getPlaylist();
+                if (index >= list.size()) {
+                    return PlayerResult.createFuture(RESULT_CODE_BAD_VALUE);
+                }
+                return player.removePlaylistItem(list.get(index));
             }
         });
     }
@@ -1135,9 +1138,15 @@
 
     private static class SessionPlayerCallback extends SessionPlayer2.PlayerCallback {
         private final WeakReference<MediaSession2ImplBase> mSession;
+        private MediaItem2 mMediaItem;
+        private List<MediaItem2> mList;
+        private final CurrentMediaItemListener mCurrentItemChangedListener;
+        private final PlaylistItemListener mPlaylistItemChangedListener;
 
         SessionPlayerCallback(MediaSession2ImplBase session) {
             mSession = new WeakReference<>(session);
+            mCurrentItemChangedListener = new CurrentMediaItemListener(session);
+            mPlaylistItemChangedListener = new PlaylistItemListener(session);
         }
 
         @Override
@@ -1146,6 +1155,17 @@
             if (session == null || session.getPlayer() != player || player == null) {
                 return;
             }
+            synchronized (session.mLock) {
+                if (mMediaItem != null) {
+                    mMediaItem.removeOnMetadataChangedListener(mCurrentItemChangedListener);
+                }
+                if (item != null)  {
+                    item.addOnMetadataChangedListener(session.mCallbackExecutor,
+                            mCurrentItemChangedListener);
+                }
+                mMediaItem = item;
+            }
+
             // Note: No sanity check whether the item is in the playlist.
             updateDurationIfNeeded(player, item);
             session.dispatchRemoteControllerCallbackTask(new RemoteControllerCallbackTask() {
@@ -1210,6 +1230,25 @@
         @Override
         public void onPlaylistChanged(final SessionPlayer2 player, final List<MediaItem2> list,
                 final MediaMetadata2 metadata) {
+            final MediaSession2ImplBase session = getSession();
+            if (session == null || session.getPlayer() != player || player == null) {
+                return;
+            }
+            synchronized (session.mLock) {
+                if (mList != null) {
+                    for (int i = 0; i < mList.size(); i++) {
+                        mList.get(i).removeOnMetadataChangedListener(mPlaylistItemChangedListener);
+                    }
+                }
+                if (list != null) {
+                    for (int i = 0; i < list.size(); i++) {
+                        list.get(i).addOnMetadataChangedListener(session.mCallbackExecutor,
+                                mPlaylistItemChangedListener);
+                    }
+                }
+                mList = list;
+            }
+
             dispatchRemoteControllerTask(player, new RemoteControllerCallbackTask() {
                 @Override
                 public void run(ControllerCb callback) throws RemoteException {
@@ -1334,6 +1373,9 @@
                         .putLong(MediaMetadata2.METADATA_KEY_DURATION, duration)
                         .putString(MediaMetadata2.METADATA_KEY_MEDIA_ID,
                                 item.getMediaId())
+                        .putLong(MediaMetadata2.METADATA_KEY_BROWSABLE,
+                                MediaMetadata2.BROWSABLE_TYPE_NONE)
+                        .putLong(MediaMetadata2.METADATA_KEY_PLAYABLE, 1)
                         .build();
             }
             if (metadata != null) {
@@ -1348,4 +1390,112 @@
             }
         }
     }
+
+    static class CurrentMediaItemListener implements MediaItem2.OnMetadataChangedListener {
+        private final WeakReference<MediaSession2ImplBase> mSession;
+
+        CurrentMediaItemListener(MediaSession2ImplBase session) {
+            mSession = new WeakReference<>(session);
+        }
+
+        @Override
+        public void onMetadataChanged(final MediaItem2 item) {
+            final MediaSession2ImplBase session = mSession.get();
+            if (session == null || item == null) {
+                return;
+            }
+            final MediaItem2 currentItem = session.getCurrentMediaItem();
+            if (currentItem != null && item.equals(currentItem)) {
+                session.dispatchRemoteControllerCallbackTask(new RemoteControllerCallbackTask() {
+                    @Override
+                    public void run(ControllerCb callback) throws RemoteException {
+                        callback.onCurrentMediaItemChanged(item);
+                    }
+                });
+            }
+        }
+    }
+
+    static class PlaylistItemListener implements MediaItem2.OnMetadataChangedListener {
+        private final WeakReference<MediaSession2ImplBase> mSession;
+
+        PlaylistItemListener(MediaSession2ImplBase session) {
+            mSession = new WeakReference<>(session);
+        }
+
+        @Override
+        public void onMetadataChanged(final MediaItem2 item) {
+            final MediaSession2ImplBase session = mSession.get();
+            if (session == null || item == null) {
+                return;
+            }
+            final List<MediaItem2> list = session.getPlaylist();
+            if (list == null) {
+                return;
+            }
+            for (int i = 0; i < list.size(); i++) {
+                if (item.equals(list.get(i))) {
+                    session.dispatchRemoteControllerCallbackTask(
+                            new RemoteControllerCallbackTask() {
+                                @Override
+                                public void run(ControllerCb callback) throws RemoteException {
+                                    callback.onPlaylistChanged(list, session.getPlaylistMetadata());
+                                }
+                            });
+                    return;
+                }
+            }
+        }
+    }
+
+    static final class CombinedCommandResultFuture<T extends BaseResult2>
+            extends AbstractResolvableFuture<T> {
+        final ListenableFuture<T>[] mFutures;
+        AtomicInteger mSuccessCount = new AtomicInteger(0);
+
+        public static <U extends BaseResult2> CombinedCommandResultFuture create(
+                Executor executor, ListenableFuture<U>... futures) {
+            return new CombinedCommandResultFuture<U>(executor, futures);
+        }
+
+        private CombinedCommandResultFuture(Executor executor,
+                ListenableFuture<T>[] futures) {
+            mFutures = futures;
+            for (int i = 0; i < mFutures.length; ++i) {
+                final int cur = i;
+                mFutures[i].addListener(new Runnable() {
+                    @Override
+                    public void run() {
+                        try {
+                            T result = mFutures[cur].get();
+                            int resultCode = result.getResultCode();
+                            if (resultCode != RESULT_CODE_SUCCESS
+                                    && resultCode != RESULT_CODE_SKIPPED) {
+                                for (int j = 0; j < mFutures.length; ++j) {
+                                    if (!mFutures[j].isCancelled() && !mFutures[j].isDone()
+                                            && cur != j) {
+                                        mFutures[j].cancel(true);
+                                    }
+                                }
+                                set(result);
+                            } else {
+                                int cnt = mSuccessCount.incrementAndGet();
+                                if (cnt == mFutures.length) {
+                                    set(result);
+                                }
+                            }
+                        } catch (Exception e) {
+                            for (int j = 0; j < mFutures.length; ++j) {
+                                if (!mFutures[j].isCancelled() && !mFutures[j].isDone()
+                                        && cur != j) {
+                                    mFutures[j].cancel(true);
+                                }
+                            }
+                            setException(e);
+                        }
+                    }
+                }, executor);
+            }
+        }
+    }
 }
diff --git a/media2/src/main/java/androidx/media2/MediaSession2Stub.java b/media2/src/main/java/androidx/media2/MediaSession2Stub.java
index 58794d4..39ec86e 100644
--- a/media2/src/main/java/androidx/media2/MediaSession2Stub.java
+++ b/media2/src/main/java/androidx/media2/MediaSession2Stub.java
@@ -51,10 +51,10 @@
 import androidx.media2.MediaSession2.ControllerInfo;
 import androidx.media2.MediaSession2.MediaSession2Impl;
 import androidx.media2.MediaSession2.SessionResult;
+import androidx.media2.MediaSession2ImplBase.PlayerTask;
 import androidx.media2.SessionCommand2.CommandCode;
 import androidx.media2.SessionPlayer2.PlayerResult;
 import androidx.versionedparcelable.ParcelImpl;
-import androidx.versionedparcelable.ParcelUtils;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
@@ -174,7 +174,7 @@
             final @CommandCode int commandCode,
             final @NonNull SessionTask task) {
         final ControllerInfo controller = mConnectedControllersManager.getController(
-                caller == null ? null : caller.asBinder());
+                caller.asBinder());
         if (mSessionImpl.isClosed() || controller == null) {
             return;
         }
@@ -296,6 +296,16 @@
                     //     (e.g. add pagination or special way to deliver Bitmap)
                     //   - DeadSystemException means that errors around it can be ignored.
                     Log.w(TAG, "Exception in " + controller.toString(), e);
+                } catch (Exception e) {
+                    // Any random exception may be happen inside of the session player / callback.
+                    if (task instanceof PlayerTask) {
+                        sendPlayerResult(controller, seq,
+                                new PlayerResult(PlayerResult.RESULT_CODE_UNKNOWN_ERROR, null));
+                    } else if (task instanceof SessionCallbackTask) {
+                        sendSessionResult(controller, seq, SessionResult.RESULT_CODE_UNKNOWN_ERROR);
+                    } else if (task instanceof LibrarySessionCallbackTask) {
+                        sendLibraryResult(controller, seq, LibraryResult.RESULT_CODE_UNKNOWN_ERROR);
+                    }
                 }
             }
         });
@@ -359,14 +369,14 @@
                     //       because IMediaController2 is oneway (i.e. async call) and Stub will
                     //       use thread poll for incoming calls.
                     final int playerState = mSessionImpl.getPlayerState();
-                    final ParcelImpl currentItem = (ParcelImpl) ParcelUtils.toParcelable(
+                    final ParcelImpl currentItem = MediaUtils2.toParcelable(
                             mSessionImpl.getCurrentMediaItem());
                     final long positionEventTimeMs = SystemClock.elapsedRealtime();
                     final long positionMs = mSessionImpl.getCurrentPosition();
                     final float playbackSpeed = mSessionImpl.getPlaybackSpeed();
                     final long bufferedPositionMs = mSessionImpl.getBufferedPosition();
                     final ParcelImpl playbackInfo =
-                            (ParcelImpl) ParcelUtils.toParcelable(mSessionImpl.getPlaybackInfo());
+                            MediaUtils2.toParcelable(mSessionImpl.getPlaybackInfo());
                     final int repeatMode = mSessionImpl.getRepeatMode();
                     final int shuffleMode = mSessionImpl.getShuffleMode();
                     final PendingIntent sessionActivity = mSessionImpl.getSessionActivity();
@@ -384,7 +394,7 @@
                     }
                     try {
                         caller.onConnected(MediaSession2Stub.this,
-                                (ParcelImpl) ParcelUtils.toParcelable(allowedCommands),
+                                MediaUtils2.toParcelable(allowedCommands),
                                 playerState, currentItem, positionEventTimeMs, positionMs,
                                 playbackSpeed, bufferedPositionMs, playbackInfo, repeatMode,
                                 shuffleMode, playlistSlice, sessionActivity);
@@ -412,22 +422,21 @@
     @SuppressWarnings("WeakerAccess") /* synthetic access */
     @Nullable
     MediaItem2 convertMediaItem2OnExecutor(ControllerInfo controller,
-            @NonNull ParcelImpl itemParcelImpl) {
-        if (itemParcelImpl == null) {
-            return null;
-        }
-        MediaItem2 item = ParcelUtils.fromParcelable(itemParcelImpl);
-        if (item == null) {
-            Log.w(TAG, "Couldn't convert incoming parcelable to MediaItem2 ");
+            @NonNull String mediaId) {
+        if (TextUtils.isEmpty(mediaId)) {
+            Log.w(TAG, "Media ID shouldn't be null");
             return null;
         }
         MediaItem2 newItem = mSessionImpl.getCallback().onCreateMediaItem(
-                mSessionImpl.getInstance(), controller, item);
+                mSessionImpl.getInstance(), controller, mediaId);
         if (newItem == null) {
-            Log.w(TAG, "onCreateMediaItem(" + newItem + ") returned null");
-            return null;
+            Log.w(TAG, "onCreateMediaItem(mediaId=" + mediaId + ") returned null. Ignoring");
+        } else if (newItem.getMetadata() == null
+                || !TextUtils.equals(mediaId,
+                        newItem.getMetadata().getString(MediaMetadata2.METADATA_KEY_MEDIA_ID))) {
+            throw new RuntimeException("onCreateMediaItem(mediaId=" + mediaId + "): media ID in the"
+                    + " returned media item should match");
         }
-        newItem.mParcelUuid = item.mParcelUuid;
         return newItem;
     }
 
@@ -438,29 +447,41 @@
     @Override
     public void connect(final IMediaController2 caller, int seq, final String callingPackage)
             throws RuntimeException {
+        if (caller == null || TextUtils.isEmpty(callingPackage)) {
+            return;
+        }
         connect(caller, callingPackage, Binder.getCallingPid(), Binder.getCallingUid());
     }
 
     @Override
     public void release(final IMediaController2 caller, int seq) throws RemoteException {
-        mConnectedControllersManager.removeController(caller == null ? null : caller.asBinder());
+        if (caller == null) {
+            return;
+        }
+        mConnectedControllersManager.removeController(caller.asBinder());
     }
 
     @Override
     public void onControllerResult(final IMediaController2 caller, int seq,
             final ParcelImpl controllerResult) {
+        if (caller == null || controllerResult == null) {
+            return;
+        }
         SequencedFutureManager manager = mConnectedControllersManager.getSequencedFutureManager(
                 caller.asBinder());
         if (manager == null) {
             return;
         }
-        MediaController2.ControllerResult result = ParcelUtils.fromParcelable(controllerResult);
+        MediaController2.ControllerResult result = MediaUtils2.fromParcelable(controllerResult);
         manager.setFutureResult(seq, SessionResult.from(result));
     }
 
     @Override
     public void setVolumeTo(final IMediaController2 caller, int seq, final int value,
             final int flags) throws RuntimeException {
+        if (caller == null) {
+            return;
+        }
         dispatchSessionTask(caller, seq, SessionCommand2.COMMAND_CODE_VOLUME_SET_VOLUME,
                 new SessionCallbackTask<Integer>() {
                     @Override
@@ -478,6 +499,9 @@
     @Override
     public void adjustVolume(IMediaController2 caller, int seq, final int direction,
             final int flags) throws RuntimeException {
+        if (caller == null) {
+            return;
+        }
         dispatchSessionTask(caller, seq, SessionCommand2.COMMAND_CODE_VOLUME_ADJUST_VOLUME,
                 new SessionCallbackTask<Integer>() {
                     @Override
@@ -494,6 +518,9 @@
 
     @Override
     public void play(IMediaController2 caller, int seq) throws RuntimeException {
+        if (caller == null) {
+            return;
+        }
         dispatchSessionTask(caller, seq, SessionCommand2.COMMAND_CODE_PLAYER_PLAY,
                 new SessionPlayerTask() {
                     @Override
@@ -505,6 +532,9 @@
 
     @Override
     public void pause(IMediaController2 caller, int seq) throws RuntimeException {
+        if (caller == null) {
+            return;
+        }
         dispatchSessionTask(caller, seq, SessionCommand2.COMMAND_CODE_PLAYER_PAUSE,
                 new SessionPlayerTask() {
                     @Override
@@ -515,18 +545,24 @@
     }
 
     @Override
-    public void prefetch(IMediaController2 caller, int seq) throws RuntimeException {
-        dispatchSessionTask(caller, seq, SessionCommand2.COMMAND_CODE_PLAYER_PREFETCH,
+    public void prepare(IMediaController2 caller, int seq) throws RuntimeException {
+        if (caller == null) {
+            return;
+        }
+        dispatchSessionTask(caller, seq, SessionCommand2.COMMAND_CODE_PLAYER_PREPARE,
                 new SessionPlayerTask() {
                     @Override
                     public ListenableFuture<PlayerResult> run(ControllerInfo controller) {
-                        return mSessionImpl.prefetch();
+                        return mSessionImpl.prepare();
                     }
                 });
     }
 
     @Override
     public void fastForward(IMediaController2 caller, int seq) {
+        if (caller == null) {
+            return;
+        }
         dispatchSessionTask(caller, seq, SessionCommand2.COMMAND_CODE_SESSION_FAST_FORWARD,
                 new SessionCallbackTask<Integer>() {
                     @Override
@@ -539,6 +575,9 @@
 
     @Override
     public void rewind(IMediaController2 caller, int seq) {
+        if (caller == null) {
+            return;
+        }
         dispatchSessionTask(caller, seq, SessionCommand2.COMMAND_CODE_SESSION_REWIND,
                 new SessionCallbackTask<Integer>() {
                     @Override
@@ -550,7 +589,40 @@
     }
 
     @Override
+    public void skipForward(IMediaController2 caller, int seq) {
+        if (caller == null) {
+            return;
+        }
+        dispatchSessionTask(caller, seq, SessionCommand2.COMMAND_CODE_SESSION_SKIP_FORWARD,
+                new SessionCallbackTask<Integer>() {
+                    @Override
+                    public Integer run(ControllerInfo controller) {
+                        return mSessionImpl.getCallback().onSkipForward(
+                                mSessionImpl.getInstance(), controller);
+                    }
+                });
+    }
+
+    @Override
+    public void skipBackward(IMediaController2 caller, int seq) {
+        if (caller == null) {
+            return;
+        }
+        dispatchSessionTask(caller, seq, SessionCommand2.COMMAND_CODE_SESSION_SKIP_BACKWARD,
+                new SessionCallbackTask<Integer>() {
+                    @Override
+                    public Integer run(ControllerInfo controller) {
+                        return mSessionImpl.getCallback().onSkipBackward(
+                                mSessionImpl.getInstance(), controller);
+                    }
+                });
+    }
+
+    @Override
     public void seekTo(IMediaController2 caller, int seq, final long pos) throws RuntimeException {
+        if (caller == null) {
+            return;
+        }
         dispatchSessionTask(caller, seq, SessionCommand2.COMMAND_CODE_PLAYER_SEEK_TO,
                 new SessionPlayerTask() {
                     @Override
@@ -563,7 +635,10 @@
     @Override
     public void onCustomCommand(final IMediaController2 caller, final int seq,
             final ParcelImpl command, final Bundle args) {
-        final SessionCommand2 sessionCommand = ParcelUtils.fromParcelable(command);
+        if (caller == null || command == null) {
+            return;
+        }
+        final SessionCommand2 sessionCommand = MediaUtils2.fromParcelable(command);
         dispatchSessionTask(caller, seq, sessionCommand, new SessionCallbackTask<SessionResult>() {
             @Override
             public SessionResult run(final ControllerInfo controller) {
@@ -583,54 +658,63 @@
     }
 
     @Override
-    public void prefetchFromUri(final IMediaController2 caller, int seq, final Uri uri,
+    public void prepareFromUri(final IMediaController2 caller, int seq, final Uri uri,
             final Bundle extras) {
-        dispatchSessionTask(caller, seq, SessionCommand2.COMMAND_CODE_SESSION_PREFETCH_FROM_URI,
+        if (caller == null) {
+            return;
+        }
+        dispatchSessionTask(caller, seq, SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_URI,
                 new SessionCallbackTask<Integer>() {
                     @Override
                     public Integer run(ControllerInfo controller) {
                         if (uri == null) {
-                            Log.w(TAG, "prefetchFromUri(): Ignoring null uri from " + controller);
+                            Log.w(TAG, "prepareFromUri(): Ignoring null uri from " + controller);
                             return RESULT_CODE_BAD_VALUE;
                         }
-                        return mSessionImpl.getCallback().onPrefetchFromUri(
+                        return mSessionImpl.getCallback().onPrepareFromUri(
                                 mSessionImpl.getInstance(), controller, uri, extras);
                     }
                 });
     }
 
     @Override
-    public void prefetchFromSearch(final IMediaController2 caller, int seq, final String query,
+    public void prepareFromSearch(final IMediaController2 caller, int seq, final String query,
             final Bundle extras) {
-        dispatchSessionTask(caller, seq, SessionCommand2.COMMAND_CODE_SESSION_PREFETCH_FROM_SEARCH,
+        if (caller == null) {
+            return;
+        }
+        dispatchSessionTask(caller, seq, SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH,
                 new SessionCallbackTask<Integer>() {
                     @Override
                     public Integer run(ControllerInfo controller) {
                         if (TextUtils.isEmpty(query)) {
-                            Log.w(TAG, "prefetchFromSearch(): Ignoring empty query from "
+                            Log.w(TAG, "prepareFromSearch(): Ignoring empty query from "
                                     + controller);
                             return RESULT_CODE_BAD_VALUE;
                         }
-                        return mSessionImpl.getCallback().onPrefetchFromSearch(
+                        return mSessionImpl.getCallback().onPrepareFromSearch(
                                 mSessionImpl.getInstance(), controller, query, extras);
                     }
                 });
     }
 
     @Override
-    public void prefetchFromMediaId(final IMediaController2 caller, int seq, final String mediaId,
+    public void prepareFromMediaId(final IMediaController2 caller, int seq, final String mediaId,
             final Bundle extras) {
+        if (caller == null) {
+            return;
+        }
         dispatchSessionTask(caller, seq,
-                SessionCommand2.COMMAND_CODE_SESSION_PREFETCH_FROM_MEDIA_ID,
+                SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID,
                 new SessionCallbackTask<Integer>() {
                     @Override
                     public Integer run(ControllerInfo controller) {
-                        if (mediaId == null) {
-                            Log.w(TAG, "prefetchFromMediaId(): Ignoring null mediaId from "
+                        if (TextUtils.isEmpty(mediaId)) {
+                            Log.w(TAG, "prepareFromMediaId(): Ignoring empty mediaId from "
                                     + controller);
                             return RESULT_CODE_BAD_VALUE;
                         }
-                        return mSessionImpl.getCallback().onPrefetchFromMediaId(
+                        return mSessionImpl.getCallback().onPrepareFromMediaId(
                                 mSessionImpl.getInstance(), controller, mediaId, extras);
                     }
                 });
@@ -639,6 +723,9 @@
     @Override
     public void playFromUri(final IMediaController2 caller, int seq, final Uri uri,
             final Bundle extras) {
+        if (caller == null) {
+            return;
+        }
         dispatchSessionTask(caller, seq, SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_URI,
                 new SessionCallbackTask<Integer>() {
                     @Override
@@ -656,6 +743,9 @@
     @Override
     public void playFromSearch(final IMediaController2 caller, int seq, final String query,
             final Bundle extras) {
+        if (caller == null) {
+            return;
+        }
         dispatchSessionTask(caller, seq, SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_SEARCH,
                 new SessionCallbackTask<Integer>() {
                     @Override
@@ -673,13 +763,16 @@
     @Override
     public void playFromMediaId(final IMediaController2 caller, int seq, final String mediaId,
             final Bundle extras) {
+        if (caller == null) {
+            return;
+        }
         dispatchSessionTask(caller, seq, SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID,
                 new SessionCallbackTask<Integer>() {
                     @Override
                     public Integer run(ControllerInfo controller) {
-                        if (mediaId == null) {
-                            Log.w(TAG,
-                                    "playFromMediaId(): Ignoring null mediaId from " + controller);
+                        if (TextUtils.isEmpty(mediaId)) {
+                            Log.w(TAG, "playFromMediaId(): Ignoring empty mediaId from "
+                                    + controller);
                             return RESULT_CODE_BAD_VALUE;
                         }
                         return mSessionImpl.getCallback().onPlayFromMediaId(
@@ -691,18 +784,20 @@
     @Override
     public void setRating(final IMediaController2 caller, int seq, final String mediaId,
             final ParcelImpl rating) {
-        final Rating2 rating2 = ParcelUtils.fromParcelable(rating);
+        if (caller == null || rating == null) {
+            return;
+        }
+        final Rating2 rating2 = MediaUtils2.fromParcelable(rating);
         dispatchSessionTask(caller, seq, SessionCommand2.COMMAND_CODE_SESSION_SET_RATING,
                 new SessionCallbackTask<Integer>() {
                     @Override
                     public Integer run(ControllerInfo controller) {
-                        if (mediaId == null) {
-                            Log.w(TAG, "setRating(): Ignoring null mediaId from " + controller);
+                        if (TextUtils.isEmpty(mediaId)) {
+                            Log.w(TAG, "setRating(): Ignoring empty mediaId from " + controller);
                             return RESULT_CODE_BAD_VALUE;
                         }
                         if (rating2 == null) {
-                            Log.w(TAG,
-                                    "setRating(): Ignoring null ratingBundle from " + controller);
+                            Log.w(TAG, "setRating(): Ignoring null rating from " + controller);
                             return RESULT_CODE_BAD_VALUE;
                         }
                         return mSessionImpl.getCallback().onSetRating(
@@ -713,6 +808,9 @@
 
     @Override
     public void setPlaybackSpeed(final IMediaController2 caller, int seq, final float speed) {
+        if (caller == null) {
+            return;
+        }
         dispatchSessionTask(caller, seq, SessionCommand2.COMMAND_CODE_PLAYER_SET_SPEED,
                 new SessionPlayerTask() {
                     @Override
@@ -723,17 +821,19 @@
     }
 
     @Override
-    public void setPlaylist(final IMediaController2 caller, int seq,
-            final ParcelImplListSlice listSlice, final Bundle metadata) {
+    public void setPlaylist(final IMediaController2 caller, int seq, final List<String> playlist,
+            final ParcelImpl metadata) {
+        if (caller == null || metadata == null) {
+            return;
+        }
         dispatchSessionTask(caller, seq, SessionCommand2.COMMAND_CODE_PLAYER_SET_PLAYLIST,
                 new SessionPlayerTask() {
                     @Override
                     public ListenableFuture<PlayerResult> run(ControllerInfo controller) {
-                        if (listSlice == null) {
+                        if (playlist == null) {
                             Log.w(TAG, "setPlaylist(): Ignoring null playlist from " + controller);
                             return PlayerResult.createFuture(RESULT_CODE_BAD_VALUE);
                         }
-                        List<ParcelImpl> playlist = listSlice.getList();
                         List<MediaItem2> list = new ArrayList<>();
                         for (int i = 0; i < playlist.size(); i++) {
                             MediaItem2 item = convertMediaItem2OnExecutor(controller,
@@ -742,22 +842,26 @@
                                 list.add(item);
                             }
                         }
-                        return mSessionImpl.setPlaylist(list, MediaMetadata2.fromBundle(metadata));
+                        return mSessionImpl.setPlaylist(list,
+                                (MediaMetadata2) MediaUtils2.fromParcelable(metadata));
                     }
                 });
     }
 
     @Override
-    public void setMediaItem(final IMediaController2 caller, int seq, final ParcelImpl mediaItem) {
+    public void setMediaItem(final IMediaController2 caller, int seq, final String mediaId) {
+        if (caller == null) {
+            return;
+        }
         dispatchSessionTask(caller, seq, SessionCommand2.COMMAND_CODE_PLAYER_SET_MEDIA_ITEM,
                 new SessionPlayerTask() {
                     @Override
                     public ListenableFuture<PlayerResult> run(ControllerInfo controller) {
-                        if (mediaItem == null) {
-                            Log.w(TAG, "setMediaItem(): Ignoring null item from " + controller);
+                        if (TextUtils.isEmpty(mediaId)) {
+                            Log.w(TAG, "setMediaItem(): Ignoring empty mediaId from " + controller);
                             return PlayerResult.createFuture(RESULT_CODE_BAD_VALUE);
                         }
-                        MediaItem2 item = convertMediaItem2OnExecutor(controller, mediaItem);
+                        MediaItem2 item = convertMediaItem2OnExecutor(controller, mediaId);
                         if (item == null) {
                             return PlayerResult.createFuture(RESULT_CODE_BAD_VALUE);
                         }
@@ -768,29 +872,36 @@
 
     @Override
     public void updatePlaylistMetadata(final IMediaController2 caller, int seq,
-            final Bundle metadata) {
+            final ParcelImpl metadata) {
+        if (caller == null) {
+            return;
+        }
         dispatchSessionTask(caller, seq, SessionCommand2.COMMAND_CODE_PLAYER_UPDATE_LIST_METADATA,
                 new SessionPlayerTask() {
                     @Override
                     public ListenableFuture<PlayerResult> run(ControllerInfo controller) {
                         return mSessionImpl.updatePlaylistMetadata(
-                                MediaMetadata2.fromBundle(metadata));
+                                (MediaMetadata2) MediaUtils2.fromParcelable(metadata));
                     }
                 });
     }
 
     @Override
     public void addPlaylistItem(IMediaController2 caller, int seq, final int index,
-            final ParcelImpl mediaItem) {
+            final String mediaId) {
+        if (caller == null) {
+            return;
+        }
         dispatchSessionTask(caller, seq, SessionCommand2.COMMAND_CODE_PLAYER_ADD_PLAYLIST_ITEM,
                 new SessionPlayerTask() {
                     @Override
                     public ListenableFuture<PlayerResult> run(ControllerInfo controller) {
-                        if (mediaItem == null) {
-                            Log.w(TAG, "addPlaylistItem(): Ignoring null item from " + controller);
+                        if (TextUtils.isEmpty(mediaId)) {
+                            Log.w(TAG, "addPlaylistItem(): Ignoring empty mediaId from "
+                                    + controller);
                             return PlayerResult.createFuture(RESULT_CODE_BAD_VALUE);
                         }
-                        MediaItem2 item = convertMediaItem2OnExecutor(controller, mediaItem);
+                        MediaItem2 item = convertMediaItem2OnExecutor(controller, mediaId);
                         if (item == null) {
                             return PlayerResult.createFuture(RESULT_CODE_BAD_VALUE);
                         }
@@ -800,31 +911,35 @@
     }
 
     @Override
-    public void removePlaylistItem(IMediaController2 caller, int seq, final ParcelImpl mediaItem) {
+    public void removePlaylistItem(IMediaController2 caller, int seq, final int index) {
+        if (caller == null) {
+            return;
+        }
         dispatchSessionTask(caller, seq, SessionCommand2.COMMAND_CODE_PLAYER_REMOVE_PLAYLIST_ITEM,
                 new SessionPlayerTask() {
                     @Override
                     public ListenableFuture<PlayerResult> run(ControllerInfo controller) {
-                        MediaItem2 item = ParcelUtils.fromParcelable(mediaItem);
-                        // Note: MediaItem2 has hidden UUID to identify it across the processes.
-                        return mSessionImpl.removePlaylistItem(item);
+                        return mSessionImpl.removePlaylistItem(index);
                     }
                 });
     }
 
     @Override
     public void replacePlaylistItem(IMediaController2 caller, int seq, final int index,
-            final ParcelImpl mediaItem) {
+            final String mediaId) {
+        if (caller == null) {
+            return;
+        }
         dispatchSessionTask(caller, seq, SessionCommand2.COMMAND_CODE_PLAYER_REPLACE_PLAYLIST_ITEM,
                 new SessionPlayerTask() {
                     @Override
                     public ListenableFuture<PlayerResult> run(ControllerInfo controller) {
-                        if (mediaItem == null) {
-                            Log.w(TAG, "replacePlaylistItem(): Ignoring null item from "
+                        if (TextUtils.isEmpty(mediaId)) {
+                            Log.w(TAG, "replacePlaylistItem(): Ignoring empty mediaId from "
                                     + controller);
                             return PlayerResult.createFuture(RESULT_CODE_BAD_VALUE);
                         }
-                        MediaItem2 item = convertMediaItem2OnExecutor(controller, mediaItem);
+                        MediaItem2 item = convertMediaItem2OnExecutor(controller, mediaId);
                         if (item == null) {
                             return PlayerResult.createFuture(RESULT_CODE_BAD_VALUE);
                         }
@@ -834,25 +949,29 @@
     }
 
     @Override
-    public void skipToPlaylistItem(IMediaController2 caller, int seq, final ParcelImpl mediaItem) {
+    public void skipToPlaylistItem(IMediaController2 caller, int seq, final int index) {
+        if (caller == null) {
+            return;
+        }
         dispatchSessionTask(caller, seq, SessionCommand2.COMMAND_CODE_PLAYER_SKIP_TO_PLAYLIST_ITEM,
                 new SessionPlayerTask() {
                     @Override
                     public ListenableFuture<PlayerResult> run(ControllerInfo controller) {
-                        if (mediaItem == null) {
-                            Log.w(TAG, "skipToPlaylistItem(): Ignoring null mediaItem from "
+                        if (index < 0) {
+                            Log.w(TAG, "skipToPlaylistItem(): Ignoring negative index from "
                                     + controller);
                             return PlayerResult.createFuture(RESULT_CODE_BAD_VALUE);
                         }
-                        // Note: MediaItem2 has hidden UUID to identify it across the processes.
-                        return mSessionImpl.skipToPlaylistItem(
-                                (MediaItem2) ParcelUtils.fromParcelable(mediaItem));
+                        return mSessionImpl.skipToPlaylistItem(index);
                     }
                 });
     }
 
     @Override
     public void skipToPreviousItem(IMediaController2 caller, int seq) {
+        if (caller == null) {
+            return;
+        }
         dispatchSessionTask(caller, seq,
                 SessionCommand2.COMMAND_CODE_PLAYER_SKIP_TO_PREVIOUS_PLAYLIST_ITEM,
                 new SessionPlayerTask() {
@@ -865,6 +984,9 @@
 
     @Override
     public void skipToNextItem(IMediaController2 caller, int seq) {
+        if (caller == null) {
+            return;
+        }
         dispatchSessionTask(caller, seq,
                 SessionCommand2.COMMAND_CODE_PLAYER_SKIP_TO_NEXT_PLAYLIST_ITEM,
                 new SessionPlayerTask() {
@@ -877,6 +999,9 @@
 
     @Override
     public void setRepeatMode(IMediaController2 caller, int seq, final int repeatMode) {
+        if (caller == null) {
+            return;
+        }
         dispatchSessionTask(caller, seq, SessionCommand2.COMMAND_CODE_PLAYER_SET_REPEAT_MODE,
                 new SessionPlayerTask() {
                     @Override
@@ -888,6 +1013,9 @@
 
     @Override
     public void setShuffleMode(IMediaController2 caller, int seq, final int shuffleMode) {
+        if (caller == null) {
+            return;
+        }
         dispatchSessionTask(caller, seq, SessionCommand2.COMMAND_CODE_PLAYER_SET_SHUFFLE_MODE,
                 new SessionPlayerTask() {
                     @Override
@@ -899,6 +1027,9 @@
 
     @Override
     public void subscribeRoutesInfo(IMediaController2 caller, int seq) {
+        if (caller == null) {
+            return;
+        }
         dispatchSessionTask(caller, seq, SessionCommand2.COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO,
                 new SessionCallbackTask<Integer>() {
                     @Override
@@ -911,6 +1042,9 @@
 
     @Override
     public void unsubscribeRoutesInfo(IMediaController2 caller, int seq) {
+        if (caller == null) {
+            return;
+        }
         dispatchSessionTask(caller, seq,
                 SessionCommand2.COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO,
                 new SessionCallbackTask<Integer>() {
@@ -924,7 +1058,11 @@
 
     @Override
     public void selectRoute(IMediaController2 caller, int seq, final Bundle route) {
+        if (caller == null) {
+            return;
+        }
         if (MediaUtils2.isUnparcelableBundle(route)) {
+            // TODO (b/118472216): Prevent app crash from illegal binder call.
             throw new RuntimeException("Unexpected route bundle: " + route);
         }
         dispatchSessionTask(caller, seq,
@@ -953,13 +1091,16 @@
     @Override
     public void getLibraryRoot(final IMediaController2 caller, int seq,
             final ParcelImpl libraryParams) throws RuntimeException {
+        if (caller == null || libraryParams == null) {
+            return;
+        }
         dispatchLibrarySessionTask(caller, seq,
                 SessionCommand2.COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT,
                 new LibrarySessionCallbackTask<LibraryResult>() {
                     @Override
                     public LibraryResult run(ControllerInfo controller) {
                         return getLibrarySession().onGetLibraryRootOnExecutor(controller,
-                                (LibraryParams) ParcelUtils.fromParcelable(libraryParams));
+                                (LibraryParams) MediaUtils2.fromParcelable(libraryParams));
                     }
                 });
     }
@@ -984,6 +1125,9 @@
     public void getChildren(final IMediaController2 caller, int seq, final String parentId,
             final int page, final int pageSize, final ParcelImpl libraryParams)
             throws RuntimeException {
+        if (caller == null || libraryParams == null) {
+            return;
+        }
         dispatchLibrarySessionTask(caller, seq, SessionCommand2.COMMAND_CODE_LIBRARY_GET_CHILDREN,
                 new LibrarySessionCallbackTask<LibraryResult>() {
                     @Override
@@ -1003,7 +1147,7 @@
                         }
                         return getLibrarySession().onGetChildrenOnExecutor(controller, parentId,
                                 page, pageSize,
-                                (LibraryParams) ParcelUtils.fromParcelable(libraryParams));
+                                (LibraryParams) MediaUtils2.fromParcelable(libraryParams));
                     }
                 });
     }
@@ -1011,6 +1155,9 @@
     @Override
     public void search(IMediaController2 caller, int seq, final String query,
             final ParcelImpl libraryParams) {
+        if (caller == null || libraryParams == null) {
+            return;
+        }
         dispatchLibrarySessionTask(caller, seq, SessionCommand2.COMMAND_CODE_LIBRARY_SEARCH,
                 new LibrarySessionCallbackTask<Integer>() {
                     @Override
@@ -1020,7 +1167,7 @@
                             return LibraryResult.RESULT_CODE_BAD_VALUE;
                         }
                         return getLibrarySession().onSearchOnExecutor(controller, query,
-                                (LibraryParams) ParcelUtils.fromParcelable(libraryParams));
+                                (LibraryParams) MediaUtils2.fromParcelable(libraryParams));
                     }
                 });
     }
@@ -1028,6 +1175,9 @@
     @Override
     public void getSearchResult(final IMediaController2 caller, int seq, final String query,
             final int page, final int pageSize, final ParcelImpl libraryParams) {
+        if (caller == null || libraryParams == null) {
+            return;
+        }
         dispatchLibrarySessionTask(caller, seq,
                 SessionCommand2.COMMAND_CODE_LIBRARY_GET_SEARCH_RESULT,
                 new LibrarySessionCallbackTask<LibraryResult>() {
@@ -1050,7 +1200,7 @@
                         }
                         return getLibrarySession().onGetSearchResultOnExecutor(controller,
                                 query, page, pageSize,
-                                (LibraryParams) ParcelUtils.fromParcelable(libraryParams));
+                                (LibraryParams) MediaUtils2.fromParcelable(libraryParams));
                     }
                 });
     }
@@ -1058,6 +1208,9 @@
     @Override
     public void subscribe(final IMediaController2 caller, int seq, final String parentId,
             final ParcelImpl libraryParams) {
+        if (caller == null || libraryParams == null) {
+            return;
+        }
         dispatchLibrarySessionTask(caller, seq, SessionCommand2.COMMAND_CODE_LIBRARY_SUBSCRIBE,
                 new LibrarySessionCallbackTask<Integer>() {
                     @Override
@@ -1068,19 +1221,22 @@
                         }
                         return getLibrarySession().onSubscribeOnExecutor(
                                 controller, parentId,
-                                (LibraryParams) ParcelUtils.fromParcelable(libraryParams));
+                                (LibraryParams) MediaUtils2.fromParcelable(libraryParams));
                     }
                 });
     }
 
     @Override
     public void unsubscribe(final IMediaController2 caller, int seq, final String parentId) {
+        if (caller == null) {
+            return;
+        }
         dispatchLibrarySessionTask(caller, seq, SessionCommand2.COMMAND_CODE_LIBRARY_UNSUBSCRIBE,
                 new LibrarySessionCallbackTask<Integer>() {
                     @Override
                     public Integer run(ControllerInfo controller) {
-                        if (parentId == null) {
-                            Log.w(TAG, "unsubscribe(): Ignoring null parentId from " + controller);
+                        if (TextUtils.isEmpty(parentId)) {
+                            Log.w(TAG, "unsubscribe(): Ignoring empty parentId from " + controller);
                             return LibraryResult.RESULT_CODE_BAD_VALUE;
                         }
                         return getLibrarySession().onUnsubscribeOnExecutor(controller, parentId);
@@ -1132,8 +1288,7 @@
             if (result == null) {
                 result = new SessionResult(RESULT_CODE_UNKNOWN_ERROR, null);
             }
-            mIControllerCallback.onSessionResult(seq,
-                    (ParcelImpl) ParcelUtils.toParcelable(result));
+            mIControllerCallback.onSessionResult(seq, MediaUtils2.toParcelable(result));
         }
 
         @Override
@@ -1141,8 +1296,7 @@
             if (result == null) {
                 result = new LibraryResult(LibraryResult.RESULT_CODE_UNKNOWN_ERROR);
             }
-            mIControllerCallback.onLibraryResult(seq,
-                    (ParcelImpl) ParcelUtils.toParcelable(result));
+            mIControllerCallback.onLibraryResult(seq, MediaUtils2.toParcelable(result));
         }
 
         @Override
@@ -1153,20 +1307,18 @@
 
         @Override
         void onPlaybackInfoChanged(PlaybackInfo info) throws RemoteException {
-            mIControllerCallback.onPlaybackInfoChanged((ParcelImpl) ParcelUtils.toParcelable(info));
+            mIControllerCallback.onPlaybackInfoChanged(MediaUtils2.toParcelable(info));
         }
 
         @Override
         void onAllowedCommandsChanged(SessionCommandGroup2 commands) throws RemoteException {
-            mIControllerCallback.onAllowedCommandsChanged(
-                    (ParcelImpl) ParcelUtils.toParcelable(commands));
+            mIControllerCallback.onAllowedCommandsChanged(MediaUtils2.toParcelable(commands));
         }
 
         @Override
         void sendCustomCommand(int seq, SessionCommand2 command, Bundle args)
                 throws RemoteException {
-            mIControllerCallback.onCustomCommand(seq,
-                    (ParcelImpl) ParcelUtils.toParcelable(command), args);
+            mIControllerCallback.onCustomCommand(seq, MediaUtils2.toParcelable(command), args);
         }
 
         @Override
@@ -1184,8 +1336,7 @@
         @Override
         void onBufferingStateChanged(MediaItem2 item, int bufferingState, long bufferedPositionMs)
                 throws RemoteException {
-            mIControllerCallback.onBufferingStateChanged(
-                    (ParcelImpl) ParcelUtils.toParcelable(item),
+            mIControllerCallback.onBufferingStateChanged(MediaUtils2.toParcelable(item),
                     bufferingState, bufferedPositionMs);
         }
 
@@ -1197,8 +1348,7 @@
 
         @Override
         void onCurrentMediaItemChanged(MediaItem2 item) throws RemoteException {
-            mIControllerCallback.onCurrentMediaItemChanged(
-                    (ParcelImpl) ParcelUtils.toParcelable(item));
+            mIControllerCallback.onCurrentMediaItemChanged(MediaUtils2.toParcelable(item));
         }
 
         @Override
@@ -1210,10 +1360,10 @@
                     SessionCommand2.COMMAND_CODE_PLAYER_GET_PLAYLIST)) {
                 mIControllerCallback.onPlaylistChanged(
                         MediaUtils2.convertMediaItem2ListToParcelImplListSlice(playlist),
-                        metadata == null ? null : metadata.toBundle());
+                        MediaUtils2.toParcelable(metadata));
             } else if (mConnectedControllersManager.isAllowedCommand(controller,
                     SessionCommand2.COMMAND_CODE_PLAYER_GET_PLAYLIST_METADATA)) {
-                mIControllerCallback.onPlaylistMetadataChanged(metadata.toBundle());
+                mIControllerCallback.onPlaylistMetadataChanged(MediaUtils2.toParcelable(metadata));
             }
         }
 
@@ -1223,7 +1373,7 @@
                     getCallbackBinder());
             if (mConnectedControllersManager.isAllowedCommand(controller,
                     SessionCommand2.COMMAND_CODE_PLAYER_GET_PLAYLIST_METADATA)) {
-                mIControllerCallback.onPlaylistMetadataChanged(metadata.toBundle());
+                mIControllerCallback.onPlaylistMetadataChanged(MediaUtils2.toParcelable(metadata));
             }
         }
 
@@ -1251,14 +1401,14 @@
         void onChildrenChanged(String parentId, int itemCount, LibraryParams params)
                 throws RemoteException {
             mIControllerCallback.onChildrenChanged(parentId, itemCount,
-                    (ParcelImpl) ParcelUtils.toParcelable(params));
+                    MediaUtils2.toParcelable(params));
         }
 
         @Override
         void onSearchResultChanged(String query, int itemCount, LibraryParams params)
                 throws RemoteException {
             mIControllerCallback.onSearchResultChanged(query, itemCount,
-                    (ParcelImpl) ParcelUtils.toParcelable(params));
+                    MediaUtils2.toParcelable(params));
         }
 
         @Override
diff --git a/media2/src/main/java/androidx/media2/MediaSessionLegacyStub.java b/media2/src/main/java/androidx/media2/MediaSessionLegacyStub.java
index 98aaea8..284c36b 100644
--- a/media2/src/main/java/androidx/media2/MediaSessionLegacyStub.java
+++ b/media2/src/main/java/androidx/media2/MediaSessionLegacyStub.java
@@ -119,24 +119,26 @@
 
     @Override
     public void onPrepare() {
-        dispatchSessionTask(SessionCommand2.COMMAND_CODE_PLAYER_PREFETCH, new SessionTask() {
+        dispatchSessionTask(SessionCommand2.COMMAND_CODE_PLAYER_PREPARE, new SessionTask() {
             @Override
             public void run(ControllerInfo controller) throws RemoteException {
-                mSessionImpl.prefetch();
+                mSessionImpl.prepare();
             }
         });
     }
 
     @Override
     public void onPrepareFromMediaId(final String mediaId, final Bundle extras) {
-        if (mediaId == null) {
-            return;
-        }
-        dispatchSessionTask(SessionCommand2.COMMAND_CODE_SESSION_PREFETCH_FROM_MEDIA_ID,
+        dispatchSessionTask(SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID,
                 new SessionTask() {
                     @Override
                     public void run(ControllerInfo controller) throws RemoteException {
-                        mSessionImpl.getCallback().onPrefetchFromMediaId(mSessionImpl.getInstance(),
+                        if (TextUtils.isEmpty(mediaId)) {
+                            Log.w(TAG, "onPrepareFromMediaId(): Ignoring empty mediaId from "
+                                    + controller);
+                            return;
+                        }
+                        mSessionImpl.getCallback().onPrepareFromMediaId(mSessionImpl.getInstance(),
                                 controller, mediaId, extras);
                     }
                 });
@@ -144,14 +146,16 @@
 
     @Override
     public void onPrepareFromSearch(final String query, final Bundle extras) {
-        if (query == null) {
-            return;
-        }
-        dispatchSessionTask(SessionCommand2.COMMAND_CODE_SESSION_PREFETCH_FROM_SEARCH,
+        dispatchSessionTask(SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH,
                 new SessionTask() {
                     @Override
                     public void run(ControllerInfo controller) throws RemoteException {
-                        mSessionImpl.getCallback().onPrefetchFromSearch(mSessionImpl.getInstance(),
+                        if (TextUtils.isEmpty(query)) {
+                            Log.w(TAG, "onPrepareFromSearch(): Ignoring empty query from "
+                                    + controller);
+                            return;
+                        }
+                        mSessionImpl.getCallback().onPrepareFromSearch(mSessionImpl.getInstance(),
                                 controller, query, extras);
                     }
                 });
@@ -162,11 +166,11 @@
         if (uri == null) {
             return;
         }
-        dispatchSessionTask(SessionCommand2.COMMAND_CODE_SESSION_PREFETCH_FROM_URI,
+        dispatchSessionTask(SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_URI,
                 new SessionTask() {
                     @Override
                     public void run(ControllerInfo controller) throws RemoteException {
-                        mSessionImpl.getCallback().onPrefetchFromUri(mSessionImpl.getInstance(),
+                        mSessionImpl.getCallback().onPrepareFromUri(mSessionImpl.getInstance(),
                                 controller, uri, extras);
                     }
                 });
@@ -184,13 +188,15 @@
 
     @Override
     public void onPlayFromMediaId(final String mediaId, final Bundle extras) {
-        if (mediaId == null) {
-            return;
-        }
         dispatchSessionTask(SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID,
                 new SessionTask() {
                     @Override
                     public void run(ControllerInfo controller) throws RemoteException {
+                        if (TextUtils.isEmpty(mediaId)) {
+                            Log.w(TAG, "onPlayFromMediaId(): Ignoring empty mediaId from "
+                                    + controller);
+                            return;
+                        }
                         mSessionImpl.getCallback().onPlayFromMediaId(mSessionImpl.getInstance(),
                                 controller, mediaId, extras);
                     }
@@ -199,13 +205,15 @@
 
     @Override
     public void onPlayFromSearch(final String query, final Bundle extras) {
-        if (query == null) {
-            return;
-        }
         dispatchSessionTask(SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_SEARCH,
                 new SessionTask() {
                     @Override
                     public void run(ControllerInfo controller) throws RemoteException {
+                        if (TextUtils.isEmpty(query)) {
+                            Log.w(TAG, "onPlayFromSearch(): Ignoring empty query from "
+                                    + controller);
+                            return;
+                        }
                         mSessionImpl.getCallback().onPlayFromSearch(mSessionImpl.getInstance(),
                                 controller, query, extras);
                     }
@@ -290,7 +298,7 @@
     }
 
     @Override
-    public void onSkipToQueueItem(final long id) {
+    public void onSkipToQueueItem(final long queueId) {
         dispatchSessionTask(SessionCommand2.COMMAND_CODE_PLAYER_SKIP_TO_PLAYLIST_ITEM,
                 new SessionTask() {
                     @Override
@@ -299,13 +307,9 @@
                         if (playlist == null) {
                             return;
                         }
-                        for (int i = 0; i < playlist.size(); i++) {
-                            MediaItem2 item = playlist.get(i);
-                            if (item != null && item.getUuid().getMostSignificantBits() == id) {
-                                mSessionImpl.skipToPlaylistItem(item);
-                                break;
-                            }
-                        }
+                        // Use queueId as an index as we've published {@link QueueItem} as so.
+                        // see: {@link MediaUtils2#convertToQueueItemList}.
+                        mSessionImpl.skipToPlaylistItem((int) queueId);
                     }
                 });
     }
@@ -393,18 +397,7 @@
 
     @Override
     public void onAddQueueItem(final MediaDescriptionCompat description) {
-        if (description == null) {
-            return;
-        }
-        dispatchSessionTask(SessionCommand2.COMMAND_CODE_PLAYER_ADD_PLAYLIST_ITEM,
-                new SessionTask() {
-                    @Override
-                    public void run(ControllerInfo controller) throws RemoteException {
-                        // Add the item at the end of the playlist.
-                        mSessionImpl.addPlaylistItem(Integer.MAX_VALUE,
-                                MediaUtils2.convertToMediaItem2(description));
-                    }
-                });
+        onAddQueueItem(description, Integer.MAX_VALUE);
     }
 
     @Override
@@ -416,8 +409,14 @@
                 new SessionTask() {
                     @Override
                     public void run(ControllerInfo controller) throws RemoteException {
-                        mSessionImpl.addPlaylistItem(index,
-                                MediaUtils2.convertToMediaItem2(description));
+                        String mediaId = description.getMediaId();
+                        if (TextUtils.isEmpty(mediaId)) {
+                            Log.w(TAG, "onAddQueueItem(): Media ID shouldn't be empty");
+                            return;
+                        }
+                        MediaItem2 newItem = mSessionImpl.getCallback().onCreateMediaItem(
+                                mSessionImpl.getInstance(), controller, mediaId);
+                        mSessionImpl.addPlaylistItem(index, newItem);
                     }
                 });
     }
@@ -431,14 +430,16 @@
                 new SessionTask() {
                     @Override
                     public void run(ControllerInfo controller) throws RemoteException {
-                        // Note: Here we cannot simply call
-                        // removePlaylistItem(MediaUtils2.convertToMediaItem2(description)),
-                        // because the result of the method will have different UUID.
+                        String mediaId = description.getMediaId();
+                        if (TextUtils.isEmpty(mediaId)) {
+                            Log.w(TAG, "onRemoveQueueItem(): Media ID shouldn't be null");
+                            return;
+                        }
                         List<MediaItem2> playlist = mSessionImpl.getPlaylist();
                         for (int i = 0; i < playlist.size(); i++) {
                             MediaItem2 item = playlist.get(i);
-                            if (TextUtils.equals(item.getMediaId(), description.getMediaId())) {
-                                mSessionImpl.removePlaylistItem(item);
+                            if (TextUtils.equals(item.getMediaId(), mediaId)) {
+                                mSessionImpl.removePlaylistItem(i);
                                 return;
                             }
                         }
@@ -446,6 +447,21 @@
                 });
     }
 
+    @Override
+    public void onRemoveQueueItemAt(final int index) {
+        dispatchSessionTask(SessionCommand2.COMMAND_CODE_PLAYER_REMOVE_PLAYLIST_ITEM,
+                new SessionTask() {
+                    @Override
+                    public void run(ControllerInfo controller) throws RemoteException {
+                        if (index < 0) {
+                            Log.w(TAG, "onRemoveQueueItem(): index shouldn't be negative");
+                            return;
+                        }
+                        mSessionImpl.removePlaylistItem(index);
+                    }
+                });
+    }
+
     ControllerInfo getControllersForAll() {
         return mControllerInfoForAll;
     }
@@ -514,7 +530,7 @@
     }
 
     @SuppressWarnings("WeakerAccess") /* synthetic access */
-    void handleTaskOnExecutor(@Nullable final ControllerInfo controller,
+    void handleTaskOnExecutor(@NonNull final ControllerInfo controller,
             @Nullable final SessionCommand2 sessionCommand, @CommandCode final int commandCode,
             @NonNull final SessionTask task) {
         SessionCommand2 command;
diff --git a/media2/src/main/java/androidx/media2/MediaSessionService2.java b/media2/src/main/java/androidx/media2/MediaSessionService2.java
index dadcae3..06a7b7e 100644
--- a/media2/src/main/java/androidx/media2/MediaSessionService2.java
+++ b/media2/src/main/java/androidx/media2/MediaSessionService2.java
@@ -232,7 +232,7 @@
     @CallSuper
     @Nullable
     @Override
-    public IBinder onBind(Intent intent) {
+    public IBinder onBind(@NonNull Intent intent) {
         return mImpl.onBind(intent);
     }
 
diff --git a/media2/src/main/java/androidx/media2/MediaSessionService2ImplBase.java b/media2/src/main/java/androidx/media2/MediaSessionService2ImplBase.java
index 3d64264..eff821b 100644
--- a/media2/src/main/java/androidx/media2/MediaSessionService2ImplBase.java
+++ b/media2/src/main/java/androidx/media2/MediaSessionService2ImplBase.java
@@ -239,6 +239,11 @@
                             }
                             return;
                         }
+                        if (DEBUG) {
+                            Log.d(TAG, "Handling incoming connection request from the controller"
+                                    + ", controller=" + packageName);
+
+                        }
                         final MediaSession2 session;
                         try {
                             session = service.onGetSession();
diff --git a/media2/src/main/java/androidx/media2/MediaTimestamp2.java b/media2/src/main/java/androidx/media2/MediaTimestamp2.java
index 613ceee..5b43367 100644
--- a/media2/src/main/java/androidx/media2/MediaTimestamp2.java
+++ b/media2/src/main/java/androidx/media2/MediaTimestamp2.java
@@ -22,6 +22,7 @@
 import android.media.MediaTimestamp;
 import android.os.Build;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 
 /**
@@ -39,13 +40,13 @@
  * When the anchor frame is a just-rendered one, the media time stands for
  * current position of the playback or recording.
  *
- * @see XMediaPlayer#getTimestamp
+ * @see MediaPlayer#getTimestamp
  */
 public final class MediaTimestamp2 {
     /**
      * An unknown media timestamp value
      */
-    public static final MediaTimestamp2
+    public static final @NonNull MediaTimestamp2
             TIMESTAMP_UNKNOWN = new MediaTimestamp2(-1, -1, 0.0f);
 
     /**
diff --git a/media2/src/main/java/androidx/media2/MediaUtils2.java b/media2/src/main/java/androidx/media2/MediaUtils2.java
index 945a563..bbeb639 100644
--- a/media2/src/main/java/androidx/media2/MediaUtils2.java
+++ b/media2/src/main/java/androidx/media2/MediaUtils2.java
@@ -16,7 +16,11 @@
 
 package androidx.media2;
 
-import static androidx.media2.MediaItem2.FLAG_PLAYABLE;
+import static android.support.v4.media.MediaDescriptionCompat.EXTRA_BT_FOLDER_TYPE;
+
+import static androidx.media2.MediaMetadata2.BROWSABLE_TYPE_MIXED;
+import static androidx.media2.MediaMetadata2.BROWSABLE_TYPE_NONE;
+import static androidx.media2.MediaMetadata2.METADATA_KEY_BROWSABLE;
 import static androidx.media2.MediaMetadata2.METADATA_KEY_DISPLAY_DESCRIPTION;
 import static androidx.media2.MediaMetadata2.METADATA_KEY_DISPLAY_ICON;
 import static androidx.media2.MediaMetadata2.METADATA_KEY_DISPLAY_ICON_URI;
@@ -24,6 +28,7 @@
 import static androidx.media2.MediaMetadata2.METADATA_KEY_DISPLAY_TITLE;
 import static androidx.media2.MediaMetadata2.METADATA_KEY_MEDIA_ID;
 import static androidx.media2.MediaMetadata2.METADATA_KEY_MEDIA_URI;
+import static androidx.media2.MediaMetadata2.METADATA_KEY_PLAYABLE;
 import static androidx.media2.MediaMetadata2.METADATA_KEY_TITLE;
 
 import android.annotation.SuppressLint;
@@ -40,7 +45,9 @@
 import android.support.v4.media.session.MediaControllerCompat;
 import android.support.v4.media.session.MediaSessionCompat.QueueItem;
 import android.support.v4.media.session.PlaybackStateCompat;
+import android.text.TextUtils;
 
+import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
 import androidx.media.AudioAttributesCompat;
@@ -49,10 +56,10 @@
 import androidx.media2.MediaSession2.CommandButton;
 import androidx.versionedparcelable.ParcelImpl;
 import androidx.versionedparcelable.ParcelUtils;
+import androidx.versionedparcelable.VersionedParcelable;
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.UUID;
 import java.util.concurrent.Executor;
 
 /**
@@ -88,8 +95,8 @@
         if (item2 == null) {
             return null;
         }
+        int flags = 0;
         MediaDescriptionCompat descCompat;
-
         MediaMetadata2 metadata = item2.getMetadata();
         if (metadata == null) {
             descCompat = new MediaDescriptionCompat.Builder()
@@ -121,8 +128,14 @@
             }
 
             descCompat = builder.build();
+
+            boolean browsable = metadata.containsKey(METADATA_KEY_BROWSABLE)
+                    && metadata.getLong(METADATA_KEY_BROWSABLE) != BROWSABLE_TYPE_NONE;
+            boolean playable = metadata.getLong(METADATA_KEY_PLAYABLE) != 0;
+            flags = (browsable ? MediaItem.FLAG_BROWSABLE : 0)
+                    | (playable ? MediaItem.FLAG_PLAYABLE : 0);
         }
-        return new MediaItem(descCompat, item2.getFlags());
+        return new MediaItem(descCompat, flags);
     }
 
     /**
@@ -149,9 +162,9 @@
         if (item == null) {
             return null;
         }
-        MediaMetadata2 metadata2 = convertToMediaMetadata2(item.getDescription());
-        return new MediaItem2.Builder(item.getFlags())
-                .setMediaId(item.getMediaId())
+        MediaMetadata2 metadata2 = convertToMediaMetadata2(item.getDescription(),
+                item.isBrowsable(), item.isPlayable());
+        return new MediaItem2.Builder()
                 .setMetadata(metadata2)
                 .build();
     }
@@ -165,41 +178,36 @@
         }
         // descriptionCompat cannot be null
         MediaDescriptionCompat descriptionCompat = item.getDescription();
-        MediaMetadata2 metadata2 = convertToMediaMetadata2(descriptionCompat);
-        return new MediaItem2.Builder(FLAG_PLAYABLE)
+        MediaMetadata2 metadata2 = convertToMediaMetadata2(descriptionCompat, false, true);
+        return new MediaItem2.Builder()
                 .setMetadata(metadata2)
-                .setUuid(createUuidByQueueIdAndMediaId(item.getQueueId(),
-                        descriptionCompat.getMediaId()))
                 .build();
     }
 
     /**
-     * Create a {@link UUID} with queue id and media id.
-     */
-    public static UUID createUuidByQueueIdAndMediaId(long queueId, String mediaId) {
-        return new UUID(queueId, (mediaId == null) ? 0 : mediaId.hashCode());
-    }
-
-    /**
-     * Convert a {@link MediaMetadataCompat} to a {@link MediaItem2}.
+     * Convert a {@link MediaMetadataCompat} from the {@link MediaControllerCompat#getMetadata()}
+     * to a {@link MediaItem2}.
      */
     public static MediaItem2 convertToMediaItem2(MediaMetadataCompat metadataCompat) {
-        MediaMetadata2 metadata2 = convertToMediaMetadata2(metadataCompat);
-        if (metadata2 == null) {
+        if (metadataCompat == null) {
             return null;
         }
-        return new MediaItem2.Builder(FLAG_PLAYABLE).setMetadata(metadata2).build();
+        // Item is from the MediaControllerCompat, so forcefully set the playable.
+        MediaMetadata2 metadata2 = new MediaMetadata2.Builder(metadataCompat.getBundle())
+                .putLong(METADATA_KEY_BROWSABLE, BROWSABLE_TYPE_NONE)
+                .putLong(METADATA_KEY_PLAYABLE, 1).build();
+        return new MediaItem2.Builder().setMetadata(metadata2).build();
     }
 
     /**
      * Convert a {@link MediaDescriptionCompat} to a {@link MediaItem2}.
      */
     public static MediaItem2 convertToMediaItem2(MediaDescriptionCompat descriptionCompat) {
-        MediaMetadata2 metadata2 = convertToMediaMetadata2(descriptionCompat);
+        MediaMetadata2 metadata2 = convertToMediaMetadata2(descriptionCompat, false, true);
         if (metadata2 == null) {
             return null;
         }
-        return new MediaItem2.Builder(FLAG_PLAYABLE).setMetadata(metadata2).build();
+        return new MediaItem2.Builder().setMetadata(metadata2).build();
     }
 
     /**
@@ -234,20 +242,18 @@
     }
 
     /**
-     * Convert a {@link MediaItem2} to a {@link QueueItem}.
+     * Creates {@link MediaDescriptionCompat} with the id
      */
-    public static QueueItem convertToQueueItem(MediaItem2 item) {
-        if (item == null) {
+    public static MediaDescriptionCompat createMediaDescriptionCompat(String mediaId) {
+        if (TextUtils.isEmpty(mediaId)) {
             return null;
         }
-        MediaDescriptionCompat description = (item.getMetadata() == null)
-                ? new MediaDescriptionCompat.Builder().setMediaId(item.getMediaId()).build()
-                : convertToMediaMetadataCompat(item.getMetadata()).getDescription();
-        return new QueueItem(description, item.getUuid().getMostSignificantBits());
+        return new MediaDescriptionCompat.Builder().setMediaId(mediaId).build();
     }
 
     /**
-     * Convert a list of {@link MediaItem2} to a list of {@link QueueItem}.
+     * Convert a list of {@link MediaItem2} to a list of {@link QueueItem}. The index of the item
+     * would be used as the queue ID to match the behavior of {@link MediaController2}.
      */
     public static List<QueueItem> convertToQueueItemList(List<MediaItem2> items) {
         if (items == null) {
@@ -255,10 +261,11 @@
         }
         List<QueueItem> result = new ArrayList<>();
         for (int i = 0; i < items.size(); i++) {
-            QueueItem queueItem = convertToQueueItem(items.get(i));
-            if (queueItem != null) {
-                result.add(queueItem);
-            }
+            MediaItem2 item = items.get(i);
+            MediaDescriptionCompat description = (item.getMetadata() == null)
+                    ? new MediaDescriptionCompat.Builder().setMediaId(item.getMediaId()).build()
+                    : convertToMediaMetadataCompat(item.getMetadata()).getDescription();
+            result.add(new QueueItem(description, i));
         }
         return result;
     }
@@ -276,7 +283,7 @@
         for (int i = 0; i < parcelImplList.size(); i++) {
             final ParcelImpl itemParcelImpl = parcelImplList.get(i);
             if (itemParcelImpl != null) {
-                mediaItem2List.add((MediaItem2) ParcelUtils.fromParcelable(itemParcelImpl));
+                mediaItem2List.add((MediaItem2) fromParcelable(itemParcelImpl));
             }
         }
         return mediaItem2List;
@@ -312,9 +319,13 @@
      * Creates a {@link MediaMetadata2} from the {@link MediaDescriptionCompat}.
      *
      * @param descCompat A {@link MediaDescriptionCompat} object.
-     * @return The newly created {@link MediaMetadata2} object.
+     * @param browsable {@code true} if it's from {@link MediaItem} with browable flag.
+     * @param playable {@code true} if it's from {@link MediaItem} with playable flag, or from
+     *                 {@link QueueItem}.
+     * @return
      */
-    public static MediaMetadata2 convertToMediaMetadata2(MediaDescriptionCompat descCompat) {
+    private static MediaMetadata2 convertToMediaMetadata2(MediaDescriptionCompat descCompat,
+            boolean browsable, boolean playable) {
         if (descCompat == null) {
             return null;
         }
@@ -349,7 +360,7 @@
 
         Bundle bundle = descCompat.getExtras();
         if (bundle != null) {
-            metadata2Builder.setExtras(descCompat.getExtras());
+            metadata2Builder.setExtras(bundle);
         }
 
         Uri mediaUri = descCompat.getMediaUri();
@@ -357,20 +368,18 @@
             metadata2Builder.putText(METADATA_KEY_MEDIA_URI, mediaUri.toString());
         }
 
-        return metadata2Builder.build();
-    }
-
-    /**
-     * Creates a {@link MediaMetadata2} from the {@link MediaMetadataCompat}.
-     *
-     * @param metadataCompat A {@link MediaMetadataCompat} object.
-     * @return The newly created {@link MediaMetadata2} object.
-     */
-    public static MediaMetadata2 convertToMediaMetadata2(MediaMetadataCompat metadataCompat) {
-        if (metadataCompat == null) {
-            return null;
+        if (bundle != null && bundle.containsKey(EXTRA_BT_FOLDER_TYPE)) {
+            metadata2Builder.putLong(METADATA_KEY_BROWSABLE,
+                    bundle.getLong(EXTRA_BT_FOLDER_TYPE));
+        } else if (browsable) {
+            metadata2Builder.putLong(METADATA_KEY_BROWSABLE, BROWSABLE_TYPE_MIXED);
+        } else {
+            metadata2Builder.putLong(METADATA_KEY_BROWSABLE, BROWSABLE_TYPE_NONE);
         }
-        return new MediaMetadata2(metadataCompat.getBundle());
+
+        metadata2Builder.putLong(METADATA_KEY_PLAYABLE, playable ? 1 : 0);
+
+        return metadata2Builder.build();
     }
 
     /**
@@ -381,7 +390,10 @@
             return null;
         }
         return new MediaMetadata2.Builder()
-                .putString(METADATA_KEY_TITLE, queueTitle.toString()).build();
+                .putString(METADATA_KEY_TITLE, queueTitle.toString())
+                .putLong(METADATA_KEY_BROWSABLE, BROWSABLE_TYPE_MIXED)
+                .putLong(METADATA_KEY_PLAYABLE, 1)
+                .build();
     }
 
     /**
@@ -413,14 +425,6 @@
     }
 
     /**
-     * Creates a {@link MediaMetadataCompat} from the {@link MediaDescriptionCompat}.
-     */
-    public static MediaMetadataCompat convertToMediaMetadataCompat(
-            MediaDescriptionCompat description) {
-        return convertToMediaMetadataCompat(convertToMediaMetadata2(description));
-    }
-
-    /**
      * Creates a {@link Rating2} from the {@link RatingCompat}.
      *
      * @param ratingCompat A {@link RatingCompat} object.
@@ -500,7 +504,7 @@
         List<ParcelImpl> parcelImplList = new ArrayList<>();
         for (int i = 0; i < commandButtonList.size(); i++) {
             final CommandButton commandButton = commandButtonList.get(i);
-            parcelImplList.add((ParcelImpl) ParcelUtils.toParcelable(commandButton));
+            parcelImplList.add(toParcelable(commandButton));
         }
         return parcelImplList;
     }
@@ -517,7 +521,7 @@
         for (int i = 0; i < mediaItem2List.size(); i++) {
             final MediaItem2 item = mediaItem2List.get(i);
             if (item != null) {
-                final ParcelImpl itemParcelImpl = (ParcelImpl) ParcelUtils.toParcelable(item);
+                final ParcelImpl itemParcelImpl = toParcelable(item);
                 itemParcelableList.add(itemParcelImpl);
             }
         }
@@ -693,4 +697,68 @@
         rootHints.putBoolean(BrowserRoot.EXTRA_SUGGESTED, params.isSuggested());
         return rootHints;
     }
+
+    /**
+     * Removes all null elements from the list and returns it.
+     *
+     * @param list
+     * @return
+     */
+    public static <T> List<T> removeNullElements(@Nullable List<T> list) {
+        if (list == null) {
+            return null;
+        }
+        List<T> newList = new ArrayList<>();
+        for (T item : list) {
+            if (item != null) {
+                newList.add(item);
+            }
+        }
+        return newList;
+    }
+
+    /**
+     * Media2 version of {@link ParcelUtils#toParcelable(VersionedParcelable)}.
+     * <p>
+     * This sanitizes {@link MediaItem2}'s subclass information.
+     *
+     * @param item
+     * @return
+     */
+    public static ParcelImpl toParcelable(VersionedParcelable item) {
+        if (item instanceof MediaItem2) {
+            return new MediaItemParcelImpl((MediaItem2) item);
+        }
+        return (ParcelImpl) ParcelUtils.toParcelable(item);
+    }
+
+    /**
+     * Media2 version of {@link ParcelUtils#fromParcelable(Parcelable)}.
+     */
+    @SuppressWarnings("TypeParameterUnusedInFormals")
+    public static <T extends VersionedParcelable> T fromParcelable(ParcelImpl p) {
+        return ParcelUtils.<T>fromParcelable(p);
+    }
+
+    private static class MediaItemParcelImpl extends ParcelImpl {
+        private final MediaItem2 mItem;
+
+        MediaItemParcelImpl(MediaItem2 item) {
+            // Up-cast (possibly MediaItem2's subclass object) item to MediaItem2 for the
+            // writeToParcel(). The copied media item will be only used when it's sent across the
+            // process.
+            super(new MediaItem2(item));
+
+            // Keeps the original copy for local binder to send the original item.
+            // When local binder is used (i.e. binder call happens in a single process),
+            // writeToParcel() wouldn't happen for the Parcelable object and the same object will
+            // be sent through the binder call.
+            mItem = item;
+        }
+
+        @Override
+        public MediaItem2 getVersionedParcel() {
+            return mItem;
+        }
+    }
 }
diff --git a/media2/src/main/java/androidx/media2/PlaybackParams2.java b/media2/src/main/java/androidx/media2/PlaybackParams2.java
index b262dea..c2bb67c 100644
--- a/media2/src/main/java/androidx/media2/PlaybackParams2.java
+++ b/media2/src/main/java/androidx/media2/PlaybackParams2.java
@@ -23,6 +23,8 @@
 import android.os.Build;
 
 import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
 
@@ -32,8 +34,8 @@
 /**
  * Structure for common playback params.
  *
- * Used by {@link XMediaPlayer} {@link XMediaPlayer#getPlaybackParams()} and
- * {@link XMediaPlayer#setPlaybackParams(PlaybackParams2)}
+ * Used by {@link MediaPlayer} {@link MediaPlayer#getPlaybackParams()} and
+ * {@link MediaPlayer#setPlaybackParams(PlaybackParams2)}
  * to control playback behavior.
  * <p> <strong>audio fallback mode:</strong>
  * select out-of-range parameter handling.
@@ -96,7 +98,7 @@
     /**
      * Returns the audio fallback mode. {@code null} if a value is not set.
      */
-    public @AudioFallbackMode Integer getAudioFallbackMode() {
+    public @AudioFallbackMode @Nullable Integer getAudioFallbackMode() {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
             try {
                 return mPlaybackParams.getAudioFallbackMode();
@@ -111,7 +113,7 @@
     /**
      * Returns the pitch factor. {@code null} if a value is not set.
      */
-    public Float getPitch() {
+    public @Nullable Float getPitch() {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
             try {
                 return mPlaybackParams.getPitch();
@@ -126,7 +128,7 @@
     /**
      * Returns the speed factor. {@code null} if a value is not set.
      */
-    public Float getSpeed() {
+    public @Nullable Float getSpeed() {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
             try {
                 return mPlaybackParams.getSpeed();
@@ -185,7 +187,7 @@
          *
          * @return this <code>Builder</code> instance.
          */
-        public Builder setAudioFallbackMode(@AudioFallbackMode int audioFallbackMode) {
+        public @NonNull Builder setAudioFallbackMode(@AudioFallbackMode int audioFallbackMode) {
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                 mPlaybackParams.setAudioFallbackMode(audioFallbackMode);
             } else {
@@ -200,7 +202,7 @@
          * @return this <code>Builder</code> instance.
          * @throws IllegalArgumentException if the pitch is negative.
          */
-        public Builder setPitch(float pitch) {
+        public @NonNull Builder setPitch(float pitch) {
             if (pitch < 0.f) {
                 throw new IllegalArgumentException("pitch must not be negative");
             }
@@ -217,7 +219,7 @@
          *
          * @return this <code>Builder</code> instance.
          */
-        public Builder setSpeed(float speed) {
+        public @NonNull Builder setSpeed(float speed) {
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                 mPlaybackParams.setSpeed(speed);
             } else {
@@ -230,7 +232,7 @@
          * Takes the values of the Builder object and creates a PlaybackParams2 object.
          * @return PlaybackParams2 object with values from the Builder.
          */
-        public PlaybackParams2 build() {
+        public @NonNull PlaybackParams2 build() {
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                 return new PlaybackParams2(mPlaybackParams);
             } else {
diff --git a/media2/src/main/java/androidx/media2/SessionCommand2.java b/media2/src/main/java/androidx/media2/SessionCommand2.java
index 25376d7..d5f66e8 100644
--- a/media2/src/main/java/androidx/media2/SessionCommand2.java
+++ b/media2/src/main/java/androidx/media2/SessionCommand2.java
@@ -81,7 +81,7 @@
     @IntDef({COMMAND_CODE_CUSTOM,
             COMMAND_CODE_PLAYER_PLAY,
             COMMAND_CODE_PLAYER_PAUSE,
-            COMMAND_CODE_PLAYER_PREFETCH,
+            COMMAND_CODE_PLAYER_PREPARE,
             COMMAND_CODE_PLAYER_SEEK_TO,
             COMMAND_CODE_PLAYER_SET_SPEED,
             COMMAND_CODE_PLAYER_GET_PLAYLIST,
@@ -102,12 +102,14 @@
             COMMAND_CODE_VOLUME_ADJUST_VOLUME,
             COMMAND_CODE_SESSION_FAST_FORWARD,
             COMMAND_CODE_SESSION_REWIND,
+            COMMAND_CODE_SESSION_SKIP_FORWARD,
+            COMMAND_CODE_SESSION_SKIP_BACKWARD,
             COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID,
             COMMAND_CODE_SESSION_PLAY_FROM_SEARCH,
             COMMAND_CODE_SESSION_PLAY_FROM_URI,
-            COMMAND_CODE_SESSION_PREFETCH_FROM_MEDIA_ID,
-            COMMAND_CODE_SESSION_PREFETCH_FROM_SEARCH,
-            COMMAND_CODE_SESSION_PREFETCH_FROM_URI,
+            COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID,
+            COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH,
+            COMMAND_CODE_SESSION_PREPARE_FROM_URI,
             COMMAND_CODE_SESSION_SET_RATING,
             COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO,
             COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO,
@@ -158,7 +160,7 @@
     public static final int COMMAND_CODE_PLAYER_PAUSE = 10001;
 
     /**
-     * Command code for {@link MediaController2#prefetch()}.
+     * Command code for {@link MediaController2#prepare()}.
      * <p>
      * Command would be sent directly to the player if the session doesn't reject the request
      * through the {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo,
@@ -166,7 +168,7 @@
      * <p>
      * Code version is {@link #COMMAND_VERSION_1}.
      */
-    public static final int COMMAND_CODE_PLAYER_PREFETCH = 10002;
+    public static final int COMMAND_CODE_PLAYER_PREPARE = 10002;
 
     /**
      * Command code for {@link MediaController2#seekTo(long)}.
@@ -210,7 +212,7 @@
     public static final int COMMAND_CODE_PLAYER_SET_PLAYLIST = 10006;
 
     /**
-     * Command code for {@link MediaController2#skipToPlaylistItem(MediaItem2)}.
+     * Command code for {@link MediaController2#skipToPlaylistItem(int)}.
      * <p>
      * Command would be sent directly to the player if the session doesn't reject the request
      * through the
@@ -274,7 +276,7 @@
     public static final int COMMAND_CODE_PLAYER_GET_PLAYLIST_METADATA = 10012;
 
     /**
-     * Command code for {@link MediaController2#addPlaylistItem(int, MediaItem2)}.
+     * Command code for {@link MediaController2#addPlaylistItem(int, String)}.
      * <p>
      * Command would be sent directly to the player if the session doesn't reject the request
      * through the
@@ -285,7 +287,7 @@
     public static final int COMMAND_CODE_PLAYER_ADD_PLAYLIST_ITEM = 10013;
 
     /**
-     * Command code for {@link MediaController2#addPlaylistItem(int, MediaItem2)}.
+     * Command code for {@link MediaController2#addPlaylistItem(int, String)}.
      * <p>
      * Command would be sent directly to the player if the session doesn't reject the request
      * through the
@@ -296,7 +298,7 @@
     public static final int COMMAND_CODE_PLAYER_REMOVE_PLAYLIST_ITEM = 10014;
 
     /**
-     * Command code for {@link MediaController2#replacePlaylistItem(int, MediaItem2)}.
+     * Command code for {@link MediaController2#replacePlaylistItem(int, String)}.
      * <p>
      * Command would be sent directly to the player if the session doesn't reject the request
      * through the
@@ -326,7 +328,7 @@
     public static final int COMMAND_CODE_PLAYER_UPDATE_LIST_METADATA = 10017;
 
     /**
-     * Command code for {@link MediaController2#setMediaItem(MediaItem2)}.
+     * Command code for {@link MediaController2#setMediaItem(String)}.
      * <p>
      * Command would be sent directly to the player if the session doesn't reject the request
      * through the
@@ -403,13 +405,27 @@
     public static final int COMMAND_CODE_SESSION_REWIND = 40001;
 
     /**
+     * Command code for {@link MediaController2#skipForward()}.
+     * <p>
+     * Code version is {@link #COMMAND_VERSION_1}.
+     */
+    public static final int COMMAND_CODE_SESSION_SKIP_FORWARD = 40002;
+
+    /**
+     * Command code for {@link MediaController2#skipBackward()}.
+     * <p>
+     * Code version is {@link #COMMAND_VERSION_1}.
+     */
+    public static final int COMMAND_CODE_SESSION_SKIP_BACKWARD = 40003;
+
+    /**
      * Command code for {@link MediaController2#playFromMediaId(String, Bundle)}.
      * <p>
      * Code version is {@link #COMMAND_VERSION_1}.
      * @hide
      */
     @RestrictTo(LIBRARY_GROUP)
-    public static final int COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID = 40002;
+    public static final int COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID = 40004;
 
     /**
      * Command code for {@link MediaController2#playFromSearch(String, Bundle)}.
@@ -418,7 +434,7 @@
      * @hide
      */
     @RestrictTo(LIBRARY_GROUP)
-    public static final int COMMAND_CODE_SESSION_PLAY_FROM_SEARCH = 40003;
+    public static final int COMMAND_CODE_SESSION_PLAY_FROM_SEARCH = 40005;
 
     /**
      * Command code for {@link MediaController2#playFromUri(Uri, Bundle)}.
@@ -427,41 +443,41 @@
      * @hide
      */
     @RestrictTo(LIBRARY_GROUP)
-    public static final int COMMAND_CODE_SESSION_PLAY_FROM_URI = 40004;
+    public static final int COMMAND_CODE_SESSION_PLAY_FROM_URI = 40006;
 
     /**
-     * Command code for {@link MediaController2#prefetchFromMediaId(String, Bundle)}.
+     * Command code for {@link MediaController2#prepareFromMediaId(String, Bundle)}.
      * <p>
      * Code version is {@link #COMMAND_VERSION_1}.
      * @hide
      */
     @RestrictTo(LIBRARY_GROUP)
-    public static final int COMMAND_CODE_SESSION_PREFETCH_FROM_MEDIA_ID = 40005;
+    public static final int COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID = 40007;
 
     /**
-     * Command code for {@link MediaController2#prefetchFromSearch(String, Bundle)}.
+     * Command code for {@link MediaController2#prepareFromSearch(String, Bundle)}.
      * <p>
      * Code version is {@link #COMMAND_VERSION_1}.
      * @hide
      */
     @RestrictTo(LIBRARY_GROUP)
-    public static final int COMMAND_CODE_SESSION_PREFETCH_FROM_SEARCH = 40006;
+    public static final int COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH = 40008;
 
     /**
-     * Command code for {@link MediaController2#prefetchFromUri(Uri, Bundle)}.
+     * Command code for {@link MediaController2#prepareFromUri(Uri, Bundle)}.
      * <p>
      * Code version is {@link #COMMAND_VERSION_1}.
      * @hide
      */
     @RestrictTo(LIBRARY_GROUP)
-    public static final int COMMAND_CODE_SESSION_PREFETCH_FROM_URI = 40007;
+    public static final int COMMAND_CODE_SESSION_PREPARE_FROM_URI = 40009;
 
     /**
      * Command code for {@link MediaController2#setRating(String, Rating2)}.
      * <p>
      * Code version is {@link #COMMAND_VERSION_1}.
      */
-    public static final int COMMAND_CODE_SESSION_SET_RATING = 40008;
+    public static final int COMMAND_CODE_SESSION_SET_RATING = 40010;
 
     /**
      * Command code for {@link MediaController2#subscribeRoutesInfo()}
@@ -470,7 +486,7 @@
      * @hide
      */
     @RestrictTo(LIBRARY_GROUP)
-    public static final int COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO = 40009;
+    public static final int COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO = 40011;
 
     /**
      * Command code for {@link MediaController2#unsubscribeRoutesInfo()}
@@ -479,7 +495,7 @@
      * @hide
      */
     @RestrictTo(LIBRARY_GROUP)
-    public static final int COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO = 40010;
+    public static final int COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO = 40012;
 
     /**
      * Command code for {@link MediaController2#selectRoute(Bundle)}}
@@ -488,7 +504,7 @@
      * @hide
      */
     @RestrictTo(LIBRARY_GROUP)
-    public static final int COMMAND_CODE_SESSION_SELECT_ROUTE = 40011;
+    public static final int COMMAND_CODE_SESSION_SELECT_ROUTE = 40013;
 
     static {
         VERSION_SESSION_COMMANDS_MAP.put(COMMAND_VERSION_1,
diff --git a/media2/src/main/java/androidx/media2/SessionCommandGroup2.java b/media2/src/main/java/androidx/media2/SessionCommandGroup2.java
index d9b04b63..2e67ae1 100644
--- a/media2/src/main/java/androidx/media2/SessionCommandGroup2.java
+++ b/media2/src/main/java/androidx/media2/SessionCommandGroup2.java
@@ -196,7 +196,7 @@
          * {@link SessionCommandGroup2} object.
          * @param commandGroup
          */
-        public Builder(SessionCommandGroup2 commandGroup) {
+        public Builder(@NonNull SessionCommandGroup2 commandGroup) {
             mCommands = commandGroup.getCommands();
         }
 
@@ -205,7 +205,7 @@
          *
          * @param command A command to add. Shouldn't be {@code null}.
          */
-        public @NonNull Builder addCommand(SessionCommand2 command) {
+        public @NonNull Builder addCommand(@NonNull SessionCommand2 command) {
             if (command == null) {
                 throw new IllegalArgumentException("command shouldn't be null");
             }
@@ -239,7 +239,7 @@
          *
          * @param version command version
          * @see SessionCommand2#COMMAND_VERSION_1
-         * @see MediaSession2.SessionCallback#onConnect
+         * @see MediaSession2.SessionCallback#onConnect(MediaSession2, MediaSession2.ControllerInfo)
          */
         public @NonNull Builder addAllPredefinedCommands(@CommandVersion int version) {
             if (version != COMMAND_VERSION_1) {
diff --git a/media2/src/main/java/androidx/media2/SessionPlayer2.java b/media2/src/main/java/androidx/media2/SessionPlayer2.java
index cb14cbb..fbef7c4 100644
--- a/media2/src/main/java/androidx/media2/SessionPlayer2.java
+++ b/media2/src/main/java/androidx/media2/SessionPlayer2.java
@@ -43,15 +43,39 @@
 /**
  * Base interface for all media players that want media session.
  * <p>
- * Methods here may be the asynchronous calls depending on the implementation. Wait with returned
- * {@link ListenableFuture} or callback for the completion.
+ * APIs that return {@link ListenableFuture} should be the asynchronous calls and shouldn't block
+ * the calling thread. This guarantees the APIs are safe to be called on the main thread.
  *
  * <p>Topics covered here are:
  * <ol>
+ * <li><a href="#BestPractices">Best practices</a>
  * <li><a href="#PlayerStates">Player states</a>
- * <li><a href="#Invalid_States">Invalid method calls</a>
+ * <li><a href="#InvalidStates">Invalid method calls</a>
  * </ol>
  *
+ * <h3 id="BestPractices">Best practices</h3>
+ *
+ * Here are best practices when implementing/using SessionPlayer2:
+ *
+ * <ul>
+ * <li>When updating UI, you should respond to {@link PlayerCallback} invocations instead of
+ * {@link PlayerResult} objects since the player can be controlled by others.
+ * <li>When a SessionPlayer2 object is no longer being used, call {@link #close()} as soon as
+ * possible to release the resources used by the internal player engine associated with the
+ * SessionPlayer2. For example, if a player uses hardware decoder, other player instances may
+ * fallback to software decoders or fail to play. You cannot use SessionPlayer2 instance after
+ * you call {@link #close()}. There is no way to reuse the instance.
+ * <li>The current playback position can be retrieved with a call to {@link #getCurrentPosition()},
+ * which is helpful for applications such as a music player that need to keep track of the playback
+ * progress.
+ * <li>The playback position can be adjusted with a call to {@link #seekTo(long)}. Although the
+ * asynchronous {@link #seekTo} call returns right away, the actual seek operation may take a
+ * while to finish, especially for audio/video being streamed.
+ * <li>You can call {@link #seekTo(long)} from the {@link #PLAYER_STATE_PAUSED}. In these cases, if
+ * you are playing a video stream and the requested position is valid, one video frame may be
+ * displayed.
+ * </ul>
+ *
  * <h3 id="PlayerStates">Player states</h3>
  * The playback control of audio/video files is managed as a state machine. The SessionPlayer2
  * defines four states:
@@ -62,10 +86,7 @@
  *         {@link #setPlaylist(List, MediaMetadata2)}. Check returned {@link ListenableFuture} for
  *         potential error.
  *         <p>
- *         Calling {@link #prepare()} transfers this object to {@link #PLAYER_STATE_PAUSED}. Note
- *         that {@link #prepare()} is asynchronous, so wait for the returned
- *         {@link ListenableFuture} or
- *         {@link PlayerCallback#onPlayerStateChanged(SessionPlayer2, int)}.
+ *         Calling {@link #prepare()} transfers this object to {@link #PLAYER_STATE_PAUSED}.
  *
  *     <li>{@link #PLAYER_STATE_PAUSED}: State when the audio/video playback is paused.
  *         <p>
@@ -91,34 +112,12 @@
  *         In general, playback might fail due to various reasons such as unsupported audio/video
  *         format, poorly interleaved audio/video, resolution too high, streaming timeout, and
  *         others. In addition, due to programming errors, a playback control operation might be
- *         performed from an <a href="#invalid_state">invalid state</a>. In these cases the player
+ *         performed from an <a href="#InvalidStates">invalid state</a>. In these cases the player
  *         may transition to this state.
  * </ol>
  * <p>
  *
- * Here are best practices when implementing/using SessionPlayer2:
- *
- * <ul>
- * <li>When updating UI, you should respond to {@link PlayerCallback} invocations instead of
- * {@link PlayerResult} objects since the player can be controlled by others.
- * <li>When a SessionPlayer2 object is no longer being used, call {@link #close()} as soon as
- * possible to release the resources used by the internal player engine associated with the
- * SessionPlayer2. Failure to call {@link #close()} may cause subsequent instances of SessionPlayer2
- * objects to fallback to software implementations or fail altogether. You cannot use SessionPlayer2
- * after you call {@link #close()}. There is no way to bring it back to any other state.
- * <li>The current playback position can be retrieved with a call to {@link #getCurrentPosition()},
- * which is helpful for applications such as a Music player that need to keep track of the playback
- * progress.
- * <li>The playback position can be adjusted with a call to {@link #seekTo(long)}. Although the
- * asynchronous {@link #seekTo} call returns right away, the actual seek operation may take a
- * while to finish, especially for audio/video being streamed. Wait for the return value or
- * {@link PlayerCallback#onSeekCompleted(SessionPlayer2, long)}.
- * <li>You can call {@link #seekTo(long)} from the {@link #PLAYER_STATE_PAUSED}. In these cases, if
- * you are playing a video stream and the requested position is valid, one video frame may be
- * displayed.
- * </ul>
- *
- * <h3 id="Invalid_States">Invalid method calls</h3>
+ * <h3 id="InvalidStates">Invalid method calls</h3>
  * The only method you safely call from the {@link #PLAYER_STATE_ERROR} is {@link #close()}.
  * Any other methods might throw an exception or return meaningless data.
  * <p>
@@ -141,7 +140,7 @@
  * </table>
  */
 // Previously MediaSessionCompat.Callback.
-// Players can extend this directly (e.g. XMediaPlayer) or create wrapper and control underlying
+// Players can extend this directly (e.g. MediaPlayer) or create wrapper and control underlying
 // player.
 // Preferably it can be interface, but API guideline requires to use abstract class.
 @TargetApi(Build.VERSION_CODES.P)
@@ -284,34 +283,22 @@
 
     /**
      * Plays the playback.
-     * <p>
-     * This may be the asynchronous call depending on the implementation. Wait with returned
-     * {@link ListenableFuture} or callback for the completion.
      */
     public abstract @NonNull ListenableFuture<PlayerResult> play();
 
     /**
      * Pauses playback.
-     * <p>
-     * This may be the asynchronous call depending on the implementation. Wait with returned
-     * {@link ListenableFuture} or callback for the completion.
      */
     public abstract @NonNull ListenableFuture<PlayerResult> pause();
 
     /**
      * Prepares the media items for playback. During this time, the player may allocate resources
      * required to play, such as audio and video decoders.
-     * <p>
-     * This may be the asynchronous call depending on the implementation. Wait with returned
-     * {@link ListenableFuture} or callback for the completion.
      */
     public abstract @NonNull ListenableFuture<PlayerResult> prepare();
 
     /**
      * Seeks to the specified position. Moves the playback head to the specified position.
-     * <p>
-     * This may be the asynchronous call depending on the implementation. Wait with returned
-     * {@link ListenableFuture} or callback for the completion.
      *
      * @param position the new playback position in ms. The value should be in the range of start
      * and end positions defined in {@link MediaItem2}.
@@ -323,9 +310,6 @@
      * <p>
      * After changing the playback speed, it is recommended to query the actual speed supported
      * by the player, see {@link #getPlaybackSpeed()}.
-     * <p>
-     * This may be the asynchronous call depending on the implementation. Wait with returned
-     * {@link ListenableFuture} or callback for the completion.
      *
      * @param playbackSpeed playback speed
      */
@@ -336,9 +320,6 @@
      * <p>
      * You must call this method in {@link #PLAYER_STATE_IDLE} in order for the audio attributes to
      * become effective thereafter.
-     * <p>
-     * This may be the asynchronous call depending on the implementation. Wait with returned
-     * {@link ListenableFuture} or callback for the completion.
      *
      * @param attributes non-null <code>AudioAttributes</code>.
      */
@@ -347,9 +328,6 @@
 
     /**
      * Gets the current player state.
-     * <p>
-     * This may be the asynchronous call depending on the implementation. Wait with returned
-     * {@link ListenableFuture} or callback for the completion.
      *
      * @return the current player state
      * @see PlayerCallback#onPlayerStateChanged(SessionPlayer2, int)
@@ -401,10 +379,8 @@
 
     /**
      * Sets a list of {@link MediaItem2} with metadata. Ensure uniqueness of each {@link MediaItem2}
-     * in the playlist so the session can uniquely identity individual items.
-     * <p>
-     * This may be the asynchronous call depending on the implementation. Wait with returned
-     * {@link ListenableFuture} or callback for the completion.
+     * in the playlist so the session can uniquely identity individual items. All
+     * {@link MediaItem2}s shouldn't be {@code null} as well.
      * <p>
      * It's recommended to fill {@link MediaMetadata2} in each {@link MediaItem2} especially for the
      * duration information with the key {@link MediaMetadata2#METADATA_KEY_DURATION}. Without the
@@ -432,9 +408,6 @@
     /**
      * Sets a {@link MediaItem2} for playback.
      * <p>
-     * This may be the asynchronous call depending on the implementation. Wait with returned
-     * {@link ListenableFuture} or callback for the completion.
-     * <p>
      * It's recommended to fill {@link MediaMetadata2} in each {@link MediaItem2} especially for the
      * duration information with the key {@link MediaMetadata2#METADATA_KEY_DURATION}. Without the
      * duration information in the metadata, session will do extra work to get the duration and send
@@ -456,9 +429,6 @@
      * the current playlist size (e.g. {@link Integer#MAX_VALUE}) will add the item at the end of
      * the playlist.
      * <p>
-     * This may be the asynchronous call depending on the implementation. Wait with returned
-     * {@link ListenableFuture} or callback for the completion.
-     * <p>
      * The implementation may not change the currently playing media item.
      * If index is less than or equal to the current index of the playlist,
      * the current index of the playlist will be increased correspondingly.
@@ -477,9 +447,6 @@
     /**
      * Removes the media item from the playlist
      * <p>
-     * This may be the asynchronous call depending on the implementation. Wait with returned
-     * {@link ListenableFuture} or callback for the completion.
-     * <p>
      * The implementation may not change the currently playing media item even when it's removed.
      * <p>
      * The implementation must notify registered callbacks with
@@ -497,9 +464,6 @@
      * Replaces the media item at index in the playlist. This can be also used to update metadata of
      * an item.
      * <p>
-     * This may be the asynchronous call depending on the implementation. Wait with returned
-     * {@link ListenableFuture} or callback for the completion.
-     * <p>
      * The implementation must notify registered callbacks with
      * {@link PlayerCallback#onPlaylistChanged(SessionPlayer2, List, MediaMetadata2)} when it's
      * completed.
@@ -514,9 +478,6 @@
     /**
      * Skips to the previous item in the playlist.
      * <p>
-     * This may be the asynchronous call depending on the implementation. Wait with returned
-     * {@link ListenableFuture} or callback for the completion.
-     * <p>
      * The implementation must notify registered callbacks with
      * {@link PlayerCallback#onCurrentMediaItemChanged(SessionPlayer2, MediaItem2)} when it's
      * completed.
@@ -528,9 +489,6 @@
     /**
      * Skips to the next item in the playlist.
      * <p>
-     * This may be the asynchronous call depending on the implementation. Wait with returned
-     * {@link ListenableFuture} or callback for the completion.
-     * <p>
      * The implementation must notify registered callbacks with
      * {@link PlayerCallback#onCurrentMediaItemChanged(SessionPlayer2, MediaItem2)} when it's
      * completed.
@@ -542,9 +500,6 @@
     /**
      * Skips to the the media item.
      * <p>
-     * This may be the asynchronous call depending on the implementation. Wait with returned
-     * {@link ListenableFuture} or callback for the completion.
-     * <p>
      * The implementation must notify registered callbacks with
      * {@link PlayerCallback#onCurrentMediaItemChanged(SessionPlayer2, MediaItem2)} when it's
      * completed.
@@ -558,9 +513,6 @@
     /**
      * Updates the playlist metadata while keeping the playlist as-is.
      * <p>
-     * This may be the asynchronous call depending on the implementation. Wait with returned
-     * {@link ListenableFuture} or callback for the completion.
-     * <p>
      * The implementation must notify registered callbacks with
      * {@link PlayerCallback#onPlaylistMetadataChanged(SessionPlayer2, MediaMetadata2)} when it's
      * completed.
@@ -574,9 +526,6 @@
     /**
      * Sets the repeat mode.
      * <p>
-     * This may be the asynchronous call depending on the implementation. Wait with returned
-     * {@link ListenableFuture} or callback for the completion.
-     * <p>
      * The implementation must notify registered callbacks with
      * {@link PlayerCallback#onRepeatModeChanged(SessionPlayer2, int)} when it's completed.
      *
@@ -593,9 +542,6 @@
     /**
      * Sets the shuffle mode.
      * <p>
-     * This may be the asynchronous call depending on the implementation. Wait with returned
-     * {@link ListenableFuture} or callback for the completion.
-     * <p>
      * The implementation must notify registered callbacks with
      * {@link PlayerCallback#onShuffleModeChanged(SessionPlayer2, int)} when it's completed.
      *
@@ -776,8 +722,8 @@
          * @see #getPlaylist()
          * @see #getPlaylistMetadata()
          */
-        public void onPlaylistChanged(@NonNull SessionPlayer2 player, List<MediaItem2> list,
-                @Nullable MediaMetadata2 metadata) {
+        public void onPlaylistChanged(@NonNull SessionPlayer2 player,
+                @Nullable List<MediaItem2> list, @Nullable MediaMetadata2 metadata) {
         }
 
         /**
diff --git a/media2/src/main/java/androidx/media2/SubtitleData2.java b/media2/src/main/java/androidx/media2/SubtitleData2.java
index 3ff4fc8..e3f0df2 100644
--- a/media2/src/main/java/androidx/media2/SubtitleData2.java
+++ b/media2/src/main/java/androidx/media2/SubtitleData2.java
@@ -28,7 +28,7 @@
 
 /**
  * Class encapsulating subtitle data, as received through the
- * {@link XMediaPlayer.PlayerCallback#onSubtitleData} interface.
+ * {@link MediaPlayer.PlayerCallback#onSubtitleData} interface.
  * The subtitle data includes:
  * <ul>
  * <li> the track index</li>
@@ -38,15 +38,15 @@
  * </ul>
  * The data is stored in a byte-array, and is encoded in one of the supported in-band
  * subtitle formats. The subtitle encoding is determined by the MIME type of the
- * {@link XMediaPlayer.TrackInfo} of the subtitle track, one of
+ * {@link MediaPlayer.TrackInfo} of the subtitle track, one of
  * {@link #MIMETYPE_TEXT_CEA_608}, {@link #MIMETYPE_TEXT_CEA_708},
  * {@link #MIMETYPE_TEXT_VTT}.
  * <p>
- * Here is an example of iterating over the tracks of a {@link XMediaPlayer}, and checking which
+ * Here is an example of iterating over the tracks of a {@link MediaPlayer}, and checking which
  * encoding is used for the subtitle tracks:
  * <p>
  * <pre class="prettyprint">
- * XMediaPlayer mp = new XMediaPlayer(context);
+ * MediaPlayer mp = new MediaPlayer(context);
  * // prepare the player with a valid media item.
  * &hellip;
  *
@@ -65,8 +65,8 @@
  * }
  * </pre>
  * <p>
- * @see XMediaPlayer#registerPlayerCallback(Executor, SessionPlayer2.PlayerCallback)
- * @see XMediaPlayer.PlayerCallback#onSubtitleData(XMediaPlayer, MediaItem2, SubtitleData2)
+ * @see MediaPlayer#registerPlayerCallback(Executor, SessionPlayer2.PlayerCallback)
+ * @see MediaPlayer.PlayerCallback#onSubtitleData(MediaPlayer, MediaItem2, SubtitleData2)
  */
 public final class SubtitleData2 {
     private static final String TAG = "SubtitleData2";
@@ -112,7 +112,7 @@
 
     /**
      * Returns the index of the MediaPlayer track which contains this subtitle data.
-     * @return an index in the array returned by {@link XMediaPlayer#getTrackInfo()}.
+     * @return an index in the array returned by {@link MediaPlayer#getTrackInfo()}.
      */
     public int getTrackIndex() {
         return mTrackIndex;
diff --git a/media2/src/main/java/androidx/media2/TimedMetaData2.java b/media2/src/main/java/androidx/media2/TimedMetaData2.java
index 62b9811..5421511 100644
--- a/media2/src/main/java/androidx/media2/TimedMetaData2.java
+++ b/media2/src/main/java/androidx/media2/TimedMetaData2.java
@@ -32,7 +32,7 @@
  * <li> raw uninterpreted byte-array extracted directly from the container. </li>
  * </ul>
  *
- * @see XMediaPlayer.PlayerCallback#onTimedMetaDataAvailable
+ * @see MediaPlayer.PlayerCallback#onTimedMetaDataAvailable
  */
 public class TimedMetaData2 {
     private static final String TAG = "TimedMetaData";
diff --git a/media2/src/main/java/androidx/media2/UriMediaItem2.java b/media2/src/main/java/androidx/media2/UriMediaItem2.java
index f67205a..3c364e1 100644
--- a/media2/src/main/java/androidx/media2/UriMediaItem2.java
+++ b/media2/src/main/java/androidx/media2/UriMediaItem2.java
@@ -23,6 +23,7 @@
 import androidx.annotation.Nullable;
 import androidx.core.util.Preconditions;
 import androidx.versionedparcelable.NonParcelField;
+import androidx.versionedparcelable.ParcelUtils;
 import androidx.versionedparcelable.VersionedParcelize;
 
 import java.net.CookieHandler;
@@ -37,10 +38,13 @@
  * Structure for media item descriptor for {@link Uri}.
  * <p>
  * Users should use {@link Builder} to create {@link UriMediaItem2}.
+ * <p>
+ * You cannot directly send this object across the process through {@link ParcelUtils}. See
+ * {@link MediaItem2} for detail.
  *
  * @see MediaItem2
  */
-@VersionedParcelize
+@VersionedParcelize(isCustom = true)
 public class UriMediaItem2 extends MediaItem2 {
     @NonParcelField
     @SuppressWarnings("WeakerAccess") /* synthetic access */
@@ -153,7 +157,6 @@
          */
         public Builder(@NonNull Context context, @NonNull Uri uri,
                 @Nullable Map<String, String> headers, @Nullable List<HttpCookie> cookies) {
-            super(FLAG_PLAYABLE);
             Preconditions.checkNotNull(context, "context cannot be null");
             Preconditions.checkNotNull(uri, "uri cannot be null");
             mUri = uri;
@@ -181,7 +184,7 @@
          * @return A new UriMediaItem2 with values supplied by the Builder.
          */
         @Override
-        public UriMediaItem2 build() {
+        public @NonNull UriMediaItem2 build() {
             return new UriMediaItem2(this);
         }
     }
diff --git a/media2/src/main/java/androidx/media2/exoplayer/ExoPlayerWrapper.java b/media2/src/main/java/androidx/media2/exoplayer/ExoPlayerWrapper.java
index 411c7a5..9e7f850 100644
--- a/media2/src/main/java/androidx/media2/exoplayer/ExoPlayerWrapper.java
+++ b/media2/src/main/java/androidx/media2/exoplayer/ExoPlayerWrapper.java
@@ -70,7 +70,9 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
  * Wraps an ExoPlayer instance and provides methods and notifies events like those in the
@@ -248,8 +250,8 @@
         // groups.
         switch (state) {
             case Player.STATE_IDLE:
-            case Player.STATE_ENDED:
                 return MediaPlayer2.PLAYER_STATE_IDLE;
+            case Player.STATE_ENDED:
             case Player.STATE_BUFFERING:
                 return MediaPlayer2.PLAYER_STATE_PAUSED;
             case Player.STATE_READY:
@@ -669,17 +671,42 @@
             mFileDescriptor = fileDescriptor;
         }
 
-        public void close() {
-            try {
-                if (mFileDescriptor != null) {
-                    FileDescriptorUtil.close(mFileDescriptor);
-                } else if (mMediaItem instanceof CallbackMediaItem2) {
-                    ((CallbackMediaItem2) mMediaItem).getDataSourceCallback2().close();
-                }
-            } catch (IOException e) {
-                Log.w(TAG, "Error closing media item " + mMediaItem, e);
+    }
+
+    private static final class FileDescriptorRegistry {
+
+        private static final class Entry {
+            public final Object mLock;
+
+            public int mMediaItemCount;
+
+            Entry() {
+                mLock = new Object();
             }
         }
+
+        private final Map<FileDescriptor, Entry> mEntries;
+
+        FileDescriptorRegistry() {
+            mEntries = new HashMap<>();
+        }
+
+        public Object registerMediaItemAndGetLock(FileDescriptor fileDescriptor) {
+            if (!mEntries.containsKey(fileDescriptor)) {
+                mEntries.put(fileDescriptor, new Entry());
+            }
+            Entry entry = Preconditions.checkNotNull(mEntries.get(fileDescriptor));
+            entry.mMediaItemCount++;
+            return entry.mLock;
+        }
+
+        public void unregisterMediaItem(FileDescriptor fileDescriptor) {
+            Entry entry = Preconditions.checkNotNull(mEntries.get(fileDescriptor));
+            if (--entry.mMediaItemCount == 0) {
+                mEntries.remove(fileDescriptor);
+            }
+        }
+
     }
 
     private static final class MediaItemQueue {
@@ -689,6 +716,7 @@
         private final DataSource.Factory mDataSourceFactory;
         private final ConcatenatingMediaSource mConcatenatingMediaSource;
         private final ArrayDeque<MediaItemInfo> mMediaItemInfos;
+        private final FileDescriptorRegistry mFileDescriptorRegistry;
 
         private long mStartPlayingTimeNs;
         private long mCurrentMediaItemPlayingTimeUs;
@@ -700,12 +728,13 @@
             mDataSourceFactory = new DefaultDataSourceFactory(context, userAgent);
             mConcatenatingMediaSource = new ConcatenatingMediaSource();
             mMediaItemInfos = new ArrayDeque<>();
+            mFileDescriptorRegistry = new FileDescriptorRegistry();
             mStartPlayingTimeNs = -1;
         }
 
         public void clear() {
             while (!mMediaItemInfos.isEmpty()) {
-                mMediaItemInfos.remove().close();
+                releaseMediaItem(mMediaItemInfos.remove());
             }
         }
 
@@ -725,7 +754,7 @@
                 mConcatenatingMediaSource.removeMediaSourceRange(
                         /* fromIndex= */ 1, /* toIndex= */ size);
                 while (mMediaItemInfos.size() > 1) {
-                    mMediaItemInfos.removeLast().close();
+                    releaseMediaItem(mMediaItemInfos.removeLast());
                 }
             }
 
@@ -736,7 +765,10 @@
                     return;
                 }
                 try {
-                    appendMediaItem(mediaItem2, mDataSourceFactory, mMediaItemInfos, mediaSources);
+                    appendMediaItem(
+                            mediaItem2,
+                            mMediaItemInfos,
+                            mediaSources);
                 } catch (IOException e) {
                     mListener.onError(mediaItem2, MEDIA_ERROR_UNKNOWN);
                 }
@@ -769,7 +801,7 @@
 
         public void skipToNext() {
             // TODO(b/68398926): Play the start position of the next media item.
-            mMediaItemInfos.removeFirst().close();
+            releaseMediaItem(mMediaItemInfos.removeFirst());
             mConcatenatingMediaSource.removeMediaSource(0);
         }
 
@@ -807,7 +839,7 @@
                     mListener.onMediaItem2Ended(getCurrentMediaItem());
                 }
                 for (int i = 0; i < windowIndex; i++) {
-                    mMediaItemInfos.removeFirst().close();
+                    releaseMediaItem(mMediaItemInfos.removeFirst());
                 }
                 if (isPeriodTransition) {
                     mListener.onMediaItem2StartedAsNext(getCurrentMediaItem());
@@ -825,20 +857,23 @@
          * Appends a media source and associated information for the given media item to the
          * collections provided.
          */
-        private static void appendMediaItem(
+        private void appendMediaItem(
                 MediaItem2 mediaItem2,
-                DataSource.Factory dataSourceFactory,
                 Collection<MediaItemInfo> mediaItemInfos,
                 Collection<MediaSource> mediaSources) throws IOException {
+            DataSource.Factory dataSourceFactory = mDataSourceFactory;
             // Create a data source for reading from the file descriptor, if needed.
             FileDescriptor fileDescriptor = null;
             if (mediaItem2 instanceof FileMediaItem2) {
                 FileMediaItem2 fileMediaItem2 = (FileMediaItem2) mediaItem2;
+                // TODO(b/68398926): Remove dup'ing the file descriptor once FileMediaItem2 does it.
+                Object lock = mFileDescriptorRegistry.registerMediaItemAndGetLock(
+                        fileMediaItem2.getFileDescriptor());
                 fileDescriptor = FileDescriptorUtil.dup(fileMediaItem2.getFileDescriptor());
                 long offset = fileMediaItem2.getFileDescriptorOffset();
                 long length = fileMediaItem2.getFileDescriptorLength();
                 dataSourceFactory =
-                        FileDescriptorDataSource.getFactory(fileDescriptor, offset, length);
+                        FileDescriptorDataSource.getFactory(fileDescriptor, offset, length, lock);
             }
 
             // Create a source for the item.
@@ -852,10 +887,14 @@
             long endPosition = mediaItem2.getEndPosition();
             if (startPosition != 0L || endPosition != MediaItem2.POSITION_UNKNOWN) {
                 durationProvidingMediaSource = new DurationProvidingMediaSource(mediaSource);
+                // Disable the initial discontinuity to give seamless transitions to clips.
                 mediaSource = new ClippingMediaSource(
                         durationProvidingMediaSource,
                         C.msToUs(startPosition),
-                        C.msToUs(endPosition));
+                        C.msToUs(endPosition),
+                        /* enableInitialDiscontinuity= */ false,
+                        /* allowDynamicClippingUpdates= */ false,
+                        /* relativeToDefaultPosition= */ true);
             }
 
             mediaSources.add(mediaSource);
@@ -863,6 +902,24 @@
                     new MediaItemInfo(mediaItem2, durationProvidingMediaSource, fileDescriptor));
         }
 
+        private void releaseMediaItem(MediaItemInfo mediaItemInfo) {
+            MediaItem2 mediaItem = mediaItemInfo.mMediaItem;
+            try {
+                if (mediaItem instanceof FileMediaItem2) {
+                    FileDescriptorUtil.close(mediaItemInfo.mFileDescriptor);
+                    // TODO(b/68398926): Remove separate file descriptors once FileMediaItem2 dup's.
+                    FileDescriptor fileDescriptor =
+                            ((FileMediaItem2) mediaItem).getFileDescriptor();
+                    mFileDescriptorRegistry.unregisterMediaItem(fileDescriptor);
+                } else if (mediaItem instanceof CallbackMediaItem2) {
+                    ((CallbackMediaItem2) mediaItemInfo.mMediaItem)
+                            .getDataSourceCallback2().close();
+                }
+            } catch (IOException e) {
+                Log.w(TAG, "Error releasing media item " + mediaItem, e);
+            }
+        }
+
     }
 
 }
diff --git a/media2/src/main/java/androidx/media2/exoplayer/FileDescriptorDataSource.java b/media2/src/main/java/androidx/media2/exoplayer/FileDescriptorDataSource.java
index f73b92e..aab490d 100644
--- a/media2/src/main/java/androidx/media2/exoplayer/FileDescriptorDataSource.java
+++ b/media2/src/main/java/androidx/media2/exoplayer/FileDescriptorDataSource.java
@@ -53,43 +53,48 @@
      * @param fileDescriptor The file descriptor to read from.
      * @param offset The start offset in the file descriptor.
      * @param length The length of the range to read from the file descriptor.
+     * @param lock An object to synchronize on when using the file descriptor.
      * @return A factory for data sources that read from the file descriptor.
      */
     static DataSource.Factory getFactory(
-            final FileDescriptor fileDescriptor, final long offset, final long length) {
+            final FileDescriptor fileDescriptor,
+            final long offset,
+            final long length,
+            final Object lock) {
         return new DataSource.Factory() {
             @Override
             public DataSource createDataSource() {
-                return new FileDescriptorDataSource(fileDescriptor, offset, length);
+                return new FileDescriptorDataSource(fileDescriptor, offset, length, lock);
             }
         };
     }
 
     // TODO(b/80232248): Move into core ExoPlayer library and delete this class.
 
-    private final FileDescriptor mFactoryFileDescriptor;
+    private final FileDescriptor mFileDescriptor;
     private final long mOffset;
     private final long mLength;
+    private final Object mLock;
 
-    private @Nullable FileDescriptor mFileDescriptor;
     private @Nullable Uri mUri;
     private @Nullable InputStream mInputStream;
     private long mBytesRemaining;
     private boolean mOpened;
+    private long mPosition;
 
-    public FileDescriptorDataSource(FileDescriptor fileDescriptor, long offset, long length) {
+    FileDescriptorDataSource(
+            FileDescriptor fileDescriptor, long offset, long length, Object lock) {
         super(/* isNetwork= */ false);
-        mFactoryFileDescriptor = fileDescriptor;
+        mFileDescriptor = fileDescriptor;
         mOffset = offset;
         mLength = length;
+        mLock = lock;
     }
 
     @Override
-    public long open(DataSpec dataSpec) throws IOException {
+    public long open(DataSpec dataSpec) {
         mUri = dataSpec.uri;
         transferInitializing(dataSpec);
-        mFileDescriptor = FileDescriptorUtil.dup(mFactoryFileDescriptor);
-        FileDescriptorUtil.seek(mFileDescriptor, mOffset + dataSpec.position);
         mInputStream = new FileInputStream(mFileDescriptor);
         if (dataSpec.length != C.LENGTH_UNSET) {
             mBytesRemaining = dataSpec.length;
@@ -98,14 +103,14 @@
         } else {
             mBytesRemaining = C.LENGTH_UNSET;
         }
+        mPosition = mOffset + dataSpec.position;
         mOpened = true;
         transferStarted(dataSpec);
         return mBytesRemaining;
     }
 
     @Override
-    public int read(byte[] buffer, int offset, int readLength)
-            throws IOException {
+    public int read(byte[] buffer, int offset, int readLength) throws IOException {
         if (readLength == 0) {
             return 0;
         } else if (mBytesRemaining == 0) {
@@ -113,12 +118,18 @@
         }
         int bytesToRead = mBytesRemaining == C.LENGTH_UNSET
                 ? readLength : (int) Math.min(mBytesRemaining, readLength);
-        int bytesRead = Preconditions.checkNotNull(mInputStream).read(buffer, offset, bytesToRead);
-        if (bytesRead == -1) {
-            if (mBytesRemaining != C.LENGTH_UNSET) {
-                throw new EOFException();
+        int bytesRead;
+        synchronized (mLock) {
+            // The file descriptor position is shared across all users, so seek before reading.
+            FileDescriptorUtil.seek(mFileDescriptor, mPosition);
+            bytesRead = Preconditions.checkNotNull(mInputStream).read(buffer, offset, bytesToRead);
+            if (bytesRead == -1) {
+                if (mBytesRemaining != C.LENGTH_UNSET) {
+                    throw new EOFException();
+                }
+                return C.RESULT_END_OF_INPUT;
             }
-            return C.RESULT_END_OF_INPUT;
+            mPosition += bytesRead;
         }
         if (mBytesRemaining != C.LENGTH_UNSET) {
             mBytesRemaining -= bytesRead;
@@ -129,7 +140,7 @@
 
     @Override
     public Uri getUri() {
-        return mUri;
+        return Preconditions.checkNotNull(mUri);
     }
 
     @Override
@@ -141,13 +152,9 @@
             }
         } finally {
             mInputStream = null;
-            try {
-                FileDescriptorUtil.close(mFileDescriptor);
-            } finally {
-                if (mOpened) {
-                    mOpened = false;
-                    transferEnded();
-                }
+            if (mOpened) {
+                mOpened = false;
+                transferEnded();
             }
         }
     }
diff --git a/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouterInitializationTest.java b/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouterInitializationTest.java
index f3426b5..9a6ff34 100644
--- a/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouterInitializationTest.java
+++ b/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouterInitializationTest.java
@@ -22,7 +22,9 @@
 import static org.junit.Assert.assertTrue;
 
 import android.content.Context;
+import android.os.Build;
 
+import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
@@ -37,6 +39,7 @@
     /**
      * This test checks weather MediaRouter is initialized well if an empty route exists
      */
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN)
     @Test
     @SmallTest
     public void testEmptyUserRoute() throws Exception {
diff --git a/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java b/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java
index 373fefd..ab2a3467 100644
--- a/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java
+++ b/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java
@@ -18,7 +18,9 @@
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
 
+import android.app.Activity;
 import android.app.ActivityManager;
+import android.app.Application;
 import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -2132,6 +2134,7 @@
                 }
             }
         };
+        private ForegroundChecker mForegroundChecker;
 
         GlobalMediaRouter(Context applicationContext) {
             mApplicationContext = applicationContext;
@@ -2144,6 +2147,10 @@
             // the framework media router.  This one is special and receives
             // synchronization messages from the media router.
             mSystemProvider = SystemMediaRouteProvider.obtain(applicationContext, this);
+
+            mForegroundChecker = new ForegroundChecker();
+            Application app = (Application) mApplicationContext;
+            app.registerActivityLifecycleCallbacks(mForegroundChecker);
         }
 
         public void start() {
@@ -2341,6 +2348,7 @@
             // Combine all of the callback selectors and active scan flags.
             boolean discover = false;
             boolean activeScan = false;
+
             MediaRouteSelector.Builder builder = new MediaRouteSelector.Builder();
             for (int i = mRouters.size(); --i >= 0; ) {
                 MediaRouter router = mRouters.get(i).get();
@@ -2366,6 +2374,10 @@
                     }
                 }
             }
+            // When the app is in background, remove discovery request
+            if (!activeScan && !mForegroundChecker.isForeground()) {
+                discover = false;
+            }
             MediaRouteSelector selector = discover ? builder.build() : MediaRouteSelector.EMPTY;
 
             // Create a new discovery request.
@@ -3244,5 +3256,63 @@
                 }
             }
         }
+
+        private static final class ForegroundChecker
+                implements Application.ActivityLifecycleCallbacks {
+            // Use the same delay in ProcessLifecycleOwner
+            public static final long DELAY_MS = 700;
+            private final Handler mHandler = new Handler();
+            // Assume we are in foreground if we have no further information
+            private boolean mIsForeground = true;
+            private boolean mIsStopped = false;
+            private Runnable mChecker = new Runnable() {
+                @Override
+                public void run() {
+                    if (mIsForeground && mIsStopped) {
+                        mIsForeground = false;
+                        sGlobal.updateDiscoveryRequest();
+                    }
+                }
+            };
+
+            public boolean isForeground() {
+                return mIsForeground;
+            }
+
+            @Override
+            public void onActivityCreated(Activity activity, Bundle savedInstanceState) { }
+
+            @Override
+            public void onActivityStarted(Activity activity) {
+                mIsStopped = false;
+                boolean wasBackground = !mIsForeground;
+                mIsForeground = true;
+
+                mHandler.removeCallbacks(mChecker);
+                if (wasBackground) {
+                    sGlobal.updateDiscoveryRequest();
+                }
+            }
+
+            @Override
+            public void onActivityResumed(Activity activity) { }
+
+            @Override
+            public void onActivityPaused(Activity activity) { }
+
+            @Override
+            public void onActivityStopped(Activity activity) {
+                mIsStopped = true;
+                // By checking if onActivityStarted is not called after DELAY_MS
+                // we can note that the app goes to background
+                mHandler.postDelayed(mChecker, DELAY_MS);
+            }
+
+            @Override
+            public void onActivitySaveInstanceState(Activity activity, Bundle outState) { }
+
+            @Override
+            public void onActivityDestroyed(Activity activity) { }
+        }
     }
 }
diff --git a/paging/common/api/2.0.0.txt b/paging/common/api/2.0.0.txt
index 7428bf6..ecb3bc1 100644
--- a/paging/common/api/2.0.0.txt
+++ b/paging/common/api/2.0.0.txt
@@ -124,10 +124,8 @@
   }
 
   public static class PagedList.Config {
-    field public static final int MAX_SIZE_UNBOUNDED = 2147483647; // 0x7fffffff
     field public final boolean enablePlaceholders;
     field public final int initialLoadSizeHint;
-    field public final int maxSize;
     field public final int pageSize;
     field public final int prefetchDistance;
   }
@@ -137,7 +135,6 @@
     method public androidx.paging.PagedList.Config build();
     method public androidx.paging.PagedList.Config.Builder setEnablePlaceholders(boolean);
     method public androidx.paging.PagedList.Config.Builder setInitialLoadSizeHint(int);
-    method public androidx.paging.PagedList.Config.Builder setMaxSize(int);
     method public androidx.paging.PagedList.Config.Builder setPageSize(int);
     method public androidx.paging.PagedList.Config.Builder setPrefetchDistance(int);
   }
diff --git a/paging/common/api/2.1.0-beta01.txt b/paging/common/api/2.1.0-beta01.txt
new file mode 100644
index 0000000..c8e0be1
--- /dev/null
+++ b/paging/common/api/2.1.0-beta01.txt
@@ -0,0 +1,182 @@
+// Signature format: 2.0
+package androidx.paging {
+
+  public abstract class DataSource<Key, Value> {
+    method @AnyThread public void addInvalidatedCallback(androidx.paging.DataSource.InvalidatedCallback);
+    method @AnyThread public void invalidate();
+    method @WorkerThread public boolean isInvalid();
+    method public abstract <ToValue> androidx.paging.DataSource<Key,ToValue> map(androidx.arch.core.util.Function<Value,ToValue>);
+    method public abstract <ToValue> androidx.paging.DataSource<Key,ToValue> mapByPage(androidx.arch.core.util.Function<java.util.List<Value>,java.util.List<ToValue>>);
+    method @AnyThread public void removeInvalidatedCallback(androidx.paging.DataSource.InvalidatedCallback);
+  }
+
+  public abstract static class DataSource.Factory<Key, Value> {
+    ctor public DataSource.Factory();
+    method public abstract androidx.paging.DataSource<Key,Value> create();
+    method public <ToValue> androidx.paging.DataSource.Factory<Key,ToValue> map(androidx.arch.core.util.Function<Value,ToValue>);
+    method public <ToValue> androidx.paging.DataSource.Factory<Key,ToValue> mapByPage(androidx.arch.core.util.Function<java.util.List<Value>,java.util.List<ToValue>>);
+  }
+
+  public static interface DataSource.InvalidatedCallback {
+    method @AnyThread public void onInvalidated();
+  }
+
+  public abstract class ItemKeyedDataSource<Key, Value> extends androidx.paging.DataSource<Key,Value> {
+    method public abstract Key getKey(Value);
+    method public abstract void loadAfter(androidx.paging.ItemKeyedDataSource.LoadParams<Key>, androidx.paging.ItemKeyedDataSource.LoadCallback<Value>);
+    method public abstract void loadBefore(androidx.paging.ItemKeyedDataSource.LoadParams<Key>, androidx.paging.ItemKeyedDataSource.LoadCallback<Value>);
+    method public abstract void loadInitial(androidx.paging.ItemKeyedDataSource.LoadInitialParams<Key>, androidx.paging.ItemKeyedDataSource.LoadInitialCallback<Value>);
+    method public final <ToValue> androidx.paging.ItemKeyedDataSource<Key,ToValue> map(androidx.arch.core.util.Function<Value,ToValue>);
+    method public final <ToValue> androidx.paging.ItemKeyedDataSource<Key,ToValue> mapByPage(androidx.arch.core.util.Function<java.util.List<Value>,java.util.List<ToValue>>);
+  }
+
+  public abstract static class ItemKeyedDataSource.LoadCallback<Value> {
+    ctor public ItemKeyedDataSource.LoadCallback();
+    method public abstract void onResult(java.util.List<Value>);
+  }
+
+  public abstract static class ItemKeyedDataSource.LoadInitialCallback<Value> extends androidx.paging.ItemKeyedDataSource.LoadCallback<Value> {
+    ctor public ItemKeyedDataSource.LoadInitialCallback();
+    method public abstract void onResult(java.util.List<Value>, int, int);
+  }
+
+  public static class ItemKeyedDataSource.LoadInitialParams<Key> {
+    ctor public ItemKeyedDataSource.LoadInitialParams(Key?, int, boolean);
+    field public final boolean placeholdersEnabled;
+    field public final Key? requestedInitialKey;
+    field public final int requestedLoadSize;
+  }
+
+  public static class ItemKeyedDataSource.LoadParams<Key> {
+    ctor public ItemKeyedDataSource.LoadParams(Key, int);
+    field public final Key key;
+    field public final int requestedLoadSize;
+  }
+
+  public abstract class PageKeyedDataSource<Key, Value> extends androidx.paging.DataSource<Key,Value> {
+    method public abstract void loadAfter(androidx.paging.PageKeyedDataSource.LoadParams<Key>, androidx.paging.PageKeyedDataSource.LoadCallback<Key,Value>);
+    method public abstract void loadBefore(androidx.paging.PageKeyedDataSource.LoadParams<Key>, androidx.paging.PageKeyedDataSource.LoadCallback<Key,Value>);
+    method public abstract void loadInitial(androidx.paging.PageKeyedDataSource.LoadInitialParams<Key>, androidx.paging.PageKeyedDataSource.LoadInitialCallback<Key,Value>);
+    method public final <ToValue> androidx.paging.PageKeyedDataSource<Key,ToValue> map(androidx.arch.core.util.Function<Value,ToValue>);
+    method public final <ToValue> androidx.paging.PageKeyedDataSource<Key,ToValue> mapByPage(androidx.arch.core.util.Function<java.util.List<Value>,java.util.List<ToValue>>);
+  }
+
+  public abstract static class PageKeyedDataSource.LoadCallback<Key, Value> {
+    ctor public PageKeyedDataSource.LoadCallback();
+    method public abstract void onResult(java.util.List<Value>, Key?);
+  }
+
+  public abstract static class PageKeyedDataSource.LoadInitialCallback<Key, Value> {
+    ctor public PageKeyedDataSource.LoadInitialCallback();
+    method public abstract void onResult(java.util.List<Value>, int, int, Key?, Key?);
+    method public abstract void onResult(java.util.List<Value>, Key?, Key?);
+  }
+
+  public static class PageKeyedDataSource.LoadInitialParams<Key> {
+    ctor public PageKeyedDataSource.LoadInitialParams(int, boolean);
+    field public final boolean placeholdersEnabled;
+    field public final int requestedLoadSize;
+  }
+
+  public static class PageKeyedDataSource.LoadParams<Key> {
+    ctor public PageKeyedDataSource.LoadParams(Key, int);
+    field public final Key key;
+    field public final int requestedLoadSize;
+  }
+
+  public abstract class PagedList<T> extends java.util.AbstractList<T> {
+    method public void addWeakCallback(java.util.List<T>?, androidx.paging.PagedList.Callback);
+    method public void detach();
+    method public T? get(int);
+    method public androidx.paging.PagedList.Config getConfig();
+    method public abstract androidx.paging.DataSource<?,T> getDataSource();
+    method public abstract Object? getLastKey();
+    method public int getLoadedCount();
+    method public int getPositionOffset();
+    method public boolean isDetached();
+    method public boolean isImmutable();
+    method public void loadAround(int);
+    method public void removeWeakCallback(androidx.paging.PagedList.Callback);
+    method public int size();
+    method public java.util.List<T> snapshot();
+  }
+
+  @MainThread public abstract static class PagedList.BoundaryCallback<T> {
+    ctor public PagedList.BoundaryCallback();
+    method public void onItemAtEndLoaded(T);
+    method public void onItemAtFrontLoaded(T);
+    method public void onZeroItemsLoaded();
+  }
+
+  public static final class PagedList.Builder<Key, Value> {
+    ctor public PagedList.Builder(androidx.paging.DataSource<Key,Value>, androidx.paging.PagedList.Config);
+    ctor public PagedList.Builder(androidx.paging.DataSource<Key,Value>, int);
+    method @WorkerThread public androidx.paging.PagedList<Value> build();
+    method public androidx.paging.PagedList.Builder<Key,Value> setBoundaryCallback(androidx.paging.PagedList.BoundaryCallback?);
+    method public androidx.paging.PagedList.Builder<Key,Value> setFetchExecutor(java.util.concurrent.Executor);
+    method public androidx.paging.PagedList.Builder<Key,Value> setInitialKey(Key?);
+    method public androidx.paging.PagedList.Builder<Key,Value> setNotifyExecutor(java.util.concurrent.Executor);
+  }
+
+  public abstract static class PagedList.Callback {
+    ctor public PagedList.Callback();
+    method public abstract void onChanged(int, int);
+    method public abstract void onInserted(int, int);
+    method public abstract void onRemoved(int, int);
+  }
+
+  public static class PagedList.Config {
+    field public static final int MAX_SIZE_UNBOUNDED = 2147483647; // 0x7fffffff
+    field public final boolean enablePlaceholders;
+    field public final int initialLoadSizeHint;
+    field public final int maxSize;
+    field public final int pageSize;
+    field public final int prefetchDistance;
+  }
+
+  public static final class PagedList.Config.Builder {
+    ctor public PagedList.Config.Builder();
+    method public androidx.paging.PagedList.Config build();
+    method public androidx.paging.PagedList.Config.Builder setEnablePlaceholders(boolean);
+    method public androidx.paging.PagedList.Config.Builder setInitialLoadSizeHint(@IntRange(from=1) int);
+    method public androidx.paging.PagedList.Config.Builder setMaxSize(@IntRange(from=2) int);
+    method public androidx.paging.PagedList.Config.Builder setPageSize(@IntRange(from=1) int);
+    method public androidx.paging.PagedList.Config.Builder setPrefetchDistance(@IntRange(from=0) int);
+  }
+
+  public abstract class PositionalDataSource<T> extends androidx.paging.DataSource<java.lang.Integer,T> {
+    method public static int computeInitialLoadPosition(androidx.paging.PositionalDataSource.LoadInitialParams, int);
+    method public static int computeInitialLoadSize(androidx.paging.PositionalDataSource.LoadInitialParams, int, int);
+    method @WorkerThread public abstract void loadInitial(androidx.paging.PositionalDataSource.LoadInitialParams, androidx.paging.PositionalDataSource.LoadInitialCallback<T>);
+    method @WorkerThread public abstract void loadRange(androidx.paging.PositionalDataSource.LoadRangeParams, androidx.paging.PositionalDataSource.LoadRangeCallback<T>);
+    method public final <V> androidx.paging.PositionalDataSource<V> map(androidx.arch.core.util.Function<T,V>);
+    method public final <V> androidx.paging.PositionalDataSource<V> mapByPage(androidx.arch.core.util.Function<java.util.List<T>,java.util.List<V>>);
+  }
+
+  public abstract static class PositionalDataSource.LoadInitialCallback<T> {
+    ctor public PositionalDataSource.LoadInitialCallback();
+    method public abstract void onResult(java.util.List<T>, int, int);
+    method public abstract void onResult(java.util.List<T>, int);
+  }
+
+  public static class PositionalDataSource.LoadInitialParams {
+    ctor public PositionalDataSource.LoadInitialParams(int, int, int, boolean);
+    field public final int pageSize;
+    field public final boolean placeholdersEnabled;
+    field public final int requestedLoadSize;
+    field public final int requestedStartPosition;
+  }
+
+  public abstract static class PositionalDataSource.LoadRangeCallback<T> {
+    ctor public PositionalDataSource.LoadRangeCallback();
+    method public abstract void onResult(java.util.List<T>);
+  }
+
+  public static class PositionalDataSource.LoadRangeParams {
+    ctor public PositionalDataSource.LoadRangeParams(int, int);
+    field public final int loadSize;
+    field public final int startPosition;
+  }
+
+}
+
diff --git a/paging/common/build.gradle b/paging/common/build.gradle
index 3d74916..8e166b3 100644
--- a/paging/common/build.gradle
+++ b/paging/common/build.gradle
@@ -30,7 +30,7 @@
 
 dependencies {
     compile(SUPPORT_ANNOTATIONS)
-    compile(project(":arch:core-common"))
+    compile("androidx.arch.core:core-common:2.0.0")
 
     testCompile(JUNIT)
     testCompile(MOCKITO_CORE)
diff --git a/paging/common/ktx/api/2.1.0-beta01.txt b/paging/common/ktx/api/2.1.0-beta01.txt
new file mode 100644
index 0000000..0f070c2
--- /dev/null
+++ b/paging/common/ktx/api/2.1.0-beta01.txt
@@ -0,0 +1,15 @@
+// Signature format: 2.0
+package androidx.paging {
+
+  public final class PagedListConfigKt {
+    ctor public PagedListConfigKt();
+    method public static androidx.paging.PagedList.Config Config(int pageSize, int prefetchDistance = pageSize, boolean enablePlaceholders = true, int initialLoadSizeHint = pageSize * androidx.paging.PagedList.Config.Builder.DEFAULT_INITIAL_PAGE_MULTIPLIER, int maxSize = 2147483647);
+  }
+
+  public final class PagedListKt {
+    ctor public PagedListKt();
+    method public static <Key, Value> androidx.paging.PagedList<Value> PagedList(androidx.paging.DataSource<Key,Value> dataSource, androidx.paging.PagedList.Config config, java.util.concurrent.Executor notifyExecutor, java.util.concurrent.Executor fetchExecutor, androidx.paging.PagedList.BoundaryCallback<Value>? boundaryCallback = null, Key? initialKey = null);
+  }
+
+}
+
diff --git a/paging/runtime/api/2.1.0-beta01.txt b/paging/runtime/api/2.1.0-beta01.txt
new file mode 100644
index 0000000..0f658dfd
--- /dev/null
+++ b/paging/runtime/api/2.1.0-beta01.txt
@@ -0,0 +1,42 @@
+// Signature format: 2.0
+package androidx.paging {
+
+  public class AsyncPagedListDiffer<T> {
+    ctor public AsyncPagedListDiffer(androidx.recyclerview.widget.RecyclerView.Adapter, androidx.recyclerview.widget.DiffUtil.ItemCallback<T>);
+    ctor public AsyncPagedListDiffer(androidx.recyclerview.widget.ListUpdateCallback, androidx.recyclerview.widget.AsyncDifferConfig<T>);
+    method public void addPagedListListener(androidx.paging.AsyncPagedListDiffer.PagedListListener<T>);
+    method public androidx.paging.PagedList<T>? getCurrentList();
+    method public T? getItem(int);
+    method public int getItemCount();
+    method public void removePagedListListener(androidx.paging.AsyncPagedListDiffer.PagedListListener<T>);
+    method public void submitList(androidx.paging.PagedList<T>?);
+    method public void submitList(androidx.paging.PagedList<T>?, Runnable?);
+  }
+
+  public static interface AsyncPagedListDiffer.PagedListListener<T> {
+    method public void onCurrentListChanged(androidx.paging.PagedList<T>?, androidx.paging.PagedList<T>?);
+  }
+
+  public final class LivePagedListBuilder<Key, Value> {
+    ctor public LivePagedListBuilder(androidx.paging.DataSource.Factory<Key,Value>, androidx.paging.PagedList.Config);
+    ctor public LivePagedListBuilder(androidx.paging.DataSource.Factory<Key,Value>, int);
+    method public androidx.lifecycle.LiveData<androidx.paging.PagedList<Value>> build();
+    method public androidx.paging.LivePagedListBuilder<Key,Value> setBoundaryCallback(androidx.paging.PagedList.BoundaryCallback<Value>?);
+    method public androidx.paging.LivePagedListBuilder<Key,Value> setFetchExecutor(java.util.concurrent.Executor);
+    method public androidx.paging.LivePagedListBuilder<Key,Value> setInitialLoadKey(Key?);
+  }
+
+  public abstract class PagedListAdapter<T, VH extends androidx.recyclerview.widget.RecyclerView.ViewHolder> extends androidx.recyclerview.widget.RecyclerView.Adapter<VH> {
+    ctor protected PagedListAdapter(androidx.recyclerview.widget.DiffUtil.ItemCallback<T>);
+    ctor protected PagedListAdapter(androidx.recyclerview.widget.AsyncDifferConfig<T>);
+    method public androidx.paging.PagedList<T>? getCurrentList();
+    method protected T? getItem(int);
+    method public int getItemCount();
+    method @Deprecated public void onCurrentListChanged(androidx.paging.PagedList<T>?);
+    method public void onCurrentListChanged(androidx.paging.PagedList<T>?, androidx.paging.PagedList<T>?);
+    method public void submitList(androidx.paging.PagedList<T>?);
+    method public void submitList(androidx.paging.PagedList<T>?, Runnable?);
+  }
+
+}
+
diff --git a/paging/runtime/build.gradle b/paging/runtime/build.gradle
index 1d5d447..2b0ec98 100644
--- a/paging/runtime/build.gradle
+++ b/paging/runtime/build.gradle
@@ -36,8 +36,7 @@
     api("androidx.arch.core:core-runtime:2.0.0")
     api("androidx.lifecycle:lifecycle-runtime:2.0.0")
     api("androidx.lifecycle:lifecycle-livedata:2.0.0")
-
-    api(SUPPORT_RECYCLERVIEW, libs.support_exclude_config)
+    api("androidx.recyclerview:recyclerview:1.0.0", libs.support_exclude_config)
 
     androidTestImplementation(JUNIT)
     androidTestImplementation(MOCKITO_CORE, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
diff --git a/paging/runtime/ktx/api/2.1.0-beta01.txt b/paging/runtime/ktx/api/2.1.0-beta01.txt
new file mode 100644
index 0000000..b7750be
--- /dev/null
+++ b/paging/runtime/ktx/api/2.1.0-beta01.txt
@@ -0,0 +1,11 @@
+// Signature format: 2.0
+package androidx.paging {
+
+  public final class LivePagedListKt {
+    ctor public LivePagedListKt();
+    method public static <Key, Value> androidx.lifecycle.LiveData<androidx.paging.PagedList<Value>> toLiveData(androidx.paging.DataSource.Factory<Key,Value>, androidx.paging.PagedList.Config config, Key? initialLoadKey = null, androidx.paging.PagedList.BoundaryCallback<Value>? boundaryCallback = null, java.util.concurrent.Executor fetchExecutor = ArchTaskExecutor.getIOThreadExecutor());
+    method public static <Key, Value> androidx.lifecycle.LiveData<androidx.paging.PagedList<Value>> toLiveData(androidx.paging.DataSource.Factory<Key,Value>, int pageSize, Key? initialLoadKey = null, androidx.paging.PagedList.BoundaryCallback<Value>? boundaryCallback = null, java.util.concurrent.Executor fetchExecutor = ArchTaskExecutor.getIOThreadExecutor());
+  }
+
+}
+
diff --git a/paging/rxjava2/api/2.1.0-beta01.txt b/paging/rxjava2/api/2.1.0-beta01.txt
new file mode 100644
index 0000000..067f422
--- /dev/null
+++ b/paging/rxjava2/api/2.1.0-beta01.txt
@@ -0,0 +1,16 @@
+// Signature format: 2.0
+package androidx.paging {
+
+  public final class RxPagedListBuilder<Key, Value> {
+    ctor public RxPagedListBuilder(androidx.paging.DataSource.Factory<Key,Value>, androidx.paging.PagedList.Config);
+    ctor public RxPagedListBuilder(androidx.paging.DataSource.Factory<Key,Value>, int);
+    method public io.reactivex.Flowable<androidx.paging.PagedList<Value>> buildFlowable(io.reactivex.BackpressureStrategy);
+    method public io.reactivex.Observable<androidx.paging.PagedList<Value>> buildObservable();
+    method public androidx.paging.RxPagedListBuilder<Key,Value> setBoundaryCallback(androidx.paging.PagedList.BoundaryCallback<Value>?);
+    method public androidx.paging.RxPagedListBuilder<Key,Value> setFetchScheduler(io.reactivex.Scheduler);
+    method public androidx.paging.RxPagedListBuilder<Key,Value> setInitialLoadKey(Key?);
+    method public androidx.paging.RxPagedListBuilder<Key,Value> setNotifyScheduler(io.reactivex.Scheduler);
+  }
+
+}
+
diff --git a/paging/rxjava2/build.gradle b/paging/rxjava2/build.gradle
index bf0ed5b..b011d9c 100644
--- a/paging/rxjava2/build.gradle
+++ b/paging/rxjava2/build.gradle
@@ -31,9 +31,10 @@
 }
 
 dependencies {
-    api(project(":arch:core-runtime"))
     api(project(":paging:paging-common"))
 
+    api("androidx.arch.core:core-runtime:2.0.0")
+
     api(RX_JAVA)
     androidTestImplementation(JUNIT)
     androidTestImplementation(MOCKITO_CORE, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
diff --git a/paging/rxjava2/ktx/api/2.1.0-beta01.txt b/paging/rxjava2/ktx/api/2.1.0-beta01.txt
new file mode 100644
index 0000000..6b573b4
--- /dev/null
+++ b/paging/rxjava2/ktx/api/2.1.0-beta01.txt
@@ -0,0 +1,13 @@
+// Signature format: 2.0
+package androidx.paging {
+
+  public final class RxPagedListKt {
+    ctor public RxPagedListKt();
+    method public static <Key, Value> io.reactivex.Flowable<androidx.paging.PagedList<Value>> toFlowable(androidx.paging.DataSource.Factory<Key,Value>, androidx.paging.PagedList.Config config, Key? initialLoadKey = null, androidx.paging.PagedList.BoundaryCallback<Value>? boundaryCallback = null, io.reactivex.Scheduler? fetchScheduler = null, io.reactivex.Scheduler? notifyScheduler = null, io.reactivex.BackpressureStrategy backpressureStrategy = io.reactivex.BackpressureStrategy.LATEST);
+    method public static <Key, Value> io.reactivex.Flowable<androidx.paging.PagedList<Value>> toFlowable(androidx.paging.DataSource.Factory<Key,Value>, int pageSize, Key? initialLoadKey = null, androidx.paging.PagedList.BoundaryCallback<Value>? boundaryCallback = null, io.reactivex.Scheduler? fetchScheduler = null, io.reactivex.Scheduler? notifyScheduler = null, io.reactivex.BackpressureStrategy backpressureStrategy = io.reactivex.BackpressureStrategy.LATEST);
+    method public static <Key, Value> io.reactivex.Observable<androidx.paging.PagedList<Value>> toObservable(androidx.paging.DataSource.Factory<Key,Value>, androidx.paging.PagedList.Config config, Key? initialLoadKey = null, androidx.paging.PagedList.BoundaryCallback<Value>? boundaryCallback = null, io.reactivex.Scheduler? fetchScheduler = null, io.reactivex.Scheduler? notifyScheduler = null);
+    method public static <Key, Value> io.reactivex.Observable<androidx.paging.PagedList<Value>> toObservable(androidx.paging.DataSource.Factory<Key,Value>, int pageSize, Key? initialLoadKey = null, androidx.paging.PagedList.BoundaryCallback<Value>? boundaryCallback = null, io.reactivex.Scheduler? fetchScheduler = null, io.reactivex.Scheduler? notifyScheduler = null);
+  }
+
+}
+
diff --git a/palette/ktx/build.gradle b/palette/ktx/build.gradle
index c4a9ed4..d999771 100644
--- a/palette/ktx/build.gradle
+++ b/palette/ktx/build.gradle
@@ -41,7 +41,7 @@
 supportLibrary {
     name = "Palette Kotlin Extensions"
     publish = true
-    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenVersion = LibraryVersions.PALETTE_KTX
     mavenGroup = LibraryGroups.PALETTE
     inceptionYear = "2018"
     description = "Kotlin extensions for 'palette' artifact"
diff --git a/percent/build.gradle b/percent/build.gradle
index 7faabd5..35f4af5 100644
--- a/percent/build.gradle
+++ b/percent/build.gradle
@@ -23,7 +23,7 @@
 supportLibrary {
     name = "Android Percent Support Library"
     publish = true
-    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenVersion = LibraryVersions.PERCENTLAYOUT
     mavenGroup = LibraryGroups.PERCENTLAYOUT
     inceptionYear = "2015"
     description = "Android Percent Support Library"
diff --git a/preference/build.gradle b/preference/build.gradle
index fc69785..350fb4f 100644
--- a/preference/build.gradle
+++ b/preference/build.gradle
@@ -23,11 +23,11 @@
 }
 
 dependencies {
-    api(project(":core"))
-    api(project(":collection"))
-    api(project(":fragment"))
-    api(project(":appcompat"))
-    api(project(":recyclerview"))
+    api("androidx.core:core:1.1.0-alpha01")
+    api("androidx.collection:collection:1.0.0")
+    api("androidx.fragment:fragment:1.1.0-alpha01")
+    api("androidx.appcompat:appcompat:1.0.0")
+    api("androidx.recyclerview:recyclerview:1.0.0")
 
     androidTestImplementation(TEST_RUNNER)
     androidTestImplementation(TEST_RULES)
diff --git a/preference/ktx/build.gradle b/preference/ktx/build.gradle
index 2a43336..d344a29 100644
--- a/preference/ktx/build.gradle
+++ b/preference/ktx/build.gradle
@@ -14,10 +14,11 @@
  * limitations under the License.
  */
 
-import static androidx.build.dependencies.DependenciesKt.*
+
 import androidx.build.LibraryGroups
 import androidx.build.LibraryVersions
-import androidx.build.SupportLibraryExtension
+
+import static androidx.build.dependencies.DependenciesKt.*
 
 plugins {
     id("SupportAndroidLibraryPlugin")
@@ -41,12 +42,13 @@
     androidTestImplementation(TEST_RULES)
     androidTestImplementation(TRUTH)
     androidTestImplementation(project(":internal-testutils-ktx"))
+    androidTestImplementation(GUAVA_LISTENABLE_FUTURE_AVOID_CONFLICT)
 }
 
 supportLibrary {
     name = "Android Preferences KTX"
     publish = true
-    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenVersion = LibraryVersions.PREFERENCE_KTX
     mavenGroup = LibraryGroups.PREFERENCE
     inceptionYear = "2018"
     description = "Kotlin extensions for preferences"
diff --git a/print/build.gradle b/print/build.gradle
index 7901514..98a973c 100644
--- a/print/build.gradle
+++ b/print/build.gradle
@@ -12,7 +12,7 @@
 supportLibrary {
     name = "Android Support Library Print"
     publish = true
-    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenVersion = LibraryVersions.PRINT
     mavenGroup = LibraryGroups.PRINT
     inceptionYear = "2018"
     description = "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren't a part of the framework APIs. Compatible on devices running API 14 or later."
diff --git a/recommendation/build.gradle b/recommendation/build.gradle
index 3b4ea70..37ffe4b 100644
--- a/recommendation/build.gradle
+++ b/recommendation/build.gradle
@@ -18,7 +18,7 @@
 supportLibrary {
     name = "Android Support Recommendation"
     publish = true
-    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenVersion = LibraryVersions.RECOMMENDATION
     mavenGroup = LibraryGroups.RECOMMENDATION
     inceptionYear = "2015"
     description = "Android Support Recommendation"
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/BaseRecyclerViewInstrumentationTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/BaseRecyclerViewInstrumentationTest.java
index 36a34f7..9a02110 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/BaseRecyclerViewInstrumentationTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/BaseRecyclerViewInstrumentationTest.java
@@ -788,6 +788,10 @@
             this.mFocusable = mFocusable;
         }
 
+        public String getDisplayText() {
+            return mText + "(" + mId + ")";
+        }
+
         @Override
         public String toString() {
             return "Item{" +
@@ -897,7 +901,7 @@
             assertNotNull(holder.mOwnerRecyclerView);
             assertEquals(position, holder.getAdapterPosition());
             final Item item = mItems.get(position);
-            ((TextView) (holder.itemView)).setText(item.mText + "(" + item.mId + ")");
+            ((TextView) (holder.itemView)).setText(item.getDisplayText());
             holder.itemView.setBackgroundColor(position % 2 == 0 ? 0xFFFF0000 : 0xFF0000FF);
             holder.mBoundItem = item;
             if (mLayoutParams != null) {
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/PagerSnapHelperTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/PagerSnapHelperTest.java
index 8bef928..c65c9eb 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/PagerSnapHelperTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/PagerSnapHelperTest.java
@@ -20,7 +20,12 @@
 
 import static androidx.recyclerview.widget.RecyclerView.HORIZONTAL;
 import static androidx.recyclerview.widget.RecyclerView.VERTICAL;
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom;
+import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
 
+import static org.hamcrest.CoreMatchers.allOf;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotSame;
 import static org.junit.Assert.assertSame;
@@ -29,8 +34,10 @@
 import android.view.View;
 import android.widget.TextView;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.test.filters.LargeTest;
+import androidx.testutils.SwipeToLocation;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -38,6 +45,8 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 @LargeTest
@@ -178,6 +187,14 @@
         runSnapOnMaxFlingNextView(mRecyclerView.getMaxFlingVelocity());
     }
 
+    @Test
+    public void snapWhenFlingToSnapPosition() throws Throwable {
+        final Config config = (Config) mConfig.clone();
+        setupByConfig(config, true, getChildLayoutParams(), getParentLayoutParams());
+        setupSnapHelper();
+        runSnapOnFlingExactlyToNextView();
+    }
+
     private RecyclerView.LayoutParams getParentLayoutParams() {
         return new RecyclerView.LayoutParams(RECYCLERVIEW_SIZE, RECYCLERVIEW_SIZE);
     }
@@ -207,7 +224,6 @@
         waitForIdleScroll(mRecyclerView);
         assertTrue(fling(velocityDir, velocityDir));
         mLayoutManager.waitForSnap(100);
-        getInstrumentation().waitForIdleSync();
 
         View viewAfterFling = findCenterView(mLayoutManager);
 
@@ -219,6 +235,43 @@
         assertCenterAligned(viewAfterFling);
     }
 
+    private void runSnapOnFlingExactlyToNextView() throws Throwable {
+        // Record the current center view.
+        View view = findCenterView(mLayoutManager);
+        assertCenterAligned(view);
+
+        // Determine the target item to scroll to
+        final int expectedPosition = mConfig.mItemCount / 2 + (mConfig.mReverseLayout
+                ? (mReverseScroll ? 1 : -1)
+                : (mReverseScroll ? -1 : 1));
+
+        // Smooth scroll in the correct direction to allow fling snapping to the next view.
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mRecyclerView.smoothScrollToPosition(expectedPosition);
+            }
+        });
+        waitForDistanceToTarget(expectedPosition, .5f);
+
+        // Interrupt scroll and fling to target view, ending exactly when the view is snapped
+        mLayoutManager.expectIdleState(1);
+        onView(allOf(
+                isDescendantOfA(isAssignableFrom(RecyclerView.class)),
+                withText(mTestAdapter.getItemAt(expectedPosition).getDisplayText())
+        )).perform(SwipeToLocation.flingToCenter());
+        waitForIdleScroll(mRecyclerView);
+
+        // Wait until the RecyclerView comes to a rest
+        mLayoutManager.waitForSnap(100);
+
+        // Check the result
+        View viewAfterFling = findCenterView(mLayoutManager);
+        assertNotSame("The view should have scrolled", view, viewAfterFling);
+        assertEquals(expectedPosition, mLayoutManager.getPosition(viewAfterFling));
+        assertCenterAligned(viewAfterFling);
+    }
+
     private void setupSnapHelper() throws Throwable {
         SnapHelper snapHelper = new PagerSnapHelper();
 
@@ -314,4 +367,35 @@
         waitForIdleScroll(mRecyclerView);
         return true;
     }
+
+    /**
+     * Waits until the RecyclerView has smooth scrolled till within the given margin from the target
+     * item. The percentage is relative to the size of the target view.
+     *
+     * @param targetPosition The adapter position of the view we want to scroll to
+     * @param distancePercent The distance from the view when we stop waiting, relative to the
+     *                        target view
+     */
+    private void waitForDistanceToTarget(final int targetPosition, final float distancePercent)
+            throws InterruptedException {
+        final CountDownLatch latch = new CountDownLatch(1);
+        mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+            @Override
+            public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
+                View target = mLayoutManager.findViewByPosition(targetPosition);
+                if (target == null) {
+                    return;
+                }
+                int distancePx = distFromCenter(target);
+                int size = mConfig.mOrientation == HORIZONTAL
+                        ? target.getWidth()
+                        : target.getHeight();
+                if ((float) distancePx / size <= distancePercent) {
+                    latch.countDown();
+                }
+            }
+        });
+        assertTrue("should be close enough to the target view within 10 seconds",
+                latch.await(10, TimeUnit.SECONDS));
+    }
 }
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewFocusRecoveryTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewFocusRecoveryTest.java
index 8434c3ac9..415f45c 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewFocusRecoveryTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewFocusRecoveryTest.java
@@ -916,7 +916,7 @@
         }
 
         protected String getText(Item item) {
-            return item.mText + "(" + item.mId + ")";
+            return item.getDisplayText();
         }
 
         abstract void setFocusable(boolean focusable);
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StaggeredGridLayoutManagerTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StaggeredGridLayoutManagerTest.java
index caf4a54..5978b64 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StaggeredGridLayoutManagerTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StaggeredGridLayoutManagerTest.java
@@ -357,7 +357,7 @@
                         Item item = mItems.get(position);
                         holder.mBoundItem = item;
                         ((EditText) ((FrameLayout) holder.itemView).getChildAt(0)).setText(
-                                item.mText + " (" + item.mId + ")");
+                                item.getDisplayText());
                         // Good to have colors for debugging
                         StateListDrawable stl = new StateListDrawable();
                         stl.addState(new int[]{android.R.attr.state_focused},
diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/PagerSnapHelper.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/PagerSnapHelper.java
index 1dbc495..3592602 100644
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/PagerSnapHelper.java
+++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/PagerSnapHelper.java
@@ -177,9 +177,8 @@
                 final int dx = snapDistances[0];
                 final int dy = snapDistances[1];
                 final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)));
-                if (time > 0) {
-                    action.update(dx, dy, time, mDecelerateInterpolator);
-                }
+                // TODO: revert change in next line when b/118663993 is fixed
+                action.update(dx, dy, Math.max(1, time), mDecelerateInterpolator);
             }
 
             @Override
diff --git a/room/compiler/src/main/kotlin/androidx/room/ext/javapoet_ext.kt b/room/compiler/src/main/kotlin/androidx/room/ext/javapoet_ext.kt
index 1bb351b..ba49625 100644
--- a/room/compiler/src/main/kotlin/androidx/room/ext/javapoet_ext.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/ext/javapoet_ext.kt
@@ -82,6 +82,8 @@
             ClassName.get("androidx.room.paging", "LimitOffsetDataSource")
     val DB_UTIL: ClassName =
             ClassName.get("androidx.room.util", "DBUtil")
+    val CURSOR_UTIL: ClassName =
+            ClassName.get("androidx.room.util", "CursorUtil")
 }
 
 object PagingTypeNames {
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/PojoRowAdapter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/PojoRowAdapter.kt
index 03e7c20..c1e97ca 100644
--- a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/PojoRowAdapter.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/PojoRowAdapter.kt
@@ -17,6 +17,7 @@
 package androidx.room.solver.query.result
 
 import androidx.room.ext.L
+import androidx.room.ext.RoomTypeNames
 import androidx.room.ext.S
 import androidx.room.ext.T
 import androidx.room.processor.Context
@@ -118,8 +119,9 @@
             } else {
                 "getColumnIndexOrThrow"
             }
-            scope.builder().addStatement("final $T $L = $L.$L($S)",
-                    TypeName.INT, indexVar, cursorVarName, indexMethod, it.columnName)
+            scope.builder().addStatement("final $T $L = $T.$L($L, $S)",
+                TypeName.INT, indexVar, RoomTypeNames.CURSOR_UTIL, indexMethod, cursorVarName,
+                it.columnName)
             FieldWithIndex(field = it, indexVar = indexVar, alwaysExists = info != null)
         }
         if (relationCollectors.isNotEmpty()) {
diff --git a/room/compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt b/room/compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt
index 5cd3758..ae6aad7 100644
--- a/room/compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt
@@ -503,11 +503,16 @@
         elm: ExecutableElement,
         owner: DeclaredType
     ): MethodSpec.Builder {
-        val baseSpec = MethodSpec.overriding(elm, owner, processingEnv.typeUtils).build()
+        val baseSpec = MethodSpec.overriding(elm, owner, processingEnv.typeUtils)
+                .build()
+
+        // make all the params final
+        val params = baseSpec.parameters.map { it.toBuilder().addModifiers(FINAL).build() }
+
         return MethodSpec.methodBuilder(baseSpec.name).apply {
             addAnnotation(Override::class.java)
             addModifiers(baseSpec.modifiers)
-            addParameters(baseSpec.parameters)
+            addParameters(params)
             varargs(baseSpec.varargs)
             returns(baseSpec.returnType)
         }
diff --git a/room/compiler/src/main/kotlin/androidx/room/writer/RelationCollectorMethodWriter.kt b/room/compiler/src/main/kotlin/androidx/room/writer/RelationCollectorMethodWriter.kt
index 4345ea1..391f386 100644
--- a/room/compiler/src/main/kotlin/androidx/room/writer/RelationCollectorMethodWriter.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/writer/RelationCollectorMethodWriter.kt
@@ -129,8 +129,9 @@
                     if (shouldCopyCursor) "true" else "false")
 
             beginControlFlow("try").apply {
-                addStatement("final $T $L = $L.getColumnIndex($S)",
-                        TypeName.INT, itemKeyIndexVar, cursorVar, relation.entityField.columnName)
+                addStatement("final $T $L = $T.getColumnIndex($L, $S)",
+                    TypeName.INT, itemKeyIndexVar, RoomTypeNames.CURSOR_UTIL, cursorVar,
+                    relation.entityField.columnName)
 
                 beginControlFlow("if ($L == -1)", itemKeyIndexVar).apply {
                     addStatement("return")
diff --git a/room/compiler/src/test/data/daoWriter/output/ComplexDao.java b/room/compiler/src/test/data/daoWriter/output/ComplexDao.java
index 8efa8d3..52b387e 100644
--- a/room/compiler/src/test/data/daoWriter/output/ComplexDao.java
+++ b/room/compiler/src/test/data/daoWriter/output/ComplexDao.java
@@ -7,6 +7,7 @@
 import androidx.room.InvalidationTracker.Observer;
 import androidx.room.RoomDatabase;
 import androidx.room.RoomSQLiteQuery;
+import androidx.room.util.CursorUtil;
 import androidx.room.util.DBUtil;
 import androidx.room.util.StringUtil;
 import java.lang.Integer;
@@ -30,7 +31,7 @@
     }
 
     @Override
-    public boolean transactionMethod(int i, String s, long l) {
+    public boolean transactionMethod(final int i, final String s, final long l) {
         __db.beginTransaction();
         try {
             boolean _result = super.transactionMethod(i, s, l);
@@ -42,15 +43,15 @@
     }
 
     @Override
-    public List<ComplexDao.FullName> fullNames(int id) {
+    public List<ComplexDao.FullName> fullNames(final int id) {
         final String _sql = "SELECT name || lastName as fullName, uid as id FROM user where uid = ?";
         final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 1);
         int _argIndex = 1;
         _statement.bindLong(_argIndex, id);
         final Cursor _cursor = DBUtil.query(__db, _statement, false);
         try {
-            final int _cursorIndexOfFullName = _cursor.getColumnIndexOrThrow("fullName");
-            final int _cursorIndexOfId = _cursor.getColumnIndexOrThrow("id");
+            final int _cursorIndexOfFullName = CursorUtil.getColumnIndexOrThrow(_cursor, "fullName");
+            final int _cursorIndexOfId = CursorUtil.getColumnIndexOrThrow(_cursor, "id");
             final List<ComplexDao.FullName> _result = new ArrayList<ComplexDao.FullName>(_cursor.getCount());
             while(_cursor.moveToNext()) {
                 final ComplexDao.FullName _item;
@@ -67,17 +68,17 @@
     }
 
     @Override
-    public User getById(int id) {
+    public User getById(final int id) {
         final String _sql = "SELECT * FROM user where uid = ?";
         final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 1);
         int _argIndex = 1;
         _statement.bindLong(_argIndex, id);
         final Cursor _cursor = DBUtil.query(__db, _statement, false);
         try {
-            final int _cursorIndexOfUid = _cursor.getColumnIndexOrThrow("uid");
-            final int _cursorIndexOfName = _cursor.getColumnIndexOrThrow("name");
-            final int _cursorIndexOfLastName = _cursor.getColumnIndexOrThrow("lastName");
-            final int _cursorIndexOfAge = _cursor.getColumnIndexOrThrow("ageColumn");
+            final int _cursorIndexOfUid = CursorUtil.getColumnIndexOrThrow(_cursor, "uid");
+            final int _cursorIndexOfName = CursorUtil.getColumnIndexOrThrow(_cursor, "name");
+            final int _cursorIndexOfLastName = CursorUtil.getColumnIndexOrThrow(_cursor, "lastName");
+            final int _cursorIndexOfAge = CursorUtil.getColumnIndexOrThrow(_cursor, "ageColumn");
             final User _result;
             if(_cursor.moveToFirst()) {
                 _result = new User();
@@ -98,7 +99,7 @@
     }
 
     @Override
-    public User findByName(String name, String lastName) {
+    public User findByName(final String name, final String lastName) {
         final String _sql = "SELECT * FROM user where name LIKE ? AND lastName LIKE ?";
         final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 2);
         int _argIndex = 1;
@@ -115,10 +116,10 @@
         }
         final Cursor _cursor = DBUtil.query(__db, _statement, false);
         try {
-            final int _cursorIndexOfUid = _cursor.getColumnIndexOrThrow("uid");
-            final int _cursorIndexOfName = _cursor.getColumnIndexOrThrow("name");
-            final int _cursorIndexOfLastName = _cursor.getColumnIndexOrThrow("lastName");
-            final int _cursorIndexOfAge = _cursor.getColumnIndexOrThrow("ageColumn");
+            final int _cursorIndexOfUid = CursorUtil.getColumnIndexOrThrow(_cursor, "uid");
+            final int _cursorIndexOfName = CursorUtil.getColumnIndexOrThrow(_cursor, "name");
+            final int _cursorIndexOfLastName = CursorUtil.getColumnIndexOrThrow(_cursor, "lastName");
+            final int _cursorIndexOfAge = CursorUtil.getColumnIndexOrThrow(_cursor, "ageColumn");
             final User _result;
             if(_cursor.moveToFirst()) {
                 _result = new User();
@@ -139,7 +140,7 @@
     }
 
     @Override
-    public List<User> loadAllByIds(int... ids) {
+    public List<User> loadAllByIds(final int... ids) {
         StringBuilder _stringBuilder = StringUtil.newStringBuilder();
         _stringBuilder.append("SELECT * FROM user where uid IN (");
         final int _inputSize = ids.length;
@@ -155,10 +156,10 @@
         }
         final Cursor _cursor = DBUtil.query(__db, _statement, false);
         try {
-            final int _cursorIndexOfUid = _cursor.getColumnIndexOrThrow("uid");
-            final int _cursorIndexOfName = _cursor.getColumnIndexOrThrow("name");
-            final int _cursorIndexOfLastName = _cursor.getColumnIndexOrThrow("lastName");
-            final int _cursorIndexOfAge = _cursor.getColumnIndexOrThrow("ageColumn");
+            final int _cursorIndexOfUid = CursorUtil.getColumnIndexOrThrow(_cursor, "uid");
+            final int _cursorIndexOfName = CursorUtil.getColumnIndexOrThrow(_cursor, "name");
+            final int _cursorIndexOfLastName = CursorUtil.getColumnIndexOrThrow(_cursor, "lastName");
+            final int _cursorIndexOfAge = CursorUtil.getColumnIndexOrThrow(_cursor, "ageColumn");
             final List<User> _result = new ArrayList<User>(_cursor.getCount());
             while(_cursor.moveToNext()) {
                 final User _item_1;
@@ -179,7 +180,7 @@
     }
 
     @Override
-    int getAge(int id) {
+    int getAge(final int id) {
         final String _sql = "SELECT ageColumn FROM user where uid = ?";
         final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 1);
         int _argIndex = 1;
@@ -200,7 +201,7 @@
     }
 
     @Override
-    public int[] getAllAges(int... ids) {
+    public int[] getAllAges(final int... ids) {
         StringBuilder _stringBuilder = StringUtil.newStringBuilder();
         _stringBuilder.append("SELECT ageColumn FROM user where uid IN(");
         final int _inputSize = ids.length;
@@ -232,7 +233,7 @@
     }
 
     @Override
-    public List<Integer> getAllAgesAsList(List<Integer> ids) {
+    public List<Integer> getAllAgesAsList(final List<Integer> ids) {
         StringBuilder _stringBuilder = StringUtil.newStringBuilder();
         _stringBuilder.append("SELECT ageColumn FROM user where uid IN(");
         final int _inputSize = ids.size();
@@ -270,7 +271,7 @@
     }
 
     @Override
-    public LiveData<User> getByIdLive(int id) {
+    public LiveData<User> getByIdLive(final int id) {
         final String _sql = "SELECT * FROM user where uid = ?";
         final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 1);
         int _argIndex = 1;
@@ -291,10 +292,10 @@
                 }
                 final Cursor _cursor = DBUtil.query(__db, _statement, false);
                 try {
-                    final int _cursorIndexOfUid = _cursor.getColumnIndexOrThrow("uid");
-                    final int _cursorIndexOfName = _cursor.getColumnIndexOrThrow("name");
-                    final int _cursorIndexOfLastName = _cursor.getColumnIndexOrThrow("lastName");
-                    final int _cursorIndexOfAge = _cursor.getColumnIndexOrThrow("ageColumn");
+                    final int _cursorIndexOfUid = CursorUtil.getColumnIndexOrThrow(_cursor, "uid");
+                    final int _cursorIndexOfName = CursorUtil.getColumnIndexOrThrow(_cursor, "name");
+                    final int _cursorIndexOfLastName = CursorUtil.getColumnIndexOrThrow(_cursor, "lastName");
+                    final int _cursorIndexOfAge = CursorUtil.getColumnIndexOrThrow(_cursor, "ageColumn");
                     final User _result;
                     if(_cursor.moveToFirst()) {
                         _result = new User();
@@ -321,7 +322,7 @@
     }
 
     @Override
-    public LiveData<List<User>> loadUsersByIdsLive(int... ids) {
+    public LiveData<List<User>> loadUsersByIdsLive(final int... ids) {
         StringBuilder _stringBuilder = StringUtil.newStringBuilder();
         _stringBuilder.append("SELECT * FROM user where uid IN (");
         final int _inputSize = ids.length;
@@ -351,10 +352,10 @@
                 }
                 final Cursor _cursor = DBUtil.query(__db, _statement, false);
                 try {
-                    final int _cursorIndexOfUid = _cursor.getColumnIndexOrThrow("uid");
-                    final int _cursorIndexOfName = _cursor.getColumnIndexOrThrow("name");
-                    final int _cursorIndexOfLastName = _cursor.getColumnIndexOrThrow("lastName");
-                    final int _cursorIndexOfAge = _cursor.getColumnIndexOrThrow("ageColumn");
+                    final int _cursorIndexOfUid = CursorUtil.getColumnIndexOrThrow(_cursor, "uid");
+                    final int _cursorIndexOfName = CursorUtil.getColumnIndexOrThrow(_cursor, "name");
+                    final int _cursorIndexOfLastName = CursorUtil.getColumnIndexOrThrow(_cursor, "lastName");
+                    final int _cursorIndexOfAge = CursorUtil.getColumnIndexOrThrow(_cursor, "ageColumn");
                     final List<User> _result = new ArrayList<User>(_cursor.getCount());
                     while(_cursor.moveToNext()) {
                         final User _item_1;
@@ -381,7 +382,7 @@
     }
 
     @Override
-    public List<Integer> getAllAgesAsList(List<Integer> ids1, int[] ids2, int... ids3) {
+    public List<Integer> getAllAgesAsList(final List<Integer> ids1, final int[] ids2, final int... ids3) {
         StringBuilder _stringBuilder = StringUtil.newStringBuilder();
         _stringBuilder.append("SELECT ageColumn FROM user where uid IN(");
         final int _inputSize = ids1.size();
diff --git a/room/compiler/src/test/data/daoWriter/output/DeletionDao.java b/room/compiler/src/test/data/daoWriter/output/DeletionDao.java
index d34345b..59fae03 100644
--- a/room/compiler/src/test/data/daoWriter/output/DeletionDao.java
+++ b/room/compiler/src/test/data/daoWriter/output/DeletionDao.java
@@ -95,7 +95,7 @@
   }
 
   @Override
-  public void deleteUser(User user) {
+  public void deleteUser(final User user) {
     __db.beginTransaction();
     try {
       __deletionAdapterOfUser.handle(user);
@@ -106,7 +106,7 @@
   }
 
   @Override
-  public void deleteUsers(User user1, List<User> others) {
+  public void deleteUsers(final User user1, final List<User> others) {
     __db.beginTransaction();
     try {
       __deletionAdapterOfUser.handle(user1);
@@ -118,7 +118,7 @@
   }
 
   @Override
-  public void deleteArrayOfUsers(User[] users) {
+  public void deleteArrayOfUsers(final User[] users) {
     __db.beginTransaction();
     try {
       __deletionAdapterOfUser.handleMultiple(users);
@@ -129,7 +129,7 @@
   }
 
   @Override
-  public Integer deleteUserAndReturnCountObject(User user) {
+  public Integer deleteUserAndReturnCountObject(final User user) {
     int _total = 0;
     __db.beginTransaction();
     try {
@@ -142,7 +142,7 @@
   }
 
   @Override
-  public int deleteUserAndReturnCount(User user) {
+  public int deleteUserAndReturnCount(final User user) {
     int _total = 0;
     __db.beginTransaction();
     try {
@@ -155,7 +155,7 @@
   }
 
   @Override
-  public int deleteUserAndReturnCount(User user1, List<User> others) {
+  public int deleteUserAndReturnCount(final User user1, final List<User> others) {
     int _total = 0;
     __db.beginTransaction();
     try {
@@ -169,7 +169,7 @@
   }
 
   @Override
-  public int deleteUserAndReturnCount(User[] users) {
+  public int deleteUserAndReturnCount(final User[] users) {
     int _total = 0;
     __db.beginTransaction();
     try {
@@ -182,7 +182,7 @@
   }
 
   @Override
-  public Completable deleteUserCompletable(User user) {
+  public Completable deleteUserCompletable(final User user) {
     return Completable.fromCallable(new Callable() {
       @Override
       public Void call() throws Exception {
@@ -199,7 +199,7 @@
   }
 
   @Override
-  public Single<Integer> deleteUserSingle(User user) {
+  public Single<Integer> deleteUserSingle(final User user) {
     return Single.fromCallable(new Callable<Integer>() {
       @Override
       public Integer call() throws Exception {
@@ -217,7 +217,7 @@
   }
 
   @Override
-  public Maybe<Integer> deleteUserMaybe(User user) {
+  public Maybe<Integer> deleteUserMaybe(final User user) {
     return Maybe.fromCallable(new Callable<Integer>() {
       @Override
       public Integer call() throws Exception {
@@ -235,7 +235,7 @@
   }
 
   @Override
-  public int multiPKey(MultiPKeyEntity entity) {
+  public int multiPKey(final MultiPKeyEntity entity) {
     int _total = 0;
     __db.beginTransaction();
     try {
@@ -248,7 +248,7 @@
   }
 
   @Override
-  public void deleteUserAndBook(User user, Book book) {
+  public void deleteUserAndBook(final User user, final Book book) {
     __db.beginTransaction();
     try {
       __deletionAdapterOfUser.handle(user);
@@ -260,7 +260,7 @@
   }
 
   @Override
-  public int deleteByUid(int uid) {
+  public int deleteByUid(final int uid) {
     final SupportSQLiteStatement _stmt = __preparedStmtOfDeleteByUid.acquire();
     __db.beginTransaction();
     try {
@@ -290,7 +290,7 @@
   }
 
   @Override
-  public int deleteByUidList(int... uid) {
+  public int deleteByUidList(final int... uid) {
     StringBuilder _stringBuilder = StringUtil.newStringBuilder();
     _stringBuilder.append("DELETE FROM user where uid IN(");
     final int _inputSize = uid.length;
diff --git a/room/compiler/src/test/data/daoWriter/output/UpdateDao.java b/room/compiler/src/test/data/daoWriter/output/UpdateDao.java
index f9ba14a..8eb9945 100644
--- a/room/compiler/src/test/data/daoWriter/output/UpdateDao.java
+++ b/room/compiler/src/test/data/daoWriter/output/UpdateDao.java
@@ -117,7 +117,7 @@
   }
 
   @Override
-  public void updateUser(User user) {
+  public void updateUser(final User user) {
     __db.beginTransaction();
     try {
       __updateAdapterOfUser.handle(user);
@@ -128,7 +128,7 @@
   }
 
   @Override
-  public void updateUsers(User user1, List<User> others) {
+  public void updateUsers(final User user1, final List<User> others) {
     __db.beginTransaction();
     try {
       __updateAdapterOfUser.handle(user1);
@@ -140,7 +140,7 @@
   }
 
   @Override
-  public void updateArrayOfUsers(User[] users) {
+  public void updateArrayOfUsers(final User[] users) {
     __db.beginTransaction();
     try {
       __updateAdapterOfUser.handleMultiple(users);
@@ -151,7 +151,7 @@
   }
 
   @Override
-  public int updateUserAndReturnCount(User user) {
+  public int updateUserAndReturnCount(final User user) {
     int _total = 0;
     __db.beginTransaction();
     try {
@@ -164,7 +164,7 @@
   }
 
   @Override
-  public int updateUserAndReturnCount(User user1, List<User> others) {
+  public int updateUserAndReturnCount(final User user1, final List<User> others) {
     int _total = 0;
     __db.beginTransaction();
     try {
@@ -178,7 +178,7 @@
   }
 
   @Override
-  public int updateUserAndReturnCount(User[] users) {
+  public int updateUserAndReturnCount(final User[] users) {
     int _total = 0;
     __db.beginTransaction();
     try {
@@ -191,7 +191,7 @@
   }
 
   @Override
-  public Integer updateUserAndReturnCountObject(User user) {
+  public Integer updateUserAndReturnCountObject(final User user) {
     int _total = 0;
     __db.beginTransaction();
     try {
@@ -204,7 +204,7 @@
   }
 
   @Override
-  public Completable updateUserAndReturnCountCompletable(User user) {
+  public Completable updateUserAndReturnCountCompletable(final User user) {
     return Completable.fromCallable(new Callable() {
       @Override
       public Void call() throws Exception {
@@ -221,7 +221,7 @@
   }
 
   @Override
-  public Single<Integer> updateUserAndReturnCountSingle(User user) {
+  public Single<Integer> updateUserAndReturnCountSingle(final User user) {
     return Single.fromCallable(new Callable<Integer>() {
       @Override
       public Integer call() throws Exception {
@@ -239,7 +239,7 @@
   }
 
   @Override
-  public Maybe<Integer> updateUserAndReturnCountMaybe(User user) {
+  public Maybe<Integer> updateUserAndReturnCountMaybe(final User user) {
     return Maybe.fromCallable(new Callable<Integer>() {
       @Override
       public Integer call() throws Exception {
@@ -257,7 +257,7 @@
   }
 
   @Override
-  public int multiPKey(MultiPKeyEntity entity) {
+  public int multiPKey(final MultiPKeyEntity entity) {
     int _total = 0;
     __db.beginTransaction();
     try {
@@ -270,7 +270,7 @@
   }
 
   @Override
-  public void updateUserAndBook(User user, Book book) {
+  public void updateUserAndBook(final User user, final Book book) {
     __db.beginTransaction();
     try {
       __updateAdapterOfUser.handle(user);
@@ -282,7 +282,7 @@
   }
 
   @Override
-  public void updateAndAge(User user) {
+  public void updateAndAge(final User user) {
     __db.beginTransaction();
     try {
       UpdateDao.super.updateAndAge(user);
@@ -293,7 +293,7 @@
   }
 
   @Override
-  public void ageUserByUid(String uid) {
+  public void ageUserByUid(final String uid) {
     final SupportSQLiteStatement _stmt = __preparedStmtOfAgeUserByUid.acquire();
     __db.beginTransaction();
     try {
diff --git a/room/compiler/src/test/data/daoWriter/output/WriterDao.java b/room/compiler/src/test/data/daoWriter/output/WriterDao.java
index d2a1576..0e4a986 100644
--- a/room/compiler/src/test/data/daoWriter/output/WriterDao.java
+++ b/room/compiler/src/test/data/daoWriter/output/WriterDao.java
@@ -100,7 +100,7 @@
     }
 
     @Override
-    public void insertUser(User user) {
+    public void insertUser(final User user) {
         __db.beginTransaction();
         try {
             __insertionAdapterOfUser.insert(user);
@@ -111,7 +111,7 @@
     }
 
     @Override
-    public void insertUsers(User user1, List<User> others) {
+    public void insertUsers(final User user1, final List<User> others) {
         __db.beginTransaction();
         try {
             __insertionAdapterOfUser.insert(user1);
@@ -123,7 +123,7 @@
     }
 
     @Override
-    public void insertUsers(User[] users) {
+    public void insertUsers(final User[] users) {
         __db.beginTransaction();
         try {
             __insertionAdapterOfUser_1.insert(users);
@@ -134,7 +134,7 @@
     }
 
     @Override
-    public void insertUserAndBook(User user, Book book) {
+    public void insertUserAndBook(final User user, final Book book) {
         __db.beginTransaction();
         try {
             __insertionAdapterOfUser.insert(user);
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt
index 98b18be..9311bff 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt
@@ -55,6 +55,15 @@
     @Insert
     fun addPublishersMaybe(vararg publishers: Publisher): Maybe<List<Long>>
 
+    @Insert
+    fun addPublisherSingle(publisher: Publisher): Single<Long>
+
+    @Insert
+    fun addPublisherCompletable(publisher: Publisher): Completable
+
+    @Insert
+    fun addPublisherMaybe(publisher: Publisher): Maybe<Long>
+
     @Delete
     fun deletePublishers(vararg publishers: Publisher)
 
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/UserDao.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/UserDao.java
index aa463f8..c7aa469 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/UserDao.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/UserDao.java
@@ -33,6 +33,7 @@
 import androidx.room.integration.testapp.vo.Day;
 import androidx.room.integration.testapp.vo.NameAndLastName;
 import androidx.room.integration.testapp.vo.User;
+import androidx.room.integration.testapp.vo.UserSummary;
 import androidx.sqlite.db.SupportSQLiteQuery;
 
 import org.reactivestreams.Publisher;
@@ -42,6 +43,7 @@
 import java.util.Set;
 import java.util.concurrent.Callable;
 
+import io.reactivex.Completable;
 import io.reactivex.Flowable;
 import io.reactivex.Maybe;
 import io.reactivex.Observable;
@@ -88,6 +90,15 @@
     public abstract int update(User user);
 
     @Update
+    public abstract Completable updateCompletable(User user);
+
+    @Update
+    public abstract Single<Integer> updateSingle(User user);
+
+    @Update
+    public abstract Single<Integer> updateSingleUsers(User user1, User user2);
+
+    @Update
     public abstract int updateAll(List<User> users);
 
     @Insert
@@ -248,4 +259,8 @@
             + "OR mName LIKE '%' || 'video' || '%' "
             + "OR mName LIKE '%' || 'games' || '%' ")
     public abstract List<User> getUserWithCoolNames();
+
+    // The subquery is intentional (b/118398616)
+    @Query("SELECT `mId`, `mName` FROM (SELECT * FROM User)")
+    public abstract List<UserSummary> getNames();
 }
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/SimpleEntityReadWriteTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/SimpleEntityReadWriteTest.java
index cbe76b0..2f05f72 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/SimpleEntityReadWriteTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/SimpleEntityReadWriteTest.java
@@ -46,6 +46,7 @@
 import androidx.room.integration.testapp.vo.Product;
 import androidx.room.integration.testapp.vo.User;
 import androidx.room.integration.testapp.vo.UserAndAllPets;
+import androidx.room.integration.testapp.vo.UserSummary;
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -628,6 +629,16 @@
 
     }
 
+    @Test
+    public void subquery() {
+        User user = TestUtil.createUser(3);
+        user.setName("john");
+        mUserDao.insert(user);
+        List<UserSummary> users = mUserDao.getNames();
+        assertThat(users, hasSize(1));
+        assertThat(users.get(0).getName(), is(equalTo("john")));
+    }
+
     private Set<Day> toSet(Day... days) {
         return new HashSet<>(Arrays.asList(days));
     }
diff --git a/lifecycle/savedstate-core/src/main/java/androidx/lifecycle/SavedStateRegistryOwner.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/UserSummary.java
similarity index 63%
copy from lifecycle/savedstate-core/src/main/java/androidx/lifecycle/SavedStateRegistryOwner.java
copy to room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/UserSummary.java
index 9ac507b..c76e0b1 100644
--- a/lifecycle/savedstate-core/src/main/java/androidx/lifecycle/SavedStateRegistryOwner.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/UserSummary.java
@@ -14,20 +14,27 @@
  * limitations under the License.
  */
 
-package androidx.lifecycle;
+package androidx.room.integration.testapp.vo;
 
+public class UserSummary {
 
-import androidx.annotation.NonNull;
+    private int mId;
 
-/**
- * A scope that owns {@link SavedStateRegistry}
- */
-public interface SavedStateRegistryOwner {
-    /**
-     * Returns owned {@link SavedStateRegistry}
-     *
-     * @return a {@link SavedStateRegistry}
-     */
-    @NonNull
-    SavedStateRegistry getSavedState();
+    private String mName;
+
+    public int getId() {
+        return mId;
+    }
+
+    public void setId(int id) {
+        mId = id;
+    }
+
+    public String getName() {
+        return mName;
+    }
+
+    public void setName(String name) {
+        mName = name;
+    }
 }
diff --git a/room/runtime/src/main/java/androidx/room/util/CursorUtil.java b/room/runtime/src/main/java/androidx/room/util/CursorUtil.java
index 317029d..53ef36c 100644
--- a/room/runtime/src/main/java/androidx/room/util/CursorUtil.java
+++ b/room/runtime/src/main/java/androidx/room/util/CursorUtil.java
@@ -75,6 +75,39 @@
         return matrixCursor;
     }
 
+    /**
+     * Patches {@link Cursor#getColumnIndex(String)} to work around issues on older devices.
+     * If the column is not found, it retries with the specified name surrounded by backticks.
+     *
+     * @param c    The cursor.
+     * @param name The name of the target column.
+     * @return The index of the column, or -1 if not found.
+     */
+    public static int getColumnIndex(@NonNull Cursor c, @NonNull String name) {
+        final int index = c.getColumnIndex(name);
+        if (index >= 0) {
+            return index;
+        }
+        return c.getColumnIndex("`" + name + "`");
+    }
+
+    /**
+     * Patches {@link Cursor#getColumnIndexOrThrow(String)} to work around issues on older devices.
+     * If the column is not found, it retries with the specified name surrounded by backticks.
+     *
+     * @param c    The cursor.
+     * @param name The name of the target column.
+     * @return The index of the column.
+     * @throws IllegalArgumentException if the column does not exist.
+     */
+    public static int getColumnIndexOrThrow(@NonNull Cursor c, @NonNull String name) {
+        final int index = c.getColumnIndex(name);
+        if (index >= 0) {
+            return index;
+        }
+        return c.getColumnIndexOrThrow("`" + name + "`");
+    }
+
     private CursorUtil() {
     }
 }
diff --git a/samples/SupportCarDemos/src/main/AndroidManifest.xml b/samples/SupportCarDemos/src/main/AndroidManifest.xml
index a10987d..9e7b234 100644
--- a/samples/SupportCarDemos/src/main/AndroidManifest.xml
+++ b/samples/SupportCarDemos/src/main/AndroidManifest.xml
@@ -32,7 +32,7 @@
 
         <activity android:name=".PagedListViewActivity"
                   android:label="@string/paged_list_view_title"
-                  android:parentActivityName=".SupportCarDemoActivity">
+                  android:parentActivityName=".SupportCarDemoActivity" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.SAMPLE_CODE" />
@@ -213,5 +213,18 @@
                 android:name="android.support.PARENT_ACTIVITY"
                 android:value=".SupportCarDemoActivity"/>
         </activity>
+
+        <activity
+            android:name=".DefaultActionBarDemoActivity"
+            android:label="Default Action Bar"
+            android:parentActivityName=".SupportCarDemoActivity"
+            android:theme="@style/Theme.Car">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.SAMPLE_CODE" />
+            </intent-filter>
+            <meta-data android:name="android.support.PARENT_ACTIVITY"
+                       android:value=".SupportCarDemoActivity" />
+        </activity>
     </application>
 </manifest>
\ No newline at end of file
diff --git a/samples/SupportCarDemos/src/main/java/com/example/androidx/car/DefaultActionBarDemoActivity.java b/samples/SupportCarDemos/src/main/java/com/example/androidx/car/DefaultActionBarDemoActivity.java
new file mode 100644
index 0000000..77462be
--- /dev/null
+++ b/samples/SupportCarDemos/src/main/java/com/example/androidx/car/DefaultActionBarDemoActivity.java
@@ -0,0 +1,36 @@
+/*
+ * 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 com.example.androidx.car;
+
+import android.os.Bundle;
+import android.widget.FrameLayout;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+/**
+ * A demo activity that demonstrates the styling of the default
+ * {@link androidx.appcompat.app.ActionBar}.
+ */
+public class DefaultActionBarDemoActivity extends AppCompatActivity {
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        // Set an empty view since we're only interested in the action bar styling.
+        setContentView(new FrameLayout(/* context= */ this));
+    }
+}
diff --git a/settings.gradle b/settings.gradle
index beea444..c056267 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -177,7 +177,6 @@
 includeProject(":webkit", "webkit")
 includeProject(":webkit:integration-tests:testapp", "webkit/integration-tests/testapp")
 includeProject(":work:work-runtime", "work/workmanager")
-includeProject(":work:work-runtime-sync", "work/workmanager-sync")
 includeProject(":work:work-runtime-ktx", "work/workmanager-ktx")
 includeProject(":work:work-firebase", "work/workmanager-firebase")
 includeProject(":work:work-testing", "work/workmanager-testing")
diff --git a/slices/benchmark/build.gradle b/slices/benchmark/build.gradle
index ee39ae2..e57e33d 100644
--- a/slices/benchmark/build.gradle
+++ b/slices/benchmark/build.gradle
@@ -38,7 +38,7 @@
 supportLibrary {
     name = "Slices Benchmarks"
     publish = false
-    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenVersion = LibraryVersions.SLICE_BENCHMARK
     mavenGroup = LibraryGroups.SLICE
     inceptionYear = "2018"
     description = "RecyclerView Benchmarks"
diff --git a/slices/builders/api/1.1.0-alpha01.txt b/slices/builders/api/1.1.0-alpha01.txt
index 820e4ce..291321c 100644
--- a/slices/builders/api/1.1.0-alpha01.txt
+++ b/slices/builders/api/1.1.0-alpha01.txt
@@ -31,6 +31,7 @@
     method public androidx.slice.builders.ListBuilder addInputRange(androidx.slice.builders.ListBuilder.InputRangeBuilder);
     method public androidx.slice.builders.ListBuilder addRange(androidx.slice.builders.ListBuilder.RangeBuilder);
     method public androidx.slice.builders.ListBuilder addRow(androidx.slice.builders.ListBuilder.RowBuilder);
+    method public androidx.slice.builders.ListBuilder addSelection(androidx.slice.builders.SelectionBuilder);
     method public androidx.slice.builders.ListBuilder setAccentColor(@ColorInt int);
     method public androidx.slice.builders.ListBuilder setHeader(androidx.slice.builders.ListBuilder.HeaderBuilder);
     method public androidx.slice.builders.ListBuilder setIsError(boolean);
diff --git a/slices/builders/api/current.txt b/slices/builders/api/current.txt
index 820e4ce..291321c 100644
--- a/slices/builders/api/current.txt
+++ b/slices/builders/api/current.txt
@@ -31,6 +31,7 @@
     method public androidx.slice.builders.ListBuilder addInputRange(androidx.slice.builders.ListBuilder.InputRangeBuilder);
     method public androidx.slice.builders.ListBuilder addRange(androidx.slice.builders.ListBuilder.RangeBuilder);
     method public androidx.slice.builders.ListBuilder addRow(androidx.slice.builders.ListBuilder.RowBuilder);
+    method public androidx.slice.builders.ListBuilder addSelection(androidx.slice.builders.SelectionBuilder);
     method public androidx.slice.builders.ListBuilder setAccentColor(@ColorInt int);
     method public androidx.slice.builders.ListBuilder setHeader(androidx.slice.builders.ListBuilder.HeaderBuilder);
     method public androidx.slice.builders.ListBuilder setIsError(boolean);
diff --git a/slices/builders/src/main/java/androidx/slice/builders/ListBuilder.java b/slices/builders/src/main/java/androidx/slice/builders/ListBuilder.java
index 083f0c7..1145d2a 100644
--- a/slices/builders/src/main/java/androidx/slice/builders/ListBuilder.java
+++ b/slices/builders/src/main/java/androidx/slice/builders/ListBuilder.java
@@ -457,6 +457,15 @@
     }
 
     /**
+     * Add a selection row to the list builder.
+     */
+    @NonNull
+    public ListBuilder addSelection(@NonNull SelectionBuilder selectionBuilder) {
+        mImpl.addSelection(selectionBuilder);
+        return this;
+    }
+
+    /**
      * Builder to construct a range row which can be added to a {@link ListBuilder}.
      * <p>
      * A range row supports displaying a horizontal progress indicator.
diff --git a/slices/builders/src/main/java/androidx/slice/builders/impl/ListBuilder.java b/slices/builders/src/main/java/androidx/slice/builders/impl/ListBuilder.java
index 93e1b69..62fcc98 100644
--- a/slices/builders/src/main/java/androidx/slice/builders/impl/ListBuilder.java
+++ b/slices/builders/src/main/java/androidx/slice/builders/impl/ListBuilder.java
@@ -29,6 +29,7 @@
 import androidx.slice.builders.ListBuilder.InputRangeBuilder;
 import androidx.slice.builders.ListBuilder.RangeBuilder;
 import androidx.slice.builders.ListBuilder.RowBuilder;
+import androidx.slice.builders.SelectionBuilder;
 import androidx.slice.builders.SliceAction;
 
 import java.time.Duration;
@@ -75,6 +76,11 @@
     void addRange(RangeBuilder builder);
 
     /**
+     * Add a selection row to the list builder.
+     */
+    void addSelection(SelectionBuilder builder);
+
+    /**
      * If all content in a slice cannot be shown, the row added here will be displayed where the
      * content is cut off. This row should have an affordance to take the user to an activity to
      * see all of the content.
diff --git a/slices/builders/src/main/java/androidx/slice/builders/impl/ListBuilderBasicImpl.java b/slices/builders/src/main/java/androidx/slice/builders/impl/ListBuilderBasicImpl.java
index 6abdbda..4fad6d9 100644
--- a/slices/builders/src/main/java/androidx/slice/builders/impl/ListBuilderBasicImpl.java
+++ b/slices/builders/src/main/java/androidx/slice/builders/impl/ListBuilderBasicImpl.java
@@ -44,6 +44,7 @@
 import androidx.slice.builders.ListBuilder.InputRangeBuilder;
 import androidx.slice.builders.ListBuilder.RangeBuilder;
 import androidx.slice.builders.ListBuilder.RowBuilder;
+import androidx.slice.builders.SelectionBuilder;
 import androidx.slice.builders.SliceAction;
 
 import java.time.Duration;
@@ -171,6 +172,19 @@
         }
     }
 
+    @Override
+    public void addSelection(SelectionBuilder builder) {
+        if (mTitle == null && builder.getTitle() != null) {
+            mTitle = builder.getTitle();
+        }
+        if (mSubtitle == null && builder.getSubtitle() != null) {
+            mSubtitle = builder.getSubtitle();
+        }
+        if (mSliceAction == null && builder.getPrimaryAction() != null) {
+            mSliceAction = builder.getPrimaryAction();
+        }
+    }
+
     /**
      */
     @Override
diff --git a/slices/builders/src/main/java/androidx/slice/builders/impl/ListBuilderV1Impl.java b/slices/builders/src/main/java/androidx/slice/builders/impl/ListBuilderV1Impl.java
index f64b9a1..c7e51a7 100644
--- a/slices/builders/src/main/java/androidx/slice/builders/impl/ListBuilderV1Impl.java
+++ b/slices/builders/src/main/java/androidx/slice/builders/impl/ListBuilderV1Impl.java
@@ -66,6 +66,7 @@
 import androidx.slice.builders.ListBuilder.InputRangeBuilder;
 import androidx.slice.builders.ListBuilder.RangeBuilder;
 import androidx.slice.builders.ListBuilder.RowBuilder;
+import androidx.slice.builders.SelectionBuilder;
 import androidx.slice.builders.SliceAction;
 import androidx.slice.core.SliceQuery;
 
@@ -222,6 +223,12 @@
         getBuilder().addSubSlice(impl.build(), SUBTYPE_RANGE);
     }
 
+    @Override
+    public void addSelection(SelectionBuilder builder) {
+        SelectionBuilderImpl impl = new SelectionBuilderV1Impl(createChildBuilder(), builder);
+        getBuilder().addSubSlice(impl.build());
+    }
+
     /**
      */
     @Override
diff --git a/slices/view/src/androidTest/java/androidx/slice/SliceBuilderTest.java b/slices/view/src/androidTest/java/androidx/slice/SliceBuilderTest.java
index 9fa3938..3f836a6 100644
--- a/slices/view/src/androidTest/java/androidx/slice/SliceBuilderTest.java
+++ b/slices/view/src/androidTest/java/androidx/slice/SliceBuilderTest.java
@@ -34,6 +34,7 @@
 import androidx.core.graphics.drawable.IconCompat;
 import androidx.slice.builders.GridRowBuilder;
 import androidx.slice.builders.ListBuilder;
+import androidx.slice.builders.SelectionBuilder;
 import androidx.slice.builders.SliceAction;
 import androidx.slice.render.SliceRenderActivity;
 import androidx.slice.widget.SliceLiveData;
@@ -193,6 +194,37 @@
         lb.build();
     }
 
+    @Test(expected = IllegalArgumentException.class)
+    public void testThrowSelectionNoPrimaryAction() {
+        ListBuilder lb = new ListBuilder(mContext, mUri, INFINITY);
+        lb.addSelection(new SelectionBuilder()
+                .setTitle("Title")
+                .setSubtitle("Subtitle")
+                .setInputAction(getIntent("")));
+        lb.build();
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testThrowSelectionNoInputAction() {
+        ListBuilder lb = new ListBuilder(mContext, mUri, INFINITY);
+        lb.addSelection(new SelectionBuilder()
+                .setTitle("Title")
+                .setSubtitle("Subtitle")
+                .setPrimaryAction(getAction("action")));
+        lb.build();
+    }
+
+    @Test
+    public void testNoThrowSelection() {
+        ListBuilder lb = new ListBuilder(mContext, mUri, INFINITY);
+        lb.addSelection(new SelectionBuilder()
+                .setTitle("Title")
+                .setSubtitle("Subtitle")
+                .setPrimaryAction(getAction("action"))
+                .setInputAction(getIntent("")));
+        lb.build();
+    }
+
     @Test
     public void testNoThrowNoPrimaryActionWhileLoading() {
         ListBuilder lb = new ListBuilder(mContext, mUri, INFINITY);
diff --git a/slices/view/src/main/java/androidx/slice/widget/LargeSliceAdapter.java b/slices/view/src/main/java/androidx/slice/widget/LargeSliceAdapter.java
index 58a31e6..548074d 100644
--- a/slices/view/src/main/java/androidx/slice/widget/LargeSliceAdapter.java
+++ b/slices/view/src/main/java/androidx/slice/widget/LargeSliceAdapter.java
@@ -265,20 +265,17 @@
     }
 
     private View inflateForType(int viewType) {
-        View v = new RowView(mContext);
         switch (viewType) {
             case TYPE_GRID:
-                v = LayoutInflater.from(mContext).inflate(R.layout.abc_slice_grid, null);
-                break;
+                return LayoutInflater.from(mContext).inflate(R.layout.abc_slice_grid, null);
             case TYPE_MESSAGE:
-                v = LayoutInflater.from(mContext).inflate(R.layout.abc_slice_message, null);
-                break;
+                return LayoutInflater.from(mContext).inflate(R.layout.abc_slice_message, null);
             case TYPE_MESSAGE_LOCAL:
-                v = LayoutInflater.from(mContext).inflate(R.layout.abc_slice_message_local,
+                return LayoutInflater.from(mContext).inflate(R.layout.abc_slice_message_local,
                         null);
-                break;
+            default:
+                return new RowView(mContext);
         }
-        return v;
     }
 
     protected static class SliceWrapper {
diff --git a/slidingpanelayout/build.gradle b/slidingpanelayout/build.gradle
index 81730bd..44b08bc 100644
--- a/slidingpanelayout/build.gradle
+++ b/slidingpanelayout/build.gradle
@@ -14,7 +14,7 @@
 supportLibrary {
     name = "Android Support Library Sliding Pane Layout"
     publish = true
-    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenVersion = LibraryVersions.SLIDINGPANELAYOUT
     mavenGroup = LibraryGroups.SLIDINGPANELAYOUT
     inceptionYear = "2018"
     description = "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren't a part of the framework APIs. Compatible on devices running API 14 or later."
diff --git a/studiow b/studiow
new file mode 100755
index 0000000..5da0362
--- /dev/null
+++ b/studiow
@@ -0,0 +1,96 @@
+#!/bin/bash
+set -e
+
+# This is a wrapper script that runs the specific version of Android Studio that is recommended for developing in this repository.
+# (This serves a similar purpose to gradlew)
+
+
+function getStudioUrl() {
+  osName="$1"
+  buildNumber="5056338"
+  studioUrl="https://dl.google.com/dl/android/studio/ide-zips/3.2.1.0/android-studio-ide-181.${buildNumber}-${osName}.zip"
+  echo "${studioUrl}"
+}
+
+acceptsLicenseAgreement="$1"
+scriptDir="$(cd $(dirname $0) && pwd)"
+tempDir="${scriptDir}/studio"
+function getOsName() {
+  unameOutput="$(uname)"
+  osName=""
+  if [ "${unameOutput}" == "Linux" ]; then
+    osName="linux"
+  else
+    osName="mac"
+  fi
+  echo "${osName}"
+}
+osName="$(getOsName)"
+studioUrl="$(getStudioUrl $osName)"
+studioDestName="$(basename ${studioUrl})"
+studioZipPath="${tempDir}/${studioDestName}"
+studioUnzippedPath="$(echo ${studioZipPath} | sed 's/\.zip$//')"
+
+function downloadFile() {
+  fromUrl="$1"
+  destPath="$2"
+  tempPath="${destPath}.tmp"
+  echo "Downloading ${fromUrl} to ${destPath}"
+  curl "${fromUrl}" > "${tempPath}"
+  mv "${tempPath}" "${destPath}"
+}
+
+function checkLicenseAgreement() {
+  # TODO: Is there a more official way to check that the user accepts the license?
+  if [ "${acceptsLicenseAgreement}" != "-y" ]; then
+    echo "Do you accept the license agreement at ${studioUnzippedPath}/android-studio/LICENSE.txt ?"
+    echo "If you do, then rerun this script with a '-y' argument"
+    exit 1
+  fi
+}
+
+function updateStudio() {
+  # skip if already up-to-date
+  if stat "${studioZipPath}" >/dev/null 2>/dev/null; then
+    # already up-to-date
+    return
+  fi
+
+  mkdir -p "${tempDir}"
+  downloadFile "${studioUrl}" "${studioZipPath}"
+  echo
+
+  echo "Removing previous installations"
+  ls "${tempDir}" | grep -v "^${studioDestName}\$" | sed "s|^|${tempDir}/|" | xargs rm -rf
+
+  echo "Unzipping"
+  unzip "${studioZipPath}" -d "${studioUnzippedPath}"
+}
+
+function runStudioLinux() {
+  studioPath="${studioUnzippedPath}/android-studio/bin/studio.sh"
+  echo "$studioPath &"
+  "${studioPath}" &
+}
+
+function runStudioMac() {
+  studioPath="${studioUnzippedPath}/Android Studio.app"
+  echo "open ${studioPath}"
+  open "${studioPath}"
+}
+
+function runStudio() {
+  if [ "${osName}" == "mac" ]; then
+    runStudioMac
+  else
+    runStudioLinux
+  fi
+}
+
+function main() {
+  updateStudio
+  checkLicenseAgreement
+  runStudio
+}
+
+main
diff --git a/testutils/src/main/java/androidx/testutils/SwipeToLocation.java b/testutils/src/main/java/androidx/testutils/SwipeToLocation.java
new file mode 100644
index 0000000..68a30dd
--- /dev/null
+++ b/testutils/src/main/java/androidx/testutils/SwipeToLocation.java
@@ -0,0 +1,266 @@
+/*
+ * 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.testutils;
+
+import static androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast;
+
+import android.graphics.Rect;
+import android.os.SystemClock;
+import android.view.MotionEvent;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.test.espresso.InjectEventSecurityException;
+import androidx.test.espresso.PerformException;
+import androidx.test.espresso.UiController;
+import androidx.test.espresso.ViewAction;
+import androidx.test.espresso.action.CoordinatesProvider;
+
+import org.hamcrest.Matcher;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Swipes the view on which the action is performed to the given top-left coordinates. It is
+ * required that the view moves along with the swipe, as it would list views (e.g., a RecyclerView).
+ *
+ * Provides two different ways to provide the target coordinates: either the center of the view's
+ * parent ({@link #swipeToCenter()} and {@link #flingToCenter()}), or fixed coordinates ({@link
+ * #swipeTo(float[])} and {@link #flingTo(float[])})
+ */
+public class SwipeToLocation implements ViewAction {
+
+    private static CoordinatesProvider sCenterInParent = new CoordinatesProvider() {
+        @Override
+        public float[] calculateCoordinates(View view) {
+            View parent = (View) view.getParent();
+
+            int horizontalPadding = parent.getPaddingLeft() + parent.getPaddingRight();
+            int verticalPadding = parent.getPaddingTop() + parent.getPaddingBottom();
+            int widthParent = parent.getWidth() - horizontalPadding;
+            int heightParent = parent.getHeight() - verticalPadding;
+            int widthView = view.getWidth();
+            int heightView = view.getHeight();
+
+            float[] coords = new float[2];
+            coords[X] = (widthParent - widthView) / 2;
+            coords[Y] = (heightParent - heightView) / 2;
+            return coords;
+        }
+
+        @NonNull
+        @Override
+        public String toString() {
+            return "center in parent";
+        }
+    };
+
+    private static class FixedCoordinates implements CoordinatesProvider {
+        private final float[] mCoordinates;
+
+        FixedCoordinates(float[] coordinates) {
+            mCoordinates = coordinates;
+        }
+
+        @Override
+        public float[] calculateCoordinates(View view) {
+            return mCoordinates;
+        }
+
+        @NonNull
+        @Override
+        public String toString() {
+            return String.format("fixed coordinates (%f, %f)", mCoordinates[X], mCoordinates[Y]);
+        }
+    }
+
+    private static final int X = 0;
+    private static final int Y = 1;
+
+    private final CoordinatesProvider mCoordinatesProvider;
+    private final int mDuration;
+    private final int mSteps;
+
+    private SwipeToLocation(CoordinatesProvider coordinatesProvider, int duration, int steps) {
+        mCoordinatesProvider = coordinatesProvider;
+        mDuration = duration;
+        mSteps = steps;
+    }
+
+    /**
+     * Swipe the view to the given target location. Swiping takes 1 second to complete.
+     *
+     * @param targetLocation The top-left target coordinates of the view
+     * @return The ViewAction to use in {@link
+     * androidx.test.espresso.ViewInteraction#perform(ViewAction...)}
+     */
+    public static SwipeToLocation swipeTo(float[] targetLocation) {
+        return new SwipeToLocation(new FixedCoordinates(targetLocation), 1000, 10);
+    }
+
+    /**
+     * Fling the view to the given target location. Flinging takes 0.1 seconds to complete.
+     *
+     * @param targetLocation The top-left target coordinates of the view
+     * @return The ViewAction to use in {@link
+     * androidx.test.espresso.ViewInteraction#perform(ViewAction...)}
+     */
+    public static SwipeToLocation flingTo(float[] targetLocation) {
+        return new SwipeToLocation(new FixedCoordinates(targetLocation), 100, 10);
+    }
+
+    /**
+     * Swipe the view to the center of its parent. Swiping takes 1 second to complete.
+     *
+     * @return The ViewAction to use in {@link
+     * androidx.test.espresso.ViewInteraction#perform(ViewAction...)}
+     */
+    public static SwipeToLocation swipeToCenter() {
+        return new SwipeToLocation(sCenterInParent, 1000, 10);
+    }
+
+    /**
+     * Fling the view to the center of its parent. Flinging takes 0.1 seconds to complete.
+     *
+     * @return The ViewAction to use in {@link
+     * androidx.test.espresso.ViewInteraction#perform(ViewAction...)}
+     */
+    public static SwipeToLocation flingToCenter() {
+        return new SwipeToLocation(sCenterInParent, 100, 10);
+    }
+
+    @Override
+    public Matcher<View> getConstraints() {
+        return isDisplayingAtLeast(10);
+    }
+
+    @Override
+    public String getDescription() {
+        return String.format(Locale.US, "Swiping view to location %s", mCoordinatesProvider);
+    }
+
+    @Override
+    public void perform(UiController uiController, View view) {
+        float[] swipeStart = getCenterOfView(view);
+        float[] targetCoordinates = mCoordinatesProvider.calculateCoordinates(view);
+        sendOnlineSwipe(uiController, view, targetCoordinates, swipeStart, mDuration, mSteps);
+    }
+
+    /**
+     * Inject motion events to emulate a swipe to the target location. Instead of calculating all
+     * events up front and then injecting them one by one, perform the required number of steps and
+     * determine the distance to cover in the current step based on the current distance of the view
+     * to the target. This makes it robust against movements of the view during the event sequence.
+     * This is for example likely to happen between the down event and the first move event if we're
+     * interrupting a smooth scroll.
+     *
+     * @param uiController The controller to inject the motion events with
+     * @param view The view to swipe on
+     * @param targetViewLocation The view location where we want the view to end
+     * @param swipeStart The pointer location where we start the swipe, must be on the view
+     * @param duration The duration in milliseconds of the swipe gesture
+     * @param steps The number of move motion events that will be sent for the gesture
+     */
+    private void sendOnlineSwipe(UiController uiController, View view, float[] targetViewLocation,
+            float[] swipeStart, int duration, int steps) {
+        final long startTime = SystemClock.uptimeMillis();
+        long eventTime = startTime;
+        float[] pointerLocation = new float[]{swipeStart[X], swipeStart[Y]};
+        float[] viewLocation = new float[2];
+        float[] nextViewLocation = new float[2];
+        List<MotionEvent> events = new ArrayList<>();
+        try {
+            // Down event
+            MotionEvent downEvent = obtainDownEvent(startTime, pointerLocation);
+            events.add(downEvent);
+            injectMotionEvent(uiController, downEvent);
+
+            // Move events
+            for (int i = 1; i <= steps; i++) {
+                eventTime = startTime + duration * i / duration;
+                getCurrentCoords(view, viewLocation);
+                lerp(viewLocation, targetViewLocation, 1f / (steps - i + 1), nextViewLocation);
+                updatePointerLocation(pointerLocation, viewLocation, nextViewLocation);
+
+                MotionEvent moveEvent = obtainMoveEvent(startTime, eventTime, pointerLocation);
+                events.add(moveEvent);
+                injectMotionEvent(uiController, moveEvent);
+            }
+
+            // Up event
+            MotionEvent upEvent = obtainUpEvent(startTime, eventTime, pointerLocation);
+            events.add(upEvent);
+            injectMotionEvent(uiController, upEvent);
+        } catch (Exception e) {
+            throw new PerformException.Builder().withActionDescription("Perform swipe")
+                    .withViewDescription("unknown").withCause(e).build();
+        } finally {
+            for (MotionEvent event : events) {
+                event.recycle();
+            }
+        }
+    }
+
+    private static MotionEvent obtainDownEvent(long time, float[] coord) {
+        return MotionEvent.obtain(time, time,
+                MotionEvent.ACTION_DOWN, coord[X], coord[Y], 0);
+    }
+
+    private static MotionEvent obtainMoveEvent(long startTime, long elapsedTime, float[] coord) {
+        return MotionEvent.obtain(startTime, elapsedTime,
+                MotionEvent.ACTION_MOVE, coord[X], coord[Y], 0);
+    }
+
+    private static MotionEvent obtainUpEvent(long startTime, long elapsedTime, float[] coord) {
+        return MotionEvent.obtain(startTime, elapsedTime,
+                MotionEvent.ACTION_UP, coord[X], coord[Y], 0);
+    }
+
+    private static void injectMotionEvent(UiController uiController, MotionEvent event)
+            throws InjectEventSecurityException {
+        while (event.getEventTime() - SystemClock.uptimeMillis() > 10) {
+            // Because the loopMainThreadForAtLeast is overkill for waiting, intentionally only
+            // call it with a smaller amount of milliseconds as best effort
+            uiController.loopMainThreadForAtLeast(10);
+        }
+        uiController.injectMotionEvent(event);
+    }
+
+    private void updatePointerLocation(float[] pointerLocation, float[] viewLocation,
+            float[] nextViewLocation) {
+        pointerLocation[X] += nextViewLocation[X] - viewLocation[X];
+        pointerLocation[Y] += nextViewLocation[Y] - viewLocation[Y];
+    }
+
+    private static float[] getCenterOfView(View view) {
+        Rect r = new Rect();
+        view.getGlobalVisibleRect(r);
+        return new float[]{r.centerX(), r.centerY()};
+    }
+
+    private static void getCurrentCoords(View view, float[] out) {
+        out[X] = view.getLeft();
+        out[Y] = view.getTop();
+    }
+
+    private static void lerp(float[] from, float[] to, float f, float[] out) {
+        out[X] = (int) (from[X] + (to[X] - from[X]) * f);
+        out[Y] = (int) (from[Y] + (to[Y] - from[Y]) * f);
+    }
+}
diff --git a/textclassifier/build.gradle b/textclassifier/build.gradle
index 3ce017b..0a81aa3 100644
--- a/textclassifier/build.gradle
+++ b/textclassifier/build.gradle
@@ -18,6 +18,7 @@
     androidTestImplementation(ESPRESSO_CORE, libs.exclude_for_espresso)
     androidTestImplementation(MOCKITO_CORE, libs.exclude_bytebuddy)
     androidTestImplementation(DEXMAKER_MOCKITO, libs.exclude_bytebuddy)
+    androidTestImplementation(GUAVA_LISTENABLE_FUTURE_AVOID_CONFLICT)
 }
 
 android {
diff --git a/transition/api/1.1.0-alpha01.txt b/transition/api/1.1.0-alpha01.txt
index e1151ab..6f25f20 100644
--- a/transition/api/1.1.0-alpha01.txt
+++ b/transition/api/1.1.0-alpha01.txt
@@ -98,6 +98,7 @@
     ctor public Scene(android.view.ViewGroup, android.view.View);
     method public void enter();
     method public void exit();
+    method public static androidx.transition.Scene? getCurrentScene(android.view.View);
     method public static androidx.transition.Scene getSceneForLayout(android.view.ViewGroup, @LayoutRes int, android.content.Context);
     method public android.view.ViewGroup getSceneRoot();
     method public void setEnterAction(Runnable?);
@@ -230,7 +231,7 @@
     method public void captureEndValues(androidx.transition.TransitionValues);
     method public void captureStartValues(androidx.transition.TransitionValues);
     method public int getOrdering();
-    method public androidx.transition.Transition! getTransitionAt(int);
+    method public androidx.transition.Transition? getTransitionAt(int);
     method public int getTransitionCount();
     method public androidx.transition.TransitionSet removeListener(androidx.transition.Transition.TransitionListener);
     method public androidx.transition.TransitionSet removeTarget(@IdRes int);
diff --git a/transition/api/current.txt b/transition/api/current.txt
index e1151ab..6f25f20 100644
--- a/transition/api/current.txt
+++ b/transition/api/current.txt
@@ -98,6 +98,7 @@
     ctor public Scene(android.view.ViewGroup, android.view.View);
     method public void enter();
     method public void exit();
+    method public static androidx.transition.Scene? getCurrentScene(android.view.View);
     method public static androidx.transition.Scene getSceneForLayout(android.view.ViewGroup, @LayoutRes int, android.content.Context);
     method public android.view.ViewGroup getSceneRoot();
     method public void setEnterAction(Runnable?);
@@ -230,7 +231,7 @@
     method public void captureEndValues(androidx.transition.TransitionValues);
     method public void captureStartValues(androidx.transition.TransitionValues);
     method public int getOrdering();
-    method public androidx.transition.Transition! getTransitionAt(int);
+    method public androidx.transition.Transition? getTransitionAt(int);
     method public int getTransitionCount();
     method public androidx.transition.TransitionSet removeListener(androidx.transition.Transition.TransitionListener);
     method public androidx.transition.TransitionSet removeTarget(@IdRes int);
diff --git a/transition/src/androidTest/java/androidx/transition/ChangeBoundsTest.java b/transition/src/androidTest/java/androidx/transition/ChangeBoundsTest.java
index bc19a6a..c438bc2 100644
--- a/transition/src/androidTest/java/androidx/transition/ChangeBoundsTest.java
+++ b/transition/src/androidTest/java/androidx/transition/ChangeBoundsTest.java
@@ -29,6 +29,7 @@
 import android.widget.FrameLayout;
 
 import androidx.annotation.NonNull;
+import androidx.test.annotation.UiThreadTest;
 import androidx.test.filters.MediumTest;
 import androidx.transition.test.R;
 
@@ -68,6 +69,19 @@
         assertThat(endHolder.red, is(below(endHolder.green)));
     }
 
+    @UiThreadTest
+    @Test
+    public void testApplyingBounds() {
+        View view = new View(rule.getActivity());
+
+        ViewUtils.setLeftTopRightBottom(view, 10, 20, 30, 40);
+
+        assertThat(view.getLeft(), is(10));
+        assertThat(view.getTop(), is(20));
+        assertThat(view.getRight(), is(30));
+        assertThat(view.getBottom(), is(40));
+    }
+
     @Test
     public void testSuppressLayoutWhileAnimating() throws Throwable {
         if (Build.VERSION.SDK_INT < 18) {
diff --git a/transition/src/androidTest/java/androidx/transition/SceneTest.java b/transition/src/androidTest/java/androidx/transition/SceneTest.java
index c5a2d18..fa02615 100644
--- a/transition/src/androidTest/java/androidx/transition/SceneTest.java
+++ b/transition/src/androidTest/java/androidx/transition/SceneTest.java
@@ -31,7 +31,7 @@
 import org.junit.Test;
 
 @MediumTest
-public class SceneTest extends BaseTest {
+public class SceneTest extends BaseTransitionTest {
 
     @Test
     public void testGetSceneRoot() {
@@ -125,4 +125,11 @@
                 is(sameInstance(scene)));
     }
 
+    @Test
+    public void testGetCurrentScene() throws Throwable {
+        Scene scene = Scene.getSceneForLayout(mRoot, R.layout.support_scene0, rule.getActivity());
+        enterScene(scene);
+        assertThat(Scene.getCurrentScene(mRoot), is(scene));
+    }
+
 }
diff --git a/transition/src/main/java/androidx/transition/Scene.java b/transition/src/main/java/androidx/transition/Scene.java
index 72846a3..5657a35 100644
--- a/transition/src/main/java/androidx/transition/Scene.java
+++ b/transition/src/main/java/androidx/transition/Scene.java
@@ -190,21 +190,23 @@
      * information is used by Scene to determine whether there is a previous
      * scene which should be exited before the new scene is entered.
      *
-     * @param view The view on which the current scene is being set
+     * @param sceneRoot The view on which the current scene is being set
      */
-    static void setCurrentScene(View view, Scene scene) {
-        view.setTag(R.id.transition_current_scene, scene);
+    static void setCurrentScene(@NonNull View sceneRoot, @Nullable Scene scene) {
+        sceneRoot.setTag(R.id.transition_current_scene, scene);
     }
 
     /**
      * Gets the current {@link Scene} set on the given view. A scene is set on a view
      * only if that view is the scene root.
      *
+     * @param sceneRoot The view on which the current scene will be returned
      * @return The current Scene set on this view. A value of null indicates that
      * no Scene is currently set.
      */
-    static Scene getCurrentScene(View view) {
-        return (Scene) view.getTag(R.id.transition_current_scene);
+    @Nullable
+    public static Scene getCurrentScene(@NonNull View sceneRoot) {
+        return (Scene) sceneRoot.getTag(R.id.transition_current_scene);
     }
 
     /**
diff --git a/transition/src/main/java/androidx/transition/ViewUtilsBase.java b/transition/src/main/java/androidx/transition/ViewUtilsBase.java
index c3dad8f..a383d23 100644
--- a/transition/src/main/java/androidx/transition/ViewUtilsBase.java
+++ b/transition/src/main/java/androidx/transition/ViewUtilsBase.java
@@ -16,14 +16,24 @@
 
 package androidx.transition;
 
+import android.annotation.SuppressLint;
 import android.graphics.Matrix;
+import android.util.Log;
 import android.view.View;
 import android.view.ViewParent;
 
 import androidx.annotation.NonNull;
 
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
 class ViewUtilsBase {
 
+    private static final String TAG = "ViewUtilsBase";
+
+    private static Method sSetFrameMethod;
+    private static boolean sSetFrameFetched;
+
     private float[] mMatrixValues;
 
     public void setTransitionAlpha(@NonNull View view, float alpha) {
@@ -123,10 +133,30 @@
     }
 
     public void setLeftTopRightBottom(View v, int left, int top, int right, int bottom) {
-        v.setLeft(left);
-        v.setTop(top);
-        v.setRight(right);
-        v.setBottom(bottom);
+        fetchSetFrame();
+        if (sSetFrameMethod != null) {
+            try {
+                sSetFrameMethod.invoke(v, left, top, right, bottom);
+            } catch (IllegalAccessException e) {
+                // Do nothing
+            } catch (InvocationTargetException e) {
+                throw new RuntimeException(e.getCause());
+            }
+        }
+    }
+
+    @SuppressLint("PrivateApi")
+    private void fetchSetFrame() {
+        if (!sSetFrameFetched) {
+            try {
+                sSetFrameMethod = View.class.getDeclaredMethod("setFrame",
+                        int.class, int.class, int.class, int.class);
+                sSetFrameMethod.setAccessible(true);
+            } catch (NoSuchMethodException e) {
+                Log.i(TAG, "Failed to retrieve setFrame method", e);
+            }
+            sSetFrameFetched = true;
+        }
     }
 
 }
diff --git a/tv-provider/build.gradle b/tv-provider/build.gradle
index 7bb5478..4eccff9 100644
--- a/tv-provider/build.gradle
+++ b/tv-provider/build.gradle
@@ -24,7 +24,7 @@
 supportLibrary {
     name = "Android Support TV Provider"
     publish = true
-    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenVersion = LibraryVersions.TVPROVIDER
     mavenGroup = LibraryGroups.TVPROVIDER
     inceptionYear = "2017"
     description = "Android Support Library for TV Provider"
diff --git a/versionedparcelable/build.gradle b/versionedparcelable/build.gradle
index 268b72a..899bc2c 100644
--- a/versionedparcelable/build.gradle
+++ b/versionedparcelable/build.gradle
@@ -24,8 +24,8 @@
 }
 
 dependencies {
-    implementation project(":annotation")
-    implementation project(":collection")
+    implementation("androidx.annotation:annotation:1.0.0")
+    implementation("androidx.collection:collection:1.0.0")
 
     androidTestImplementation(TEST_RUNNER)
     androidTestImplementation(MOCKITO_CORE, libs.exclude_bytebuddy)
diff --git a/viewpager/api/1.1.0-alpha01.txt b/viewpager/api/1.1.0-alpha01.txt
new file mode 100644
index 0000000..4343e5d
--- /dev/null
+++ b/viewpager/api/1.1.0-alpha01.txt
@@ -0,0 +1,128 @@
+// Signature format: 2.0
+package androidx.viewpager.widget {
+
+  public abstract class PagerAdapter {
+    ctor public PagerAdapter();
+    method public void destroyItem(android.view.ViewGroup, int, Object);
+    method @Deprecated public void destroyItem(android.view.View, int, Object);
+    method public void finishUpdate(android.view.ViewGroup);
+    method @Deprecated public void finishUpdate(android.view.View);
+    method public abstract int getCount();
+    method public int getItemPosition(Object);
+    method public CharSequence? getPageTitle(int);
+    method public float getPageWidth(int);
+    method public Object instantiateItem(android.view.ViewGroup, int);
+    method @Deprecated public Object instantiateItem(android.view.View, int);
+    method public abstract boolean isViewFromObject(android.view.View, Object);
+    method public void notifyDataSetChanged();
+    method public void registerDataSetObserver(android.database.DataSetObserver);
+    method public void restoreState(android.os.Parcelable?, ClassLoader?);
+    method public android.os.Parcelable? saveState();
+    method public void setPrimaryItem(android.view.ViewGroup, int, Object);
+    method @Deprecated public void setPrimaryItem(android.view.View, int, Object);
+    method public void startUpdate(android.view.ViewGroup);
+    method @Deprecated public void startUpdate(android.view.View);
+    method public void unregisterDataSetObserver(android.database.DataSetObserver);
+    field public static final int POSITION_NONE = -2; // 0xfffffffe
+    field public static final int POSITION_UNCHANGED = -1; // 0xffffffff
+  }
+
+  public class PagerTabStrip extends androidx.viewpager.widget.PagerTitleStrip {
+    ctor public PagerTabStrip(android.content.Context);
+    ctor public PagerTabStrip(android.content.Context, android.util.AttributeSet?);
+    method public boolean getDrawFullUnderline();
+    method @ColorInt public int getTabIndicatorColor();
+    method public void setBackgroundDrawable(android.graphics.drawable.Drawable!);
+    method public void setDrawFullUnderline(boolean);
+    method public void setTabIndicatorColor(@ColorInt int);
+    method public void setTabIndicatorColorResource(@ColorRes int);
+  }
+
+  @androidx.viewpager.widget.ViewPager.DecorView public class PagerTitleStrip extends android.view.ViewGroup {
+    ctor public PagerTitleStrip(android.content.Context);
+    ctor public PagerTitleStrip(android.content.Context, android.util.AttributeSet?);
+    method public int getTextSpacing();
+    method public void setGravity(int);
+    method public void setNonPrimaryAlpha(@FloatRange(from=0.0, to=1.0) float);
+    method public void setTextColor(@ColorInt int);
+    method public void setTextSize(int, float);
+    method public void setTextSpacing(int);
+  }
+
+  public class ViewPager extends android.view.ViewGroup {
+    ctor public ViewPager(android.content.Context);
+    ctor public ViewPager(android.content.Context, android.util.AttributeSet?);
+    method public void addOnAdapterChangeListener(androidx.viewpager.widget.ViewPager.OnAdapterChangeListener);
+    method public void addOnPageChangeListener(androidx.viewpager.widget.ViewPager.OnPageChangeListener);
+    method public boolean arrowScroll(int);
+    method public boolean beginFakeDrag();
+    method protected boolean canScroll(android.view.View!, boolean, int, int, int);
+    method public void clearOnPageChangeListeners();
+    method public void endFakeDrag();
+    method public boolean executeKeyEvent(android.view.KeyEvent);
+    method public void fakeDragBy(float);
+    method public androidx.viewpager.widget.PagerAdapter? getAdapter();
+    method public int getCurrentItem();
+    method public int getOffscreenPageLimit();
+    method public int getPageMargin();
+    method public boolean isDragInGutterEnabled();
+    method public boolean isFakeDragging();
+    method @CallSuper protected void onPageScrolled(int, float, int);
+    method public void onRestoreInstanceState(android.os.Parcelable!);
+    method public android.os.Parcelable! onSaveInstanceState();
+    method public void removeOnAdapterChangeListener(androidx.viewpager.widget.ViewPager.OnAdapterChangeListener);
+    method public void removeOnPageChangeListener(androidx.viewpager.widget.ViewPager.OnPageChangeListener);
+    method public void setAdapter(androidx.viewpager.widget.PagerAdapter?);
+    method public void setCurrentItem(int);
+    method public void setCurrentItem(int, boolean);
+    method public void setDragInGutterEnabled(boolean);
+    method public void setOffscreenPageLimit(int);
+    method @Deprecated public void setOnPageChangeListener(androidx.viewpager.widget.ViewPager.OnPageChangeListener!);
+    method public void setPageMargin(int);
+    method public void setPageMarginDrawable(android.graphics.drawable.Drawable?);
+    method public void setPageMarginDrawable(@DrawableRes int);
+    method public void setPageTransformer(boolean, androidx.viewpager.widget.ViewPager.PageTransformer?);
+    method public void setPageTransformer(boolean, androidx.viewpager.widget.ViewPager.PageTransformer?, int);
+    field public static final int SCROLL_STATE_DRAGGING = 1; // 0x1
+    field public static final int SCROLL_STATE_IDLE = 0; // 0x0
+    field public static final int SCROLL_STATE_SETTLING = 2; // 0x2
+  }
+
+  @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE) @java.lang.annotation.Inherited public static @interface ViewPager.DecorView {
+  }
+
+  public static class ViewPager.LayoutParams extends android.view.ViewGroup.LayoutParams {
+    ctor public ViewPager.LayoutParams();
+    ctor public ViewPager.LayoutParams(android.content.Context!, android.util.AttributeSet!);
+    field public int gravity;
+    field public boolean isDecor;
+  }
+
+  public static interface ViewPager.OnAdapterChangeListener {
+    method public void onAdapterChanged(androidx.viewpager.widget.ViewPager, androidx.viewpager.widget.PagerAdapter?, androidx.viewpager.widget.PagerAdapter?);
+  }
+
+  public static interface ViewPager.OnPageChangeListener {
+    method public void onPageScrollStateChanged(int);
+    method public void onPageScrolled(int, float, @Px int);
+    method public void onPageSelected(int);
+  }
+
+  public static interface ViewPager.PageTransformer {
+    method public void transformPage(android.view.View, float);
+  }
+
+  public static class ViewPager.SavedState extends androidx.customview.view.AbsSavedState {
+    ctor public ViewPager.SavedState(android.os.Parcelable);
+    field public static final android.os.Parcelable.Creator<androidx.viewpager.widget.ViewPager.SavedState>! CREATOR;
+  }
+
+  public static class ViewPager.SimpleOnPageChangeListener implements androidx.viewpager.widget.ViewPager.OnPageChangeListener {
+    ctor public ViewPager.SimpleOnPageChangeListener();
+    method public void onPageScrollStateChanged(int);
+    method public void onPageScrolled(int, float, int);
+    method public void onPageSelected(int);
+  }
+
+}
+
diff --git a/viewpager/api/current.txt b/viewpager/api/current.txt
index de25917..4343e5d 100644
--- a/viewpager/api/current.txt
+++ b/viewpager/api/current.txt
@@ -65,6 +65,7 @@
     method public int getCurrentItem();
     method public int getOffscreenPageLimit();
     method public int getPageMargin();
+    method public boolean isDragInGutterEnabled();
     method public boolean isFakeDragging();
     method @CallSuper protected void onPageScrolled(int, float, int);
     method public void onRestoreInstanceState(android.os.Parcelable!);
@@ -74,6 +75,7 @@
     method public void setAdapter(androidx.viewpager.widget.PagerAdapter?);
     method public void setCurrentItem(int);
     method public void setCurrentItem(int, boolean);
+    method public void setDragInGutterEnabled(boolean);
     method public void setOffscreenPageLimit(int);
     method @Deprecated public void setOnPageChangeListener(androidx.viewpager.widget.ViewPager.OnPageChangeListener!);
     method public void setPageMargin(int);
diff --git a/viewpager/build.gradle b/viewpager/build.gradle
index 1626d52..aa855af 100644
--- a/viewpager/build.gradle
+++ b/viewpager/build.gradle
@@ -21,7 +21,7 @@
 supportLibrary {
     name = "Android Support Library View Pager"
     publish = true
-    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenVersion = LibraryVersions.VIEWPAGER
     mavenGroup = LibraryGroups.VIEWPAGER
     inceptionYear = "2018"
     description = "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren't a part of the framework APIs. Compatible on devices running API 14 or later."
diff --git a/viewpager/src/main/java/androidx/viewpager/widget/ViewPager.java b/viewpager/src/main/java/androidx/viewpager/widget/ViewPager.java
index 08098ed..c067c75 100644
--- a/viewpager/src/main/java/androidx/viewpager/widget/ViewPager.java
+++ b/viewpager/src/main/java/androidx/viewpager/widget/ViewPager.java
@@ -191,6 +191,9 @@
     private int mDefaultGutterSize;
     private int mGutterSize;
     private int mTouchSlop;
+
+    private boolean mDragInGutterEnabled = true;
+
     /**
      * Position of the last motion event.
      */
@@ -1994,7 +1997,26 @@
         }
     }
 
+    /**
+     * @return Whether dragging in the gutter (left and right edges) of the ViewPager is enabled.
+     */
+    public boolean isDragInGutterEnabled() {
+        return mDragInGutterEnabled;
+    }
+
+    /**
+     * Set whether ViewPager should consume drag events if they are within the gutter
+     * (left and right edges) of the ViewPager. The default value {@code false}.
+     * @param enabled true if ViewPager should allow drag in gutter, false otherwise
+     */
+    public void setDragInGutterEnabled(boolean enabled) {
+        mDragInGutterEnabled = enabled;
+    }
+
     private boolean isGutterDrag(float x, float dx) {
+        if (mDragInGutterEnabled) {
+            return false;
+        }
         return (x < mGutterSize && dx > 0) || (x > getWidth() - mGutterSize && dx < 0);
     }
 
diff --git a/viewpager2/build.gradle b/viewpager2/build.gradle
index 5598c79..789032b 100644
--- a/viewpager2/build.gradle
+++ b/viewpager2/build.gradle
@@ -47,7 +47,7 @@
 supportLibrary {
     name = "AndroidX Widget ViewPager2"
     publish = false
-    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenVersion = LibraryVersions.VIEWPAGER2
     mavenGroup = LibraryGroups.VIEWPAGER2
     inceptionYear = "2017"
     description = "AndroidX Widget ViewPager2"
diff --git a/wear/build.gradle b/wear/build.gradle
index 278ce0c..c932d291 100644
--- a/wear/build.gradle
+++ b/wear/build.gradle
@@ -38,7 +38,7 @@
 supportLibrary {
     name = "Android Wear Support UI"
     publish = true
-    mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+    mavenVersion = LibraryVersions.WEAR
     mavenGroup = LibraryGroups.WEAR
     inceptionYear = "2016"
     description = "Android Wear Support UI"
diff --git a/work/integration-tests/testapp/build.gradle b/work/integration-tests/testapp/build.gradle
index 6bbb8031..a00f27f 100644
--- a/work/integration-tests/testapp/build.gradle
+++ b/work/integration-tests/testapp/build.gradle
@@ -19,6 +19,7 @@
 plugins {
     id("AndroidXPlugin")
     id("com.android.application")
+    id("kotlin-android")
 }
 
 project.ext.noDocs = true
@@ -34,7 +35,7 @@
 
 dependencies {
     implementation 'com.android.support.constraint:constraint-layout:1.0.2'
-    implementation project(':work:work-runtime')
+    implementation project(':work:work-runtime-ktx')
     implementation project(':work:work-firebase')
     implementation "android.arch.lifecycle:extensions:1.1.0"
     implementation "android.arch.persistence.room:runtime:1.1.1-rc1"
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/CoroutineSleepWorker.kt b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/CoroutineSleepWorker.kt
new file mode 100644
index 0000000..733bb866
--- /dev/null
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/CoroutineSleepWorker.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.work.integration.testapp
+
+import android.content.Context
+import android.util.Log
+import androidx.work.WorkerParameters
+import androidx.work.CoroutineWorker
+import kotlinx.coroutines.delay
+
+class CoroutineSleepWorker(context: Context, params: WorkerParameters)
+    : CoroutineWorker(context, params) {
+
+    companion object {
+        val sleepTimeKey = "sleep_time"
+        val tag = "CoroutineWorker"
+    }
+
+    override suspend fun doWork(): Payload {
+        val sleepTime = inputData.getLong(sleepTimeKey, 0L)
+        Log.e(tag, "sleeping for $sleepTime")
+        delay(sleepTime)
+        Log.e(tag, "finished sleep; stopped=$isStopped")
+        return Payload(Result.SUCCESS)
+    }
+}
\ No newline at end of file
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/MainActivity.java b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/MainActivity.java
index 011c2b0..05f7332 100644
--- a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/MainActivity.java
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/MainActivity.java
@@ -38,8 +38,8 @@
 import androidx.work.OneTimeWorkRequest;
 import androidx.work.PeriodicWorkRequest;
 import androidx.work.WorkContinuation;
+import androidx.work.WorkInfo;
 import androidx.work.WorkManager;
-import androidx.work.WorkStatus;
 import androidx.work.integration.testapp.imageprocessing.ImageProcessingActivity;
 import androidx.work.integration.testapp.sherlockholmes.AnalyzeSherlockHolmesActivity;
 
@@ -148,6 +148,31 @@
             }
         });
 
+        findViewById(R.id.coroutine_sleep).setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                String delayString = delayInMs.getText().toString();
+                long delay = Long.parseLong(delayString);
+                Log.d(TAG, "Enqueuing job with delay of " + delay + " ms");
+
+                Data inputData = new Data.Builder()
+                        .put("sleep_time", delay)
+                        .build();
+                WorkManager.getInstance().enqueue(
+                        new OneTimeWorkRequest.Builder(CoroutineSleepWorker.class)
+                                .setInputData(inputData)
+                                .addTag("coroutine_sleep")
+                                .build());
+            }
+        });
+
+        findViewById(R.id.coroutine_cancel).setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                WorkManager.getInstance().cancelAllWorkByTag("coroutine_sleep");
+            }
+        });
+
         findViewById(R.id.enqueue_periodic_work).setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
@@ -220,17 +245,17 @@
             @Override
             public void onClick(View view) {
                 WorkManager workManager = WorkManager.getInstance();
-                workManager.getStatusesForUniqueWorkLiveData(REPLACE_COMPLETED_WORK)
-                        .observe(MainActivity.this, new Observer<List<WorkStatus>>() {
+                workManager.getWorkInfosForUniqueWorkLiveData(REPLACE_COMPLETED_WORK)
+                        .observe(MainActivity.this, new Observer<List<WorkInfo>>() {
                             private int mCount;
 
                             @Override
-                            public void onChanged(@Nullable List<WorkStatus> workStatuses) {
-                                if (workStatuses == null) {
+                            public void onChanged(@Nullable List<WorkInfo> workInfos) {
+                                if (workInfos == null) {
                                     return;
                                 }
-                                if (!workStatuses.isEmpty()) {
-                                    WorkStatus status = workStatuses.get(0);
+                                if (!workInfos.isEmpty()) {
+                                    WorkInfo status = workInfos.get(0);
                                     if (status.getState().isFinished()) {
                                         if (mCount < NUM_WORKERS) {
                                             // Enqueue another worker.
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/RetryActivity.java b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/RetryActivity.java
index 4a5543b..08bdb15 100644
--- a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/RetryActivity.java
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/RetryActivity.java
@@ -33,8 +33,8 @@
 import androidx.work.ExistingWorkPolicy;
 import androidx.work.NetworkType;
 import androidx.work.OneTimeWorkRequest;
+import androidx.work.WorkInfo;
 import androidx.work.WorkManager;
-import androidx.work.WorkStatus;
 import androidx.work.WorkerParameters;
 
 import java.util.List;
@@ -65,14 +65,14 @@
             }
         });
 
-        WorkManager.getInstance().getStatusesByTagLiveData("test")
-                .observe(this, new Observer<List<WorkStatus>>() {
+        WorkManager.getInstance().getWorkInfosByTagLiveData("test")
+                .observe(this, new Observer<List<WorkInfo>>() {
                     @Override
-                    public void onChanged(@Nullable List<WorkStatus> workStatuses) {
+                    public void onChanged(@Nullable List<WorkInfo> workInfos) {
                         String text = "";
-                        for (WorkStatus workStatus : workStatuses) {
-                            text = text + "id: " + workStatus.getId().toString().substring(0, 4)
-                                    + " (" + workStatus.getState() + ")\n";
+                        for (WorkInfo workInfo : workInfos) {
+                            text = text + "id: " + workInfo.getId().toString().substring(0, 4)
+                                    + " (" + workInfo.getState() + ")\n";
                         }
 
                         if (text.equals("")) {
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/sherlockholmes/AnalyzeSherlockHolmesActivity.java b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/sherlockholmes/AnalyzeSherlockHolmesActivity.java
index 15448c9..45819b70 100644
--- a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/sherlockholmes/AnalyzeSherlockHolmesActivity.java
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/sherlockholmes/AnalyzeSherlockHolmesActivity.java
@@ -16,8 +16,8 @@
 
 package androidx.work.integration.testapp.sherlockholmes;
 
-import static androidx.work.State.FAILED;
-import static androidx.work.State.SUCCEEDED;
+import static androidx.work.WorkInfo.State.FAILED;
+import static androidx.work.WorkInfo.State.SUCCEEDED;
 
 import android.os.Bundle;
 import android.support.v7.app.AppCompatActivity;
@@ -94,7 +94,7 @@
                 .then(textReducingWork)
                 .enqueue();
 
-        workManager.getStatusByIdLiveData(textReducingWork.getId()).observe(
+        workManager.getWorkInfoByIdLiveData(textReducingWork.getId()).observe(
                 this,
                 status -> {
                     boolean loading = (status != null
diff --git a/work/integration-tests/testapp/src/main/res/layout/activity_main.xml b/work/integration-tests/testapp/src/main/res/layout/activity_main.xml
index c487485..432c11e 100644
--- a/work/integration-tests/testapp/src/main/res/layout/activity_main.xml
+++ b/work/integration-tests/testapp/src/main/res/layout/activity_main.xml
@@ -75,31 +75,46 @@
                 android:layout_marginTop="12dp"
                 android:text="Observe Image URI"/>
 
+        <EditText android:id="@+id/delay_in_ms"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginLeft="16dp"
+                android:layout_marginStart="16dp"
+                android:layout_marginTop="12dp"
+                android:ems="10"
+                android:hint="Delay in ms"
+                android:inputType="number"
+                android:singleLine="true"
+                android:text="0"/>
 
         <LinearLayout
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:orientation="horizontal">
 
-            <EditText android:id="@+id/delay_in_ms"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_marginLeft="16dp"
-                    android:layout_marginStart="16dp"
-                    android:layout_marginTop="12dp"
-                    android:ems="10"
-                    android:hint="Delay in ms"
-                    android:inputType="number"
-                    android:singleLine="true"
-                    android:text="0"/>
-
             <Button android:id="@+id/schedule_delay"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
                     android:layout_marginLeft="8dp"
                     android:layout_marginStart="8dp"
                     android:layout_marginTop="12dp"
-                    android:text="Schedule with Delay"/>
+                    android:text="Schedule w/ Delay"/>
+
+            <Button android:id="@+id/coroutine_sleep"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginLeft="8dp"
+                    android:layout_marginStart="8dp"
+                    android:layout_marginTop="12dp"
+                    android:text="Coroutine Sleep"/>
+
+            <Button android:id="@+id/coroutine_cancel"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginLeft="8dp"
+                    android:layout_marginStart="8dp"
+                    android:layout_marginTop="12dp"
+                    android:text="Coroutine Cancel"/>
 
         </LinearLayout>
 
diff --git a/work/workmanager-firebase/src/androidTest/java/androidx/work/impl/background/firebase/FirebaseJobServiceTest.java b/work/workmanager-firebase/src/androidTest/java/androidx/work/impl/background/firebase/FirebaseJobServiceTest.java
index 8f2946b..f590619 100644
--- a/work/workmanager-firebase/src/androidTest/java/androidx/work/impl/background/firebase/FirebaseJobServiceTest.java
+++ b/work/workmanager-firebase/src/androidTest/java/androidx/work/impl/background/firebase/FirebaseJobServiceTest.java
@@ -34,7 +34,7 @@
 import androidx.test.runner.AndroidJUnit4;
 import androidx.work.Configuration;
 import androidx.work.OneTimeWorkRequest;
-import androidx.work.State;
+import androidx.work.WorkInfo;
 import androidx.work.WorkRequest;
 import androidx.work.impl.WorkDatabase;
 import androidx.work.impl.WorkManagerImpl;
@@ -112,10 +112,10 @@
         Thread.sleep(5000L);
 
         WorkSpecDao workSpecDao = mDatabase.workSpecDao();
-        assertThat(workSpecDao.getState(work.getStringId()), is(State.RUNNING));
+        assertThat(workSpecDao.getState(work.getStringId()), is(WorkInfo.State.RUNNING));
 
         mFirebaseJobService.onStopJob(mockParams);
-        assertThat(workSpecDao.getState(work.getStringId()), is(State.ENQUEUED));
+        assertThat(workSpecDao.getState(work.getStringId()), is(WorkInfo.State.ENQUEUED));
     }
 
     @Test
@@ -141,7 +141,7 @@
 
         JobParameters mockParams = createMockJobParameters(work.getStringId());
         assertThat(mFirebaseJobService.onStartJob(mockParams), is(true));
-        WorkManagerImpl.getInstance().cancelWorkByIdInternal(work.getId()).get();
+        WorkManagerImpl.getInstance().cancelWorkById(work.getId()).getResult().get();
         assertThat(mFirebaseJobService.onStopJob(mockParams), is(false));
     }
 
diff --git a/work/workmanager-ktx/api/1.0.0-alpha11.txt b/work/workmanager-ktx/api/1.0.0-alpha11.txt
index 0947101..0640e2d 100644
--- a/work/workmanager-ktx/api/1.0.0-alpha11.txt
+++ b/work/workmanager-ktx/api/1.0.0-alpha11.txt
@@ -1,6 +1,15 @@
 // Signature format: 2.0
 package androidx.work {
 
+  public abstract class CoroutineWorker extends androidx.work.ListenableWorker {
+    ctor public CoroutineWorker(android.content.Context appContext, androidx.work.WorkerParameters params);
+    method public abstract Object? doWork(kotlin.coroutines.experimental.Continuation<? super androidx.work.ListenableWorker.Payload> p);
+    method public kotlinx.coroutines.CoroutineDispatcher getCoroutineContext();
+    method public final void onStopped();
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Payload> startWork();
+    property public kotlinx.coroutines.CoroutineDispatcher coroutineContext;
+  }
+
   public final class DataKt {
     ctor public DataKt();
     method public static androidx.work.Data workDataOf(kotlin.Pair<java.lang.String,?>... pairs);
diff --git a/work/workmanager-ktx/api/current.txt b/work/workmanager-ktx/api/current.txt
index 0947101..0640e2d 100644
--- a/work/workmanager-ktx/api/current.txt
+++ b/work/workmanager-ktx/api/current.txt
@@ -1,6 +1,15 @@
 // Signature format: 2.0
 package androidx.work {
 
+  public abstract class CoroutineWorker extends androidx.work.ListenableWorker {
+    ctor public CoroutineWorker(android.content.Context appContext, androidx.work.WorkerParameters params);
+    method public abstract Object? doWork(kotlin.coroutines.experimental.Continuation<? super androidx.work.ListenableWorker.Payload> p);
+    method public kotlinx.coroutines.CoroutineDispatcher getCoroutineContext();
+    method public final void onStopped();
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Payload> startWork();
+    property public kotlinx.coroutines.CoroutineDispatcher coroutineContext;
+  }
+
   public final class DataKt {
     ctor public DataKt();
     method public static androidx.work.Data workDataOf(kotlin.Pair<java.lang.String,?>... pairs);
diff --git a/work/workmanager-ktx/build.gradle b/work/workmanager-ktx/build.gradle
index 56b0a35..355e09b 100644
--- a/work/workmanager-ktx/build.gradle
+++ b/work/workmanager-ktx/build.gradle
@@ -38,6 +38,7 @@
 dependencies {
     api project(':work:work-runtime')
     api(KOTLIN_STDLIB)
+    api(KOTLIN_COROUTINES)
 
     androidTestImplementation project(':work:work-testing')
     androidTestImplementation(TEST_RUNNER)
diff --git a/work/workmanager-ktx/src/androidTest/java/androidx/work/CoroutineWorkerTest.kt b/work/workmanager-ktx/src/androidTest/java/androidx/work/CoroutineWorkerTest.kt
new file mode 100644
index 0000000..888f6cc
--- /dev/null
+++ b/work/workmanager-ktx/src/androidTest/java/androidx/work/CoroutineWorkerTest.kt
@@ -0,0 +1,171 @@
+/*
+ * 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.work
+
+import android.arch.core.executor.ArchTaskExecutor
+import android.content.Context
+import android.util.Log
+import androidx.test.InstrumentationRegistry
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import androidx.work.impl.WorkDatabase
+import androidx.work.impl.WorkManagerImpl
+import androidx.work.impl.utils.taskexecutor.TaskExecutor
+import kotlinx.coroutines.asCoroutineDispatcher
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.UUID
+import java.util.concurrent.Executor
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class CoroutineWorkerTest {
+
+    private lateinit var context: Context
+    private lateinit var configuration: Configuration
+    private lateinit var database: WorkDatabase
+    private lateinit var workManagerImpl: WorkManagerImpl
+
+    @Before
+    fun setUp() {
+        ArchTaskExecutor.getInstance()
+            .setDelegate(object : android.arch.core.executor.TaskExecutor() {
+                override fun executeOnDiskIO(runnable: Runnable) {
+                    runnable.run()
+                }
+
+                override fun postToMainThread(runnable: Runnable) {
+                    runnable.run()
+                }
+
+                override fun isMainThread(): Boolean {
+                    return true
+                }
+            })
+
+        context = InstrumentationRegistry.getTargetContext()
+        configuration = Configuration.Builder()
+            .setExecutor(SynchronousExecutor())
+            .build()
+        workManagerImpl = WorkManagerImpl(context, configuration,
+            InstantWorkTaskExecutor()
+        )
+        WorkManagerImpl.setDelegate(workManagerImpl)
+        database = workManagerImpl.getWorkDatabase()
+        Logger.setMinimumLoggingLevel(Log.DEBUG)
+    }
+
+    @After
+    fun tearDown() {
+        WorkManagerImpl.setDelegate(null)
+        ArchTaskExecutor.getInstance().setDelegate(null)
+    }
+
+    @Test
+    fun testCoroutineWorker_basicUsage() {
+        val workerFactory = WorkerFactory.getDefaultWorkerFactory()
+        val worker = workerFactory.createWorkerWithDefaultFallback(
+            context,
+            SynchronousCoroutineWorker::class.java.name,
+            WorkerParameters(
+                UUID.randomUUID(),
+                Data.EMPTY,
+                emptyList(),
+                WorkerParameters.RuntimeExtras(),
+                1,
+                configuration.executor,
+                workManagerImpl.workTaskExecutor,
+                workerFactory)) as SynchronousCoroutineWorker
+
+        assertThat(worker.job.isCompleted, `is`(false))
+
+        val future = worker.startWork()
+        val payload = future.get()
+
+        assertThat(future.isDone, `is`(true))
+        assertThat(future.isCancelled, `is`(false))
+        assertThat(payload.result, `is`(ListenableWorker.Result.SUCCESS))
+        assertThat(payload.outputData.getLong("output", 0L), `is`(999L))
+    }
+
+    @Test
+    fun testCoroutineWorker_cancellingFutureCancelsJob() {
+        val workerFactory = WorkerFactory.getDefaultWorkerFactory()
+        val worker = workerFactory.createWorkerWithDefaultFallback(
+            context,
+            SynchronousCoroutineWorker::class.java.name,
+            WorkerParameters(
+                UUID.randomUUID(),
+                Data.EMPTY,
+                emptyList(),
+                WorkerParameters.RuntimeExtras(),
+                1,
+                configuration.executor,
+                workManagerImpl.workTaskExecutor,
+                workerFactory)) as SynchronousCoroutineWorker
+
+        assertThat(worker.job.isCancelled, `is`(false))
+        worker.future.cancel(true)
+        assertThat(worker.job.isCancelled, `is`(true))
+    }
+
+    class SynchronousExecutor : Executor {
+
+        override fun execute(command: Runnable) {
+            command.run()
+        }
+    }
+
+    class InstantWorkTaskExecutor : TaskExecutor {
+
+        private val mSynchronousExecutor = SynchronousExecutor()
+
+        override fun postToMainThread(runnable: Runnable) {
+            runnable.run()
+        }
+
+        override fun getMainThreadExecutor(): Executor {
+            return mSynchronousExecutor
+        }
+
+        override fun executeOnBackgroundThread(runnable: Runnable) {
+            runnable.run()
+        }
+
+        override fun getBackgroundExecutor(): Executor {
+            return mSynchronousExecutor
+        }
+
+        override fun getBackgroundExecutorThread(): Thread {
+            return Thread.currentThread()
+        }
+    }
+
+    class SynchronousCoroutineWorker(context: Context, params: WorkerParameters) :
+        CoroutineWorker(context, params) {
+
+        override suspend fun doWork(): Payload {
+            return Payload(Result.SUCCESS, workDataOf("output" to 999L))
+        }
+
+        override val coroutineContext = SynchronousExecutor().asCoroutineDispatcher()
+    }
+}
\ No newline at end of file
diff --git a/work/workmanager-sync/src/main/AndroidManifest.xml b/work/workmanager-ktx/src/androidTest/res/values/values.xml
similarity index 76%
rename from work/workmanager-sync/src/main/AndroidManifest.xml
rename to work/workmanager-ktx/src/androidTest/res/values/values.xml
index 1eec653..bb6fa33 100644
--- a/work/workmanager-sync/src/main/AndroidManifest.xml
+++ b/work/workmanager-ktx/src/androidTest/res/values/values.xml
@@ -1,5 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2016 The Android Open Source Project
+  ~ 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.
@@ -14,7 +15,6 @@
   ~ limitations under the License.
   -->
 
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="androidx.work">
-
-</manifest>
+<resources>
+    <bool name="workmanager_test_configuration">true</bool>
+</resources>
diff --git a/work/workmanager-ktx/src/main/java/androidx/work/CoroutineWorker.kt b/work/workmanager-ktx/src/main/java/androidx/work/CoroutineWorker.kt
new file mode 100644
index 0000000..4e7ad61
--- /dev/null
+++ b/work/workmanager-ktx/src/main/java/androidx/work/CoroutineWorker.kt
@@ -0,0 +1,84 @@
+/*
+ * 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.work
+
+import android.content.Context
+import androidx.work.impl.utils.futures.SettableFuture
+import com.google.common.util.concurrent.ListenableFuture
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+
+/**
+ * A {@link ListenableWorker} implementation that provides interop with Kotlin Coroutines.  By
+ * default, CoroutineWorker runs on {@link Dispatchers#Default}, which can be modified by overriding
+ * {@link #coroutineContext}.  Override the {@link #doWork()} function to do your suspending
+ * work.
+ */
+abstract class CoroutineWorker(
+    appContext: Context,
+    params: WorkerParameters
+) : ListenableWorker(appContext, params) {
+
+    internal val job = Job()
+    internal val future: SettableFuture<Payload> = SettableFuture.create()
+
+    init {
+        future.addListener(
+            Runnable {
+                if (future.isCancelled) {
+                    job.cancel()
+                }
+            },
+            taskExecutor.backgroundExecutor)
+    }
+
+    /**
+     * The coroutine context on which {@link #doWork()} will run.  By default, this is
+     * {@link Dispatchers#Default}.
+     */
+    open val coroutineContext = Dispatchers.Default
+
+    final override fun startWork(): ListenableFuture<Payload> {
+
+        val coroutineScope = CoroutineScope(coroutineContext + job)
+        coroutineScope.launch {
+            try {
+                val payload = doWork()
+                future.set(payload)
+            } catch (t: Throwable) {
+                future.setException(t)
+            }
+        }
+
+        return future
+    }
+
+    /**
+     * A suspending method to do your work.  This function runs on the coroutine context specified
+     * by {@link #coroutineContext}.
+     *
+     * @return The {@link ListenableWorker.Payload} of the result of the background work
+     */
+    abstract suspend fun doWork(): ListenableWorker.Payload
+
+    final override fun onStopped() {
+        super.onStopped()
+        future.cancel(false)
+    }
+}
\ No newline at end of file
diff --git a/work/workmanager-sync/build.gradle b/work/workmanager-sync/build.gradle
deleted file mode 100644
index 8e61f74..0000000
--- a/work/workmanager-sync/build.gradle
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 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.
- */
-
-
-import androidx.build.LibraryGroups
-import androidx.build.LibraryVersions
-import androidx.build.SupportLibraryExtension
-
-import static androidx.build.dependencies.DependenciesKt.*
-
-plugins {
-    id("SupportAndroidLibraryPlugin")
-}
-
-dependencies {
-    api project(':work:work-runtime')
-
-    androidTestImplementation(project(":work:work-runtime"))
-    androidTestImplementation(TEST_RUNNER)
-    androidTestImplementation(ESPRESSO_CORE)
-    androidTestImplementation(MOCKITO_CORE, libs.exclude_bytebuddy) // DexMaker has its own MockMaker
-    androidTestImplementation(DEXMAKER_MOCKITO, libs.exclude_bytebuddy) // DexMaker has its own MockMaker
-}
-
-supportLibrary {
-    name = "Android WorkManager Synchronous APIs"
-    publish = true
-    mavenVersion = LibraryVersions.WORKMANAGER
-    mavenGroup = LibraryGroups.WORKMANAGER
-    inceptionYear = "2018"
-    description = "Android WorkManager Synchronous APIs"
-    url = SupportLibraryExtension.ARCHITECTURE_URL
-}
diff --git a/work/workmanager-sync/src/main/java/androidx/work/WorkManagerSync.java b/work/workmanager-sync/src/main/java/androidx/work/WorkManagerSync.java
deleted file mode 100644
index 96362c3..0000000
--- a/work/workmanager-sync/src/main/java/androidx/work/WorkManagerSync.java
+++ /dev/null
@@ -1,280 +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.work;
-
-import android.content.Context;
-import android.support.annotation.NonNull;
-import android.support.annotation.WorkerThread;
-
-import androidx.work.impl.WorkManagerImpl;
-
-import java.util.Arrays;
-import java.util.List;
-import java.util.UUID;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Blocking methods for {@link androidx.work.WorkManager} operations. These methods are expected
- * to be called from a background thread.
- */
-public final class WorkManagerSync {
-
-    private final WorkManagerImpl mWorkManagerImpl;
-
-    private static WorkManagerSync sInstance = null;
-    private static final Object sLock = new Object();
-
-    private WorkManagerSync(@NonNull WorkManagerImpl workManagerImpl) {
-        this.mWorkManagerImpl = workManagerImpl;
-    }
-
-    /**
-     * Retrieves the {@code default} singleton instance of {@link WorkManagerSync}.
-     *
-     * @return The singleton instance of {@link WorkManagerSync}; this may be {@code null} in
-     *         unusual circumstances where you have disabled automatic initialization and have
-     *         failed to manually call {@link WorkManager#initialize(Context, Configuration)}.
-     * @throws IllegalStateException Thrown when WorkManager is not initialized properly. This is
-     *         most likely because you disabled the automatic initialization but forgot to manually
-     *         call {@link WorkManager#initialize(Context, Configuration)}.
-     */
-    public static @NonNull WorkManagerSync getInstance() {
-        synchronized (sLock) {
-            if (sInstance == null) {
-                WorkManagerImpl workManager = WorkManagerImpl.getInstance();
-                if (workManager == null) {
-                    throw new IllegalStateException(
-                            "WorkManager is not initialized properly. The most "
-                                    + "likely cause is that you disabled WorkManagerInitializer "
-                                    + "in your manifest but forgot to call WorkManager#initialize "
-                                    + "in your Application#onCreate or a ContentProvider.");
-                }
-                sInstance = new WorkManagerSync(workManager);
-            }
-            return sInstance;
-        }
-    }
-
-    /**
-     * Enqueues one or more items for background processing.
-     *
-     * @param workRequests One or more {@link WorkRequest}s to enqueue
-     * @throws InterruptedException Thrown when the thread enqueueing the {@link WorkRequest}s is
-     *                              interrupted.
-     * @throws ExecutionException   Thrown when there is an error executing the request to enqueue
-     *                              {@link WorkRequest}s.
-     */
-    @WorkerThread
-    public void enqueue(@NonNull WorkRequest... workRequests)
-            throws InterruptedException, ExecutionException {
-        mWorkManagerImpl.enqueueInternal(Arrays.asList(workRequests)).get();
-    }
-
-    /**
-     * Enqueues one or more items for background processing.
-     *
-     * @param requests One or more {@link WorkRequest}s to enqueue
-     * @throws InterruptedException Thrown when the thread enqueueing the {@link WorkRequest}s is
-     *                              interrupted.
-     * @throws ExecutionException   Thrown when there is an error executing the request to enqueue
-     *                              {@link WorkRequest}s.
-     */
-    @WorkerThread
-    public void enqueue(@NonNull List<? extends WorkRequest> requests)
-            throws InterruptedException, ExecutionException {
-        mWorkManagerImpl.enqueueInternal(requests).get();
-    }
-
-    /**
-     * This method allows you to enqueue {@code work} requests to a uniquely-named
-     * {@link WorkContinuation}, where only one continuation of a particular name can be active at
-     * a time. For example, you may only want one sync operation to be active. If there is one
-     * pending, you can choose to let it run or replace it with your new work.
-     *
-     * <p>
-     * The {@code uniqueWorkName} uniquely identifies this {@link WorkContinuation}.
-     * </p>
-     *
-     * @param uniqueWorkName     A unique name which for this operation
-     * @param existingWorkPolicy An {@link ExistingWorkPolicy}; see below for more information
-     * @param work               {@link OneTimeWorkRequest}s to enqueue. {@code REPLACE} ensures
-     *                           that if there is pending work labelled with {@code uniqueWorkName},
-     *                           it will be cancelled and the new work will run. {@code KEEP} will
-     *                           run the new OneTimeWorkRequests only if there is no pending work
-     *                           labelled with {@code uniqueWorkName}. {@code APPEND} will append
-     *                           the OneTimeWorkRequests as leaf nodes labelled with
-     *                           {@code uniqueWorkName}.
-     * @throws InterruptedException Thrown when the thread enqueueing the
-     *                              {@link OneTimeWorkRequest}s is interrupted.
-     * @throws ExecutionException   Thrown when there is an error executing the request to enqueue
-     *                              {@link OneTimeWorkRequest}s.
-     */
-    @WorkerThread
-    public void enqueueUniqueWork(
-            @NonNull String uniqueWorkName,
-            @NonNull ExistingWorkPolicy existingWorkPolicy,
-            @NonNull OneTimeWorkRequest...work)
-            throws InterruptedException, ExecutionException {
-        mWorkManagerImpl.enqueueUniqueWorkInternal(
-                uniqueWorkName, existingWorkPolicy, Arrays.asList(work)).get();
-    }
-
-    /**
-     * This method allows you to enqueue {@code work} requests to a uniquely-named
-     * {@link WorkContinuation}, where only one continuation of a particular name can be active at
-     * a time. For example, you may only want one sync operation to be active. If there is one
-     * pending, you can choose to let it run or replace it with your new work.
-     *
-     * <p>
-     * The {@code uniqueWorkName} uniquely identifies this {@link WorkContinuation}.
-     * </p>
-     *
-     * @param uniqueWorkName     A unique name which for this operation
-     * @param existingWorkPolicy An {@link ExistingWorkPolicy}; see below for more information
-     * @param work               {@link OneTimeWorkRequest}s to enqueue. {@code REPLACE} ensures
-     *                           that if there is pending work labelled with {@code uniqueWorkName},
-     *                           it will be cancelled and the new work will run. {@code KEEP} will
-     *                           run the new OneTimeWorkRequests only if there is no pending work
-     *                           labelled with {@code uniqueWorkName}. {@code APPEND} will append
-     *                           the OneTimeWorkRequests as leaf nodes labelled with
-     *                           {@code uniqueWorkName}.
-     * @throws InterruptedException Thrown when the thread enqueueing the
-     *                              {@link OneTimeWorkRequest}s is interrupted.
-     * @throws ExecutionException   Thrown when there is an error executing the request to enqueue
-     *                              {@link OneTimeWorkRequest}s.
-     */
-    @WorkerThread
-    public void enqueueUniqueWork(
-            @NonNull String uniqueWorkName,
-            @NonNull ExistingWorkPolicy existingWorkPolicy,
-            @NonNull List<OneTimeWorkRequest> work)
-            throws InterruptedException, ExecutionException {
-        mWorkManagerImpl.enqueueUniqueWorkInternal(uniqueWorkName, existingWorkPolicy, work).get();
-    }
-
-    /**
-     * This method allows you to enqueue a uniquely-named {@link PeriodicWorkRequest}, where only
-     * one PeriodicWorkRequest of a particular name can be active at a time.  For example, you may
-     * only want one sync operation to be active.  If there is one pending, you can choose to let it
-     * run or replace it with your new work.
-     *
-     * <p>
-     * The {@code uniqueWorkName} uniquely identifies this PeriodicWorkRequest.
-     * </p>
-     *
-     * @param uniqueWorkName             A unique name which for this operation
-     * @param existingPeriodicWorkPolicy An {@link ExistingPeriodicWorkPolicy}
-     * @param periodicWork               A {@link PeriodicWorkRequest} to enqueue. {@code REPLACE}
-     *                                   ensures that if there is pending work labelled with
-     *                                   {@code uniqueWorkName}, it will be cancelled and the new
-     *                                   work will run. {@code KEEP} will run the new
-     *                                   PeriodicWorkRequest only if there is no pending work
-     *                                   labelled with {@code uniqueWorkName}.
-     * @throws InterruptedException Thrown when the thread enqueueing the
-     *                              {@link PeriodicWorkRequest} is interrupted.
-     * @throws ExecutionException   Thrown when there is an error executing the request to enqueue
-     *                              {@link PeriodicWorkRequest}.
-     */
-    @WorkerThread
-    public void enqueueUniquePeriodicWork(
-            @NonNull String uniqueWorkName,
-            @NonNull ExistingPeriodicWorkPolicy existingPeriodicWorkPolicy,
-            @NonNull PeriodicWorkRequest periodicWork)
-            throws InterruptedException, ExecutionException {
-        mWorkManagerImpl.enqueueUniquePeriodicWorkInternal(
-                uniqueWorkName, existingPeriodicWorkPolicy, periodicWork).get();
-    }
-
-    /**
-     * Cancels work with the given id if it isn't finished.  Note that cancellation is a best-effort
-     * policy and work that is already executing may continue to run.
-     *
-     * @param id The id of the work completed
-     * @throws InterruptedException Thrown when the thread cancelling work by id is interrupted.
-     * @throws ExecutionException   Thrown when there is an error executing the request to cancel
-     *                              work by id.
-     */
-    @WorkerThread
-    public void cancelWorkById(@NonNull UUID id)
-            throws InterruptedException, ExecutionException {
-        mWorkManagerImpl.cancelWorkByIdInternal(id).get();
-    }
-
-    /**
-     * Cancels all unfinished work with the given tag.  Note that cancellation is a best-effort
-     * policy and work that is already executing may continue to run.
-     *
-     * @param tag The tag used to identify the work
-     * @throws InterruptedException Thrown when the thread cancelling work by tag is interrupted.
-     * @throws ExecutionException   Thrown when there is an error executing the request to cancel
-     *                              work by tag.
-     */
-    @WorkerThread
-    public void cancelAllWorkByTag(@NonNull String tag)
-            throws InterruptedException, ExecutionException {
-        mWorkManagerImpl.cancelAllWorkByTagInternal(tag).get();
-    }
-
-    /**
-     * Cancels all unfinished work in the work chain with the given name.  Note that cancellation is
-     * a best-effort policy and work that is already executing may continue to run.
-     *
-     * @param uniqueWorkName The unique name used to identify the chain of work
-     * @throws InterruptedException Thrown when the thread cancelling unique work is interrupted.
-     * @throws ExecutionException   Thrown when there is an error executing the request to cancel
-     *                              unique work.
-     */
-    @WorkerThread
-    public void cancelUniqueWork(@NonNull String uniqueWorkName)
-            throws InterruptedException, ExecutionException {
-        mWorkManagerImpl.cancelUniqueWorkInternal(uniqueWorkName).get();
-    }
-
-    /**
-     * Cancels all unfinished work.  <b>Use this method with extreme caution!</b>  By invoking it,
-     * you will potentially affect other modules or libraries in your codebase.  It is strongly
-     * recommended that you use one of the other cancellation methods at your disposal.
-     * @throws InterruptedException Thrown when the thread cancelling all work is interrupted.
-     * @throws ExecutionException   Thrown when there is an error executing the request to cancel
-     *                              all work.
-     */
-    @WorkerThread
-    public void cancelAllWork() throws InterruptedException, ExecutionException {
-        mWorkManagerImpl.cancelAllWorkInternal().get();
-    }
-
-    /**
-     * Prunes all eligible finished work from the internal database.  Eligible work must be finished
-     * ({@link State#SUCCEEDED}, {@link State#FAILED}, or {@link State#CANCELLED}), with zero
-     * unfinished dependents.
-     * <p>
-     * <b>Use this method with caution</b>; by invoking it, you (and any modules and libraries in
-     * your codebase) will no longer be able to observe the {@link WorkStatus} of the pruned work.
-     * You do not normally need to call this method - WorkManager takes care to auto-prune its work
-     * after a sane period of time.  This method also ignores the
-     * {@link OneTimeWorkRequest.Builder#keepResultsForAtLeast(long, TimeUnit)} policy.
-     *
-     * @throws InterruptedException Thrown when the thread pruning all work is interrupted.
-     * @throws ExecutionException   Thrown when there is an error executing the request to prune all
-     *                              work.
-     */
-    @WorkerThread
-    public void pruneWork() throws InterruptedException, ExecutionException {
-        mWorkManagerImpl.pruneWorkInternal().get();
-    }
-}
diff --git a/work/workmanager-testing/src/androidTest/java/androidx/work/testing/TestSchedulerTest.java b/work/workmanager-testing/src/androidTest/java/androidx/work/testing/TestSchedulerTest.java
index c2ff348..5527f7a 100644
--- a/work/workmanager-testing/src/androidTest/java/androidx/work/testing/TestSchedulerTest.java
+++ b/work/workmanager-testing/src/androidTest/java/androidx/work/testing/TestSchedulerTest.java
@@ -29,9 +29,9 @@
 import androidx.work.OneTimeWorkRequest;
 import androidx.work.PeriodicWorkRequest;
 import androidx.work.WorkContinuation;
+import androidx.work.WorkInfo;
 import androidx.work.WorkManager;
 import androidx.work.WorkRequest;
-import androidx.work.WorkStatus;
 import androidx.work.impl.WorkManagerImpl;
 import androidx.work.testing.workers.CountingTestWorker;
 import androidx.work.testing.workers.TestWorker;
@@ -65,8 +65,8 @@
         WorkRequest request = createWorkRequest();
         // TestWorkManagerImpl is a subtype of WorkManagerImpl.
         WorkManagerImpl workManagerImpl = WorkManagerImpl.getInstance();
-        workManagerImpl.enqueueInternal(Collections.singletonList(request)).get();
-        WorkStatus status = workManagerImpl.getStatusById(request.getId()).get();
+        workManagerImpl.enqueue(Collections.singletonList(request)).getResult().get();
+        WorkInfo status = workManagerImpl.getWorkInfoById(request.getId()).get();
         assertThat(status.getState().isFinished(), is(true));
     }
 
@@ -79,10 +79,10 @@
         WorkManager workManager = WorkManager.getInstance();
         WorkContinuation continuation = workManager.beginWith(request)
                 .then(dependentRequest);
-        continuation.enqueue().get();
-        WorkStatus requestStatus = workManager.getStatusById(request.getId()).get();
-        WorkStatus dependentStatus = workManager
-                .getStatusById(dependentRequest.getId()).get();
+        continuation.enqueue().getResult().get();
+        WorkInfo requestStatus = workManager.getWorkInfoById(request.getId()).get();
+        WorkInfo dependentStatus = workManager
+                .getWorkInfoById(dependentRequest.getId()).get();
 
         assertThat(requestStatus.getState().isFinished(), is(true));
         assertThat(dependentStatus.getState().isFinished(), is(true));
@@ -95,7 +95,7 @@
         OneTimeWorkRequest request = createWorkRequestWithNetworkConstraints();
         WorkManager workManager = WorkManager.getInstance();
         workManager.enqueue(request);
-        WorkStatus requestStatus = workManager.getStatusById(request.getId()).get();
+        WorkInfo requestStatus = workManager.getWorkInfoById(request.getId()).get();
         assertThat(requestStatus.getState().isFinished(), is(false));
     }
 
@@ -106,10 +106,10 @@
         OneTimeWorkRequest request = createWorkRequestWithNetworkConstraints();
         WorkManager workManager = WorkManager.getInstance();
         workManager.enqueue(request);
-        WorkStatus requestStatus = workManager.getStatusById(request.getId()).get();
+        WorkInfo requestStatus = workManager.getWorkInfoById(request.getId()).get();
         assertThat(requestStatus.getState().isFinished(), is(false));
         mTestDriver.setAllConstraintsMet(request.getId());
-        requestStatus = workManager.getStatusById(request.getId()).get();
+        requestStatus = workManager.getWorkInfoById(request.getId()).get();
         assertThat(requestStatus.getState().isFinished(), is(true));
     }
 
@@ -120,7 +120,7 @@
         OneTimeWorkRequest request = createWorkRequestWithInitialDelay();
         WorkManager workManager = WorkManager.getInstance();
         workManager.enqueue(request);
-        WorkStatus requestStatus = workManager.getStatusById(request.getId()).get();
+        WorkInfo requestStatus = workManager.getWorkInfoById(request.getId()).get();
         assertThat(requestStatus.getState().isFinished(), is(false));
     }
 
@@ -132,7 +132,7 @@
         WorkManager workManager = WorkManager.getInstance();
         workManager.enqueue(request);
         mTestDriver.setInitialDelayMet(request.getId());
-        WorkStatus requestStatus = workManager.getStatusById(request.getId()).get();
+        WorkInfo requestStatus = workManager.getWorkInfoById(request.getId()).get();
         assertThat(requestStatus.getState().isFinished(), is(true));
     }
 
@@ -155,7 +155,7 @@
         for (int i = 0; i < 5; ++i) {
             mTestDriver.setPeriodDelayMet(request.getId());
             assertThat(CountingTestWorker.COUNT.get(), is(i + 2));
-            WorkStatus requestStatus = workManager.getStatusById(request.getId()).get();
+            WorkInfo requestStatus = workManager.getWorkInfoById(request.getId()).get();
             assertThat(requestStatus.getState().isFinished(), is(false));
         }
     }
diff --git a/work/workmanager/api/1.0.0-alpha11.txt b/work/workmanager/api/1.0.0-alpha11.txt
index ced045a..c0e4fac 100644
--- a/work/workmanager/api/1.0.0-alpha11.txt
+++ b/work/workmanager/api/1.0.0-alpha11.txt
@@ -78,7 +78,6 @@
     method public long[]? getLongArray(String);
     method public String? getString(String);
     method public String[]? getStringArray(String);
-    method @VisibleForTesting public int size();
     field public static final androidx.work.Data! EMPTY;
     field public static final int MAX_DATA_BYTES = 10240; // 0x2800
   }
@@ -89,17 +88,17 @@
     method public androidx.work.Data.Builder putAll(androidx.work.Data);
     method public androidx.work.Data.Builder putAll(java.util.Map<java.lang.String,java.lang.Object>);
     method public androidx.work.Data.Builder putBoolean(String, boolean);
-    method public androidx.work.Data.Builder putBooleanArray(String, boolean[]!);
+    method public androidx.work.Data.Builder putBooleanArray(String, boolean[]);
     method public androidx.work.Data.Builder putDouble(String, double);
-    method public androidx.work.Data.Builder putDoubleArray(String, double[]!);
+    method public androidx.work.Data.Builder putDoubleArray(String, double[]);
     method public androidx.work.Data.Builder putFloat(String, float);
-    method public androidx.work.Data.Builder putFloatArray(String!, float[]!);
+    method public androidx.work.Data.Builder putFloatArray(String, float[]);
     method public androidx.work.Data.Builder putInt(String, int);
-    method public androidx.work.Data.Builder putIntArray(String, int[]!);
+    method public androidx.work.Data.Builder putIntArray(String, int[]);
     method public androidx.work.Data.Builder putLong(String, long);
-    method public androidx.work.Data.Builder putLongArray(String, long[]!);
-    method public androidx.work.Data.Builder putString(String, String!);
-    method public androidx.work.Data.Builder putStringArray(String, String[]!);
+    method public androidx.work.Data.Builder putLongArray(String, long[]);
+    method public androidx.work.Data.Builder putString(String, String?);
+    method public androidx.work.Data.Builder putStringArray(String, String[]);
   }
 
   public enum ExistingPeriodicWorkPolicy {
@@ -128,9 +127,8 @@
     method public final java.util.Set<java.lang.String> getTags();
     method @RequiresApi(24) public final java.util.List<java.lang.String>? getTriggeredContentAuthorities();
     method @RequiresApi(24) public final java.util.List<android.net.Uri>? getTriggeredContentUris();
-    method public final boolean isCancelled();
     method public final boolean isStopped();
-    method public void onStopped(boolean);
+    method public void onStopped();
     method @MainThread public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Payload> startWork();
   }
 
@@ -167,6 +165,25 @@
     method public androidx.work.OneTimeWorkRequest.Builder setInputMerger(Class<? extends androidx.work.InputMerger>);
   }
 
+  public interface Operation {
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.Operation.State.SUCCESS> getResult();
+    method public android.arch.lifecycle.LiveData<androidx.work.Operation.State> getState();
+  }
+
+  public abstract static class Operation.State {
+  }
+
+  public static final class Operation.State.FAILURE extends androidx.work.Operation.State {
+    ctor public Operation.State.FAILURE(Throwable);
+    method public Throwable getException();
+  }
+
+  public static final class Operation.State.IN_PROGRESS extends androidx.work.Operation.State {
+  }
+
+  public static final class Operation.State.SUCCESS extends androidx.work.Operation.State {
+  }
+
   public final class OverwritingInputMerger extends androidx.work.InputMerger {
     ctor public OverwritingInputMerger();
     method public androidx.work.Data merge(java.util.List<androidx.work.Data>);
@@ -184,54 +201,61 @@
     ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, java.time.Duration, java.time.Duration);
   }
 
-  public enum State {
-    method public boolean isFinished();
-    enum_constant public static final androidx.work.State BLOCKED;
-    enum_constant public static final androidx.work.State CANCELLED;
-    enum_constant public static final androidx.work.State ENQUEUED;
-    enum_constant public static final androidx.work.State FAILED;
-    enum_constant public static final androidx.work.State RUNNING;
-    enum_constant public static final androidx.work.State SUCCEEDED;
-  }
-
   public abstract class WorkContinuation {
     ctor public WorkContinuation();
     method public static androidx.work.WorkContinuation combine(androidx.work.WorkContinuation...);
     method public static androidx.work.WorkContinuation combine(java.util.List<androidx.work.WorkContinuation>);
     method public static androidx.work.WorkContinuation combine(androidx.work.OneTimeWorkRequest, androidx.work.WorkContinuation...);
     method public static androidx.work.WorkContinuation combine(androidx.work.OneTimeWorkRequest, java.util.List<androidx.work.WorkContinuation>);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void> enqueue();
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkStatus>> getStatuses();
-    method public abstract android.arch.lifecycle.LiveData<java.util.List<androidx.work.WorkStatus>> getStatusesLiveData();
+    method public abstract androidx.work.Operation enqueue();
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo>> getWorkInfos();
+    method public abstract android.arch.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo>> getWorkInfosLiveData();
     method public final androidx.work.WorkContinuation then(androidx.work.OneTimeWorkRequest);
     method public abstract androidx.work.WorkContinuation then(java.util.List<androidx.work.OneTimeWorkRequest>);
   }
 
+  public final class WorkInfo {
+    method public java.util.UUID getId();
+    method public androidx.work.Data getOutputData();
+    method public androidx.work.WorkInfo.State getState();
+    method public java.util.Set<java.lang.String> getTags();
+  }
+
+  public static enum WorkInfo.State {
+    method public boolean isFinished();
+    enum_constant public static final androidx.work.WorkInfo.State BLOCKED;
+    enum_constant public static final androidx.work.WorkInfo.State CANCELLED;
+    enum_constant public static final androidx.work.WorkInfo.State ENQUEUED;
+    enum_constant public static final androidx.work.WorkInfo.State FAILED;
+    enum_constant public static final androidx.work.WorkInfo.State RUNNING;
+    enum_constant public static final androidx.work.WorkInfo.State SUCCEEDED;
+  }
+
   public abstract class WorkManager {
     method public final androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest...);
     method public abstract androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest>);
     method public final androidx.work.WorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
     method public abstract androidx.work.WorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest>);
-    method public void cancelAllWork();
-    method public void cancelAllWorkByTag(String);
-    method public void cancelUniqueWork(String);
-    method public void cancelWorkById(java.util.UUID);
-    method public final void enqueue(androidx.work.WorkRequest);
-    method public void enqueue(java.util.List<? extends androidx.work.WorkRequest>);
-    method public void enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
-    method public void enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest...);
-    method public void enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest>);
+    method public abstract androidx.work.Operation cancelAllWork();
+    method public abstract androidx.work.Operation cancelAllWorkByTag(String);
+    method public abstract androidx.work.Operation cancelUniqueWork(String);
+    method public abstract androidx.work.Operation cancelWorkById(java.util.UUID);
+    method public final androidx.work.Operation enqueue(androidx.work.WorkRequest);
+    method public abstract androidx.work.Operation enqueue(java.util.List<? extends androidx.work.WorkRequest>);
+    method public abstract androidx.work.Operation enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
+    method public androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest...);
+    method public abstract androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest>);
     method public static androidx.work.WorkManager getInstance();
     method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Long> getLastCancelAllTimeMillis();
     method public abstract android.arch.lifecycle.LiveData<java.lang.Long> getLastCancelAllTimeMillisLiveData();
-    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkStatus> getStatusById(java.util.UUID);
-    method public abstract android.arch.lifecycle.LiveData<androidx.work.WorkStatus> getStatusByIdLiveData(java.util.UUID);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkStatus>> getStatusesByTag(String);
-    method public abstract android.arch.lifecycle.LiveData<java.util.List<androidx.work.WorkStatus>> getStatusesByTagLiveData(String);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkStatus>> getStatusesForUniqueWork(String);
-    method public abstract android.arch.lifecycle.LiveData<java.util.List<androidx.work.WorkStatus>> getStatusesForUniqueWorkLiveData(String);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkInfo> getWorkInfoById(java.util.UUID);
+    method public abstract android.arch.lifecycle.LiveData<androidx.work.WorkInfo> getWorkInfoByIdLiveData(java.util.UUID);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo>> getWorkInfosByTag(String);
+    method public abstract android.arch.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo>> getWorkInfosByTagLiveData(String);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo>> getWorkInfosForUniqueWork(String);
+    method public abstract android.arch.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo>> getWorkInfosForUniqueWorkLiveData(String);
     method public static void initialize(android.content.Context, androidx.work.Configuration);
-    method public void pruneWork();
+    method public abstract androidx.work.Operation pruneWork();
   }
 
   public abstract class WorkRequest {
@@ -251,13 +275,6 @@
     method public final B setInputData(androidx.work.Data);
   }
 
-  public final class WorkStatus {
-    method public java.util.UUID getId();
-    method public androidx.work.Data getOutputData();
-    method public androidx.work.State getState();
-    method public java.util.Set<java.lang.String> getTags();
-  }
-
   public abstract class Worker extends androidx.work.ListenableWorker {
     ctor @Keep public Worker(android.content.Context, androidx.work.WorkerParameters);
     method @WorkerThread public abstract androidx.work.ListenableWorker.Result doWork();
diff --git a/work/workmanager/api/current.txt b/work/workmanager/api/current.txt
index ced045a..c0e4fac 100644
--- a/work/workmanager/api/current.txt
+++ b/work/workmanager/api/current.txt
@@ -78,7 +78,6 @@
     method public long[]? getLongArray(String);
     method public String? getString(String);
     method public String[]? getStringArray(String);
-    method @VisibleForTesting public int size();
     field public static final androidx.work.Data! EMPTY;
     field public static final int MAX_DATA_BYTES = 10240; // 0x2800
   }
@@ -89,17 +88,17 @@
     method public androidx.work.Data.Builder putAll(androidx.work.Data);
     method public androidx.work.Data.Builder putAll(java.util.Map<java.lang.String,java.lang.Object>);
     method public androidx.work.Data.Builder putBoolean(String, boolean);
-    method public androidx.work.Data.Builder putBooleanArray(String, boolean[]!);
+    method public androidx.work.Data.Builder putBooleanArray(String, boolean[]);
     method public androidx.work.Data.Builder putDouble(String, double);
-    method public androidx.work.Data.Builder putDoubleArray(String, double[]!);
+    method public androidx.work.Data.Builder putDoubleArray(String, double[]);
     method public androidx.work.Data.Builder putFloat(String, float);
-    method public androidx.work.Data.Builder putFloatArray(String!, float[]!);
+    method public androidx.work.Data.Builder putFloatArray(String, float[]);
     method public androidx.work.Data.Builder putInt(String, int);
-    method public androidx.work.Data.Builder putIntArray(String, int[]!);
+    method public androidx.work.Data.Builder putIntArray(String, int[]);
     method public androidx.work.Data.Builder putLong(String, long);
-    method public androidx.work.Data.Builder putLongArray(String, long[]!);
-    method public androidx.work.Data.Builder putString(String, String!);
-    method public androidx.work.Data.Builder putStringArray(String, String[]!);
+    method public androidx.work.Data.Builder putLongArray(String, long[]);
+    method public androidx.work.Data.Builder putString(String, String?);
+    method public androidx.work.Data.Builder putStringArray(String, String[]);
   }
 
   public enum ExistingPeriodicWorkPolicy {
@@ -128,9 +127,8 @@
     method public final java.util.Set<java.lang.String> getTags();
     method @RequiresApi(24) public final java.util.List<java.lang.String>? getTriggeredContentAuthorities();
     method @RequiresApi(24) public final java.util.List<android.net.Uri>? getTriggeredContentUris();
-    method public final boolean isCancelled();
     method public final boolean isStopped();
-    method public void onStopped(boolean);
+    method public void onStopped();
     method @MainThread public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Payload> startWork();
   }
 
@@ -167,6 +165,25 @@
     method public androidx.work.OneTimeWorkRequest.Builder setInputMerger(Class<? extends androidx.work.InputMerger>);
   }
 
+  public interface Operation {
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.Operation.State.SUCCESS> getResult();
+    method public android.arch.lifecycle.LiveData<androidx.work.Operation.State> getState();
+  }
+
+  public abstract static class Operation.State {
+  }
+
+  public static final class Operation.State.FAILURE extends androidx.work.Operation.State {
+    ctor public Operation.State.FAILURE(Throwable);
+    method public Throwable getException();
+  }
+
+  public static final class Operation.State.IN_PROGRESS extends androidx.work.Operation.State {
+  }
+
+  public static final class Operation.State.SUCCESS extends androidx.work.Operation.State {
+  }
+
   public final class OverwritingInputMerger extends androidx.work.InputMerger {
     ctor public OverwritingInputMerger();
     method public androidx.work.Data merge(java.util.List<androidx.work.Data>);
@@ -184,54 +201,61 @@
     ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, java.time.Duration, java.time.Duration);
   }
 
-  public enum State {
-    method public boolean isFinished();
-    enum_constant public static final androidx.work.State BLOCKED;
-    enum_constant public static final androidx.work.State CANCELLED;
-    enum_constant public static final androidx.work.State ENQUEUED;
-    enum_constant public static final androidx.work.State FAILED;
-    enum_constant public static final androidx.work.State RUNNING;
-    enum_constant public static final androidx.work.State SUCCEEDED;
-  }
-
   public abstract class WorkContinuation {
     ctor public WorkContinuation();
     method public static androidx.work.WorkContinuation combine(androidx.work.WorkContinuation...);
     method public static androidx.work.WorkContinuation combine(java.util.List<androidx.work.WorkContinuation>);
     method public static androidx.work.WorkContinuation combine(androidx.work.OneTimeWorkRequest, androidx.work.WorkContinuation...);
     method public static androidx.work.WorkContinuation combine(androidx.work.OneTimeWorkRequest, java.util.List<androidx.work.WorkContinuation>);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void> enqueue();
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkStatus>> getStatuses();
-    method public abstract android.arch.lifecycle.LiveData<java.util.List<androidx.work.WorkStatus>> getStatusesLiveData();
+    method public abstract androidx.work.Operation enqueue();
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo>> getWorkInfos();
+    method public abstract android.arch.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo>> getWorkInfosLiveData();
     method public final androidx.work.WorkContinuation then(androidx.work.OneTimeWorkRequest);
     method public abstract androidx.work.WorkContinuation then(java.util.List<androidx.work.OneTimeWorkRequest>);
   }
 
+  public final class WorkInfo {
+    method public java.util.UUID getId();
+    method public androidx.work.Data getOutputData();
+    method public androidx.work.WorkInfo.State getState();
+    method public java.util.Set<java.lang.String> getTags();
+  }
+
+  public static enum WorkInfo.State {
+    method public boolean isFinished();
+    enum_constant public static final androidx.work.WorkInfo.State BLOCKED;
+    enum_constant public static final androidx.work.WorkInfo.State CANCELLED;
+    enum_constant public static final androidx.work.WorkInfo.State ENQUEUED;
+    enum_constant public static final androidx.work.WorkInfo.State FAILED;
+    enum_constant public static final androidx.work.WorkInfo.State RUNNING;
+    enum_constant public static final androidx.work.WorkInfo.State SUCCEEDED;
+  }
+
   public abstract class WorkManager {
     method public final androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest...);
     method public abstract androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest>);
     method public final androidx.work.WorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
     method public abstract androidx.work.WorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest>);
-    method public void cancelAllWork();
-    method public void cancelAllWorkByTag(String);
-    method public void cancelUniqueWork(String);
-    method public void cancelWorkById(java.util.UUID);
-    method public final void enqueue(androidx.work.WorkRequest);
-    method public void enqueue(java.util.List<? extends androidx.work.WorkRequest>);
-    method public void enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
-    method public void enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest...);
-    method public void enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest>);
+    method public abstract androidx.work.Operation cancelAllWork();
+    method public abstract androidx.work.Operation cancelAllWorkByTag(String);
+    method public abstract androidx.work.Operation cancelUniqueWork(String);
+    method public abstract androidx.work.Operation cancelWorkById(java.util.UUID);
+    method public final androidx.work.Operation enqueue(androidx.work.WorkRequest);
+    method public abstract androidx.work.Operation enqueue(java.util.List<? extends androidx.work.WorkRequest>);
+    method public abstract androidx.work.Operation enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
+    method public androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest...);
+    method public abstract androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest>);
     method public static androidx.work.WorkManager getInstance();
     method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Long> getLastCancelAllTimeMillis();
     method public abstract android.arch.lifecycle.LiveData<java.lang.Long> getLastCancelAllTimeMillisLiveData();
-    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkStatus> getStatusById(java.util.UUID);
-    method public abstract android.arch.lifecycle.LiveData<androidx.work.WorkStatus> getStatusByIdLiveData(java.util.UUID);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkStatus>> getStatusesByTag(String);
-    method public abstract android.arch.lifecycle.LiveData<java.util.List<androidx.work.WorkStatus>> getStatusesByTagLiveData(String);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkStatus>> getStatusesForUniqueWork(String);
-    method public abstract android.arch.lifecycle.LiveData<java.util.List<androidx.work.WorkStatus>> getStatusesForUniqueWorkLiveData(String);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkInfo> getWorkInfoById(java.util.UUID);
+    method public abstract android.arch.lifecycle.LiveData<androidx.work.WorkInfo> getWorkInfoByIdLiveData(java.util.UUID);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo>> getWorkInfosByTag(String);
+    method public abstract android.arch.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo>> getWorkInfosByTagLiveData(String);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo>> getWorkInfosForUniqueWork(String);
+    method public abstract android.arch.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo>> getWorkInfosForUniqueWorkLiveData(String);
     method public static void initialize(android.content.Context, androidx.work.Configuration);
-    method public void pruneWork();
+    method public abstract androidx.work.Operation pruneWork();
   }
 
   public abstract class WorkRequest {
@@ -251,13 +275,6 @@
     method public final B setInputData(androidx.work.Data);
   }
 
-  public final class WorkStatus {
-    method public java.util.UUID getId();
-    method public androidx.work.Data getOutputData();
-    method public androidx.work.State getState();
-    method public java.util.Set<java.lang.String> getTags();
-  }
-
   public abstract class Worker extends androidx.work.ListenableWorker {
     ctor @Keep public Worker(android.content.Context, androidx.work.WorkerParameters);
     method @WorkerThread public abstract androidx.work.ListenableWorker.Result doWork();
diff --git a/work/workmanager/src/androidTest/java/androidx/work/WorkSpecDaoTest.java b/work/workmanager/src/androidTest/java/androidx/work/WorkSpecDaoTest.java
index c2437bd..c874541 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/WorkSpecDaoTest.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/WorkSpecDaoTest.java
@@ -16,9 +16,9 @@
 
 package androidx.work;
 
-import static androidx.work.State.BLOCKED;
-import static androidx.work.State.FAILED;
-import static androidx.work.State.SUCCEEDED;
+import static androidx.work.WorkInfo.State.BLOCKED;
+import static androidx.work.WorkInfo.State.FAILED;
+import static androidx.work.WorkInfo.State.SUCCEEDED;
 import static androidx.work.impl.Scheduler.MAX_SCHEDULER_LIMIT;
 
 import static org.hamcrest.CoreMatchers.equalTo;
diff --git a/work/workmanager/src/androidTest/java/androidx/work/impl/WorkContinuationImplTest.java b/work/workmanager/src/androidTest/java/androidx/work/impl/WorkContinuationImplTest.java
index 43fd7ab..dcc8c5b 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/impl/WorkContinuationImplTest.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/impl/WorkContinuationImplTest.java
@@ -41,11 +41,10 @@
 import androidx.work.Configuration;
 import androidx.work.Data;
 import androidx.work.OneTimeWorkRequest;
-import androidx.work.State;
 import androidx.work.TestLifecycleOwner;
 import androidx.work.WorkContinuation;
+import androidx.work.WorkInfo;
 import androidx.work.WorkManagerTest;
-import androidx.work.WorkStatus;
 import androidx.work.impl.model.WorkSpec;
 import androidx.work.impl.model.WorkSpecDao;
 import androidx.work.impl.utils.SynchronousExecutor;
@@ -116,7 +115,9 @@
     public void tearDown() throws ExecutionException, InterruptedException {
         List<String> ids = mDatabase.workSpecDao().getAllWorkSpecIds();
         for (String id : ids) {
-            mWorkManagerImpl.cancelWorkByIdInternal(UUID.fromString(id)).get();
+            mWorkManagerImpl.cancelWorkById(UUID.fromString(id))
+                    .getResult()
+                    .get();
         }
         WorkManagerImpl.setDelegate(null);
         ArchTaskExecutor.getInstance().setDelegate(null);
@@ -157,7 +158,7 @@
         WorkContinuationImpl continuation = new WorkContinuationImpl(mWorkManagerImpl,
                 createTestWorkerList());
         assertThat(continuation.isEnqueued(), is(false));
-        continuation.enqueue().get();
+        continuation.enqueue().getResult().get();
         verifyEnqueued(continuation);
         verifyScheduled(mScheduler, continuation);
     }
@@ -169,7 +170,7 @@
         WorkContinuationImpl chain = (WorkContinuationImpl) (
                 continuation.then(createTestWorker())
                         .then(Arrays.asList(createTestWorker(), createTestWorker())));
-        chain.enqueue().get();
+        chain.enqueue().getResult().get();
         verifyEnqueued(continuation);
         verifyScheduled(mScheduler, continuation);
     }
@@ -183,11 +184,11 @@
         WorkContinuationImpl chain = (WorkContinuationImpl) (
                 continuation.then(createTestWorker())
                         .then(Arrays.asList(createTestWorker(), createTestWorker())));
-        chain.enqueue().get();
+        chain.enqueue().getResult().get();
         verifyEnqueued(continuation);
         verifyScheduled(mScheduler, continuation);
         WorkContinuationImpl spy = spy(chain);
-        spy.enqueue().get();
+        spy.enqueue().getResult().get();
         // Verify no more calls to markEnqueued().
         verify(spy, times(0)).markEnqueued();
     }
@@ -239,7 +240,7 @@
                 third, fourth);
         WorkContinuationImpl dependent = (WorkContinuationImpl) WorkContinuation.combine(
                 firstDependent, secondDependent);
-        dependent.enqueue().get();
+        dependent.enqueue().getResult().get();
         verifyEnqueued(dependent);
         verifyScheduled(mScheduler, dependent);
     }
@@ -260,7 +261,7 @@
                 first, third);
         WorkContinuationImpl dependent = (WorkContinuationImpl) WorkContinuation.combine(
                 firstDependent, secondDependent);
-        dependent.enqueue().get();
+        dependent.enqueue().getResult().get();
         verifyEnqueued(dependent);
         verifyScheduled(mScheduler, dependent);
     }
@@ -275,10 +276,10 @@
         final String stringTag = "mystring";
 
         OneTimeWorkRequest firstWork = new OneTimeWorkRequest.Builder(TestWorker.class)
-                .setInitialState(State.SUCCEEDED)
+                .setInitialState(WorkInfo.State.SUCCEEDED)
                 .build();
         OneTimeWorkRequest secondWork = new OneTimeWorkRequest.Builder(TestWorker.class)
-                .setInitialState(State.SUCCEEDED)
+                .setInitialState(WorkInfo.State.SUCCEEDED)
                 .build();
 
         WorkSpecDao workSpecDao = mDatabase.workSpecDao();
@@ -299,7 +300,7 @@
         WorkContinuationImpl dependentContinuation =
                 (WorkContinuationImpl) WorkContinuation.combine(
                         firstContinuation, secondContinuation);
-        dependentContinuation.enqueue().get();
+        dependentContinuation.enqueue().getResult().get();
 
         String joinId = null;
         for (String id : dependentContinuation.getAllIds()) {
@@ -326,7 +327,7 @@
         assertThat(joinId, is(not(nullValue())));
         WorkSpec joinWorkSpec = mDatabase.workSpecDao().getWorkSpec(joinId);
         assertThat(joinWorkSpec, is(not(nullValue())));
-        assertThat(joinWorkSpec.state, is(State.SUCCEEDED));
+        assertThat(joinWorkSpec.state, is(WorkInfo.State.SUCCEEDED));
 
         Data output = joinWorkSpec.output;
         int[] intArray = output.getIntArray(intTag);
@@ -495,7 +496,7 @@
 
     @Test
     @SmallTest
-    public void testGetStatusesSync() throws ExecutionException, InterruptedException {
+    public void testGetWorkInfosSync() throws ExecutionException, InterruptedException {
         OneTimeWorkRequest aWork = createTestWorker(); // A
         OneTimeWorkRequest bWork = createTestWorker(); // B
         OneTimeWorkRequest cWork = createTestWorker(); // C
@@ -505,11 +506,11 @@
         WorkContinuation secondChain = mWorkManagerImpl.beginWith(cWork);
         WorkContinuation combined = WorkContinuation.combine(dWork, firstChain, secondChain);
 
-        combined.enqueue().get();
-        List<WorkStatus> statuses = combined.getStatuses().get();
+        combined.enqueue().getResult().get();
+        List<WorkInfo> statuses = combined.getWorkInfos().get();
         assertThat(statuses, is(notNullValue()));
         List<UUID> ids = new ArrayList<>(statuses.size());
-        for (WorkStatus status : statuses) {
+        for (WorkInfo status : statuses) {
             ids.add(status.getId());
         }
         assertThat(ids, containsInAnyOrder(
diff --git a/work/workmanager/src/androidTest/java/androidx/work/impl/WorkManagerImplLargeExecutorTest.java b/work/workmanager/src/androidTest/java/androidx/work/impl/WorkManagerImplLargeExecutorTest.java
index 7ac2545..fa72954 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/impl/WorkManagerImplLargeExecutorTest.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/impl/WorkManagerImplLargeExecutorTest.java
@@ -40,7 +40,7 @@
 import androidx.work.OneTimeWorkRequest;
 import androidx.work.TestLifecycleOwner;
 import androidx.work.WorkContinuation;
-import androidx.work.WorkStatus;
+import androidx.work.WorkInfo;
 import androidx.work.impl.background.greedy.GreedyScheduler;
 import androidx.work.impl.model.WorkSpec;
 import androidx.work.impl.utils.taskexecutor.InstantWorkTaskExecutor;
@@ -150,18 +150,18 @@
         final CountDownLatch latch = new CountDownLatch(NUM_WORKERS);
         WorkContinuation continuation = mWorkManagerImplSpy.beginWith(workRequests);
 
-        continuation.getStatusesLiveData()
-                .observe(mLifecycleOwner, new Observer<List<WorkStatus>>() {
+        continuation.getWorkInfosLiveData()
+                .observe(mLifecycleOwner, new Observer<List<WorkInfo>>() {
                     @Override
-                    public void onChanged(@Nullable List<WorkStatus> workStatuses) {
-                        if (workStatuses == null || workStatuses.isEmpty()) {
+                    public void onChanged(@Nullable List<WorkInfo> workInfos) {
+                        if (workInfos == null || workInfos.isEmpty()) {
                             return;
                         }
 
-                        for (WorkStatus workStatus: workStatuses) {
-                            if (workStatus.getState().isFinished()) {
-                                if (!completed.contains(workStatus.getId())) {
-                                    completed.add(workStatus.getId());
+                        for (WorkInfo workInfo : workInfos) {
+                            if (workInfo.getState().isFinished()) {
+                                if (!completed.contains(workInfo.getId())) {
+                                    completed.add(workInfo.getId());
                                     latch.countDown();
                                 }
                             }
diff --git a/work/workmanager/src/androidTest/java/androidx/work/impl/WorkManagerImplTest.java b/work/workmanager/src/androidTest/java/androidx/work/impl/WorkManagerImplTest.java
index 2a6d062..32db5e4 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/impl/WorkManagerImplTest.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/impl/WorkManagerImplTest.java
@@ -21,12 +21,12 @@
 import static androidx.work.ExistingWorkPolicy.REPLACE;
 import static androidx.work.NetworkType.METERED;
 import static androidx.work.NetworkType.NOT_REQUIRED;
-import static androidx.work.State.BLOCKED;
-import static androidx.work.State.CANCELLED;
-import static androidx.work.State.ENQUEUED;
-import static androidx.work.State.FAILED;
-import static androidx.work.State.RUNNING;
-import static androidx.work.State.SUCCEEDED;
+import static androidx.work.WorkInfo.State.BLOCKED;
+import static androidx.work.WorkInfo.State.CANCELLED;
+import static androidx.work.WorkInfo.State.ENQUEUED;
+import static androidx.work.WorkInfo.State.FAILED;
+import static androidx.work.WorkInfo.State.RUNNING;
+import static androidx.work.WorkInfo.State.SUCCEEDED;
 import static androidx.work.impl.model.WorkSpec.SCHEDULE_NOT_REQUESTED_YET;
 
 import static org.hamcrest.CoreMatchers.is;
@@ -82,8 +82,8 @@
 import androidx.work.PeriodicWorkRequest;
 import androidx.work.TestLifecycleOwner;
 import androidx.work.WorkContinuation;
+import androidx.work.WorkInfo;
 import androidx.work.WorkRequest;
-import androidx.work.WorkStatus;
 import androidx.work.impl.background.systemalarm.RescheduleReceiver;
 import androidx.work.impl.model.Dependency;
 import androidx.work.impl.model.DependencyDao;
@@ -177,7 +177,8 @@
 
         mWorkManagerImpl.beginWith(workArray[0]).then(workArray[1])
                 .then(workArray[2])
-                .enqueue().get();
+                .enqueue().getResult()
+                .get();
 
         for (int i = 0; i < workCount; ++i) {
             String id = workArray[i].getStringId();
@@ -193,7 +194,7 @@
     @SmallTest
     public void testEnqueue_AddsImplicitTags() throws ExecutionException, InterruptedException {
         OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(TestWorker.class).build();
-        mWorkManagerImpl.enqueueInternal(Collections.singletonList(work)).get();
+        mWorkManagerImpl.enqueue(Collections.singletonList(work)).getResult().get();
 
         WorkTagDao workTagDao = mDatabase.workTagDao();
         List<String> tags = workTagDao.getTagsForWorkSpecId(work.getStringId());
@@ -208,7 +209,7 @@
         OneTimeWorkRequest work2 = new OneTimeWorkRequest.Builder(TestWorker.class).build();
         OneTimeWorkRequest work3 = new OneTimeWorkRequest.Builder(TestWorker.class).build();
 
-        mWorkManagerImpl.enqueueInternal(Arrays.asList(work1, work2, work3)).get();
+        mWorkManagerImpl.enqueue(Arrays.asList(work1, work2, work3)).getResult().get();
 
         WorkSpecDao workSpecDao = mDatabase.workSpecDao();
         assertThat(workSpecDao.getWorkSpec(work1.getStringId()), is(notNullValue()));
@@ -227,6 +228,7 @@
 
         mWorkManagerImpl.beginWith(Arrays.asList(work1, work2, work3))
                 .enqueue()
+                .getResult()
                 .get();
 
         WorkSpecDao workSpecDao = mDatabase.workSpecDao();
@@ -249,6 +251,7 @@
         mWorkManagerImpl.beginWith(Arrays.asList(work1a, work1b)).then(work2)
                 .then(Arrays.asList(work3a, work3b))
                 .enqueue()
+                .getResult()
                 .get();
 
         WorkSpecDao workSpecDao = mDatabase.workSpecDao();
@@ -284,12 +287,12 @@
                 .build();
 
         WorkContinuation workContinuation = mWorkManagerImpl.beginWith(work1);
-        workContinuation.enqueue().get();
+        workContinuation.enqueue().getResult().get();
         WorkSpecDao workSpecDao = mDatabase.workSpecDao();
         assertThat(workSpecDao.getState(work1.getStringId()), is(SUCCEEDED));
 
         OneTimeWorkRequest work2 = new OneTimeWorkRequest.Builder(InfiniteTestWorker.class).build();
-        workContinuation.then(work2).enqueue().get();
+        workContinuation.then(work2).enqueue().getResult().get();
         assertThat(workSpecDao.getState(work2.getStringId()), isOneOf(ENQUEUED, RUNNING));
     }
 
@@ -303,12 +306,12 @@
                 .build();
 
         WorkContinuation workContinuation = mWorkManagerImpl.beginWith(work1);
-        workContinuation.enqueue().get();
+        workContinuation.enqueue().getResult().get();
         WorkSpecDao workSpecDao = mDatabase.workSpecDao();
         assertThat(workSpecDao.getState(work1.getStringId()), is(FAILED));
 
         OneTimeWorkRequest work2 = new OneTimeWorkRequest.Builder(TestWorker.class).build();
-        workContinuation.then(work2).enqueue().get();
+        workContinuation.then(work2).enqueue().getResult().get();
         assertThat(workSpecDao.getState(work2.getStringId()), is(FAILED));
     }
 
@@ -322,12 +325,12 @@
                 .build();
 
         WorkContinuation workContinuation = mWorkManagerImpl.beginWith(work1);
-        workContinuation.enqueue().get();
+        workContinuation.enqueue().getResult().get();
         WorkSpecDao workSpecDao = mDatabase.workSpecDao();
         assertThat(workSpecDao.getState(work1.getStringId()), is(CANCELLED));
 
         OneTimeWorkRequest work2 = new OneTimeWorkRequest.Builder(TestWorker.class).build();
-        workContinuation.then(work2).enqueue().get();
+        workContinuation.then(work2).enqueue().getResult().get();
         assertThat(workSpecDao.getState(work2.getStringId()), is(CANCELLED));
     }
 
@@ -353,7 +356,7 @@
                                 .build())
                 .build();
         OneTimeWorkRequest work1 = new OneTimeWorkRequest.Builder(TestWorker.class).build();
-        mWorkManagerImpl.beginWith(work0).then(work1).enqueue().get();
+        mWorkManagerImpl.beginWith(work0).then(work1).enqueue().getResult().get();
 
         WorkSpec workSpec0 = mDatabase.workSpecDao().getWorkSpec(work0.getStringId());
         WorkSpec workSpec1 = mDatabase.workSpecDao().getWorkSpec(work1.getStringId());
@@ -395,7 +398,7 @@
                 .setInitialDelay(expectedInitialDelay, TimeUnit.MILLISECONDS)
                 .build();
         OneTimeWorkRequest work1 = new OneTimeWorkRequest.Builder(TestWorker.class).build();
-        mWorkManagerImpl.beginWith(work0).then(work1).enqueue().get();
+        mWorkManagerImpl.beginWith(work0).then(work1).enqueue().getResult().get();
 
         WorkSpec workSpec0 = mDatabase.workSpecDao().getWorkSpec(work0.getStringId());
         WorkSpec workSpec1 = mDatabase.workSpecDao().getWorkSpec(work1.getStringId());
@@ -413,7 +416,7 @@
                 .setBackoffCriteria(BackoffPolicy.LINEAR, 50000, TimeUnit.MILLISECONDS)
                 .build();
         OneTimeWorkRequest work1 = new OneTimeWorkRequest.Builder(TestWorker.class).build();
-        mWorkManagerImpl.beginWith(work0).then(work1).enqueue().get();
+        mWorkManagerImpl.beginWith(work0).then(work1).enqueue().getResult().get();
 
         WorkSpec workSpec0 = mDatabase.workSpecDao().getWorkSpec(work0.getStringId());
         WorkSpec workSpec1 = mDatabase.workSpecDao().getWorkSpec(work1.getStringId());
@@ -441,7 +444,7 @@
                 .addTag(firstTag)
                 .build();
         OneTimeWorkRequest work2 = new OneTimeWorkRequest.Builder(TestWorker.class).build();
-        mWorkManagerImpl.beginWith(work0).then(work1).then(work2).enqueue().get();
+        mWorkManagerImpl.beginWith(work0).then(work1).then(work2).enqueue().getResult().get();
 
         WorkTagDao workTagDao = mDatabase.workTagDao();
         assertThat(workTagDao.getWorkSpecIdsWithTag(firstTag),
@@ -460,7 +463,9 @@
                 TimeUnit.MILLISECONDS)
                 .build();
 
-        mWorkManagerImpl.enqueueInternal(Collections.singletonList(periodicWork)).get();
+        mWorkManagerImpl.enqueue(Collections.singletonList(periodicWork))
+                .getResult()
+                .get();
 
         WorkSpec workSpec = mDatabase.workSpecDao().getWorkSpec(periodicWork.getStringId());
         assertThat(workSpec.isPeriodic(), is(true));
@@ -478,7 +483,7 @@
 
         long beforeEnqueueTime = System.currentTimeMillis();
 
-        mWorkManagerImpl.beginWith(work).enqueue().get();
+        mWorkManagerImpl.beginWith(work).enqueue().getResult().get();
         WorkSpec workSpec = mDatabase.workSpecDao().getWorkSpec(work.getStringId());
         assertThat(workSpec.periodStartTime, is(greaterThanOrEqualTo(beforeEnqueueTime)));
     }
@@ -497,7 +502,9 @@
 
         long beforeEnqueueTime = System.currentTimeMillis();
 
-        mWorkManagerImpl.enqueueInternal(Collections.singletonList(periodicWork)).get();
+        mWorkManagerImpl.enqueue(Collections.singletonList(periodicWork))
+                .getResult()
+                .get();
 
         WorkSpec workSpec = mDatabase.workSpecDao().getWorkSpec(periodicWork.getStringId());
         assertThat(workSpec.periodStartTime, is(greaterThanOrEqualTo(beforeEnqueueTime)));
@@ -515,7 +522,8 @@
 
         mWorkManagerImpl.beginUniqueWork(uniqueName, REPLACE, work)
                 .then(next)
-                .enqueue().get();
+                .enqueue().getResult()
+                .get();
 
         List<String> workSpecIds = mDatabase.workNameDao().getWorkSpecIdsWithName(uniqueName);
         assertThat(work.getStringId(), isIn(workSpecIds));
@@ -533,10 +541,10 @@
                 15L,
                 TimeUnit.MINUTES)
                 .build();
-        mWorkManagerImpl.enqueueUniquePeriodicWorkInternal(
+        mWorkManagerImpl.enqueueUniquePeriodicWork(
                 uniqueName,
                 ExistingPeriodicWorkPolicy.REPLACE,
-                periodicWork).get();
+                periodicWork).getResult().get();
 
         List<String> workSpecIds = mDatabase.workNameDao().getWorkSpecIdsWithName(uniqueName);
         assertThat(periodicWork.getStringId(), isIn(workSpecIds));
@@ -563,7 +571,7 @@
         mWorkManagerImpl
                 .beginUniqueWork(uniqueName, REPLACE, replacementWork1)
                 .then(replacementWork2)
-                .enqueue().get();
+                .enqueue().getResult().get();
 
         workSpecIds = mDatabase.workNameDao().getWorkSpecIdsWithName(uniqueName);
         assertThat(
@@ -598,10 +606,10 @@
                 30L,
                 TimeUnit.MINUTES)
                 .build();
-        mWorkManagerImpl.enqueueUniquePeriodicWorkInternal(
+        mWorkManagerImpl.enqueueUniquePeriodicWork(
                 uniqueName,
                 ExistingPeriodicWorkPolicy.REPLACE,
-                replacementWork).get();
+                replacementWork).getResult().get();
 
         workSpecIds = mDatabase.workNameDao().getWorkSpecIdsWithName(uniqueName);
         assertThat(workSpecIds, contains(replacementWork.getStringId()));
@@ -633,6 +641,7 @@
                 .beginUniqueWork(uniqueName, KEEP, replacementWork1)
                 .then(replacementWork2)
                 .enqueue()
+                .getResult()
                 .get();
 
         workSpecIds = mDatabase.workNameDao().getWorkSpecIdsWithName(uniqueName);
@@ -666,10 +675,10 @@
                 30L,
                 TimeUnit.MINUTES)
                 .build();
-        mWorkManagerImpl.enqueueUniquePeriodicWorkInternal(
+        mWorkManagerImpl.enqueueUniquePeriodicWork(
                 uniqueName,
                 ExistingPeriodicWorkPolicy.KEEP,
-                replacementWork).get();
+                replacementWork).getResult().get();
 
         workSpecIds = mDatabase.workNameDao().getWorkSpecIdsWithName(uniqueName);
         assertThat(workSpecIds, contains(originalWork.getStringId()));
@@ -701,7 +710,7 @@
         mWorkManagerImpl
                 .beginUniqueWork(uniqueName, KEEP, replacementWork1)
                 .then(replacementWork2)
-                .enqueue().get();
+                .enqueue().getResult().get();
 
         workSpecIds = mDatabase.workNameDao().getWorkSpecIdsWithName(uniqueName);
         assertThat(workSpecIds,
@@ -736,10 +745,10 @@
                 30L,
                 TimeUnit.MINUTES)
                 .build();
-        mWorkManagerImpl.enqueueUniquePeriodicWorkInternal(
+        mWorkManagerImpl.enqueueUniquePeriodicWork(
                 uniqueName,
                 ExistingPeriodicWorkPolicy.KEEP,
-                replacementWork).get();
+                replacementWork).getResult().get();
 
         workSpecIds = mDatabase.workNameDao().getWorkSpecIdsWithName(uniqueName);
         assertThat(workSpecIds, contains(replacementWork.getStringId()));
@@ -768,7 +777,7 @@
         mWorkManagerImpl
                 .beginUniqueWork(uniqueName, APPEND, appendWork1)
                 .then(appendWork2)
-                .enqueue().get();
+                .enqueue().getResult().get();
 
         workSpecIds = mDatabase.workNameDao().getWorkSpecIdsWithName(uniqueName);
         assertThat(workSpecIds,
@@ -806,10 +815,10 @@
 
         OneTimeWorkRequest appendWork1 = new OneTimeWorkRequest.Builder(TestWorker.class).build();
         OneTimeWorkRequest appendWork2 = new OneTimeWorkRequest.Builder(TestWorker.class).build();
-        mWorkManagerImpl.enqueueUniqueWorkInternal(
+        mWorkManagerImpl.enqueueUniqueWork(
                 uniqueName,
                 APPEND,
-                Arrays.asList(appendWork1, appendWork2)).get();
+                Arrays.asList(appendWork1, appendWork2)).getResult().get();
         workSpecIds = mDatabase.workNameDao().getWorkSpecIdsWithName(uniqueName);
         assertThat(workSpecIds,
                 containsInAnyOrder(
@@ -860,7 +869,7 @@
         mWorkManagerImpl
                 .beginUniqueWork(uniqueName, APPEND, appendWork1)
                 .then(appendWork2)
-                .enqueue().get();
+                .enqueue().getResult().get();
 
         workSpecIds = mDatabase.workNameDao().getWorkSpecIdsWithName(uniqueName);
         assertThat(workSpecIds,
@@ -899,7 +908,7 @@
         mWorkManagerImpl
                 .beginUniqueWork(uniqueName, APPEND, appendWork1)
                 .then(appendWork2)
-                .enqueue().get();
+                .enqueue().getResult().get();
 
         List<String> workSpecIds = mDatabase.workNameDao().getWorkSpecIdsWithName(uniqueName);
         assertThat(workSpecIds,
@@ -908,58 +917,58 @@
 
     @Test
     @SmallTest
-    public void testGetStatusByIdSync() throws ExecutionException, InterruptedException {
+    public void testGetWorkInfoByIdSync() throws ExecutionException, InterruptedException {
         OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(TestWorker.class)
                 .setInitialState(SUCCEEDED)
                 .build();
         insertWorkSpecAndTags(work);
 
-        WorkStatus workStatus = mWorkManagerImpl.getStatusById(work.getId()).get();
-        assertThat(workStatus.getId().toString(), is(work.getStringId()));
-        assertThat(workStatus.getState(), is(SUCCEEDED));
+        WorkInfo workInfo = mWorkManagerImpl.getWorkInfoById(work.getId()).get();
+        assertThat(workInfo.getId().toString(), is(work.getStringId()));
+        assertThat(workInfo.getState(), is(SUCCEEDED));
     }
 
     @Test
     @SmallTest
-    public void testGetStatusByIdSync_returnsNullIfNotInDatabase()
+    public void testGetWorkInfoByIdSync_returnsNullIfNotInDatabase()
             throws ExecutionException, InterruptedException {
 
-        WorkStatus workStatus = mWorkManagerImpl.getStatusById(UUID.randomUUID()).get();
-        assertThat(workStatus, is(nullValue()));
+        WorkInfo workInfo = mWorkManagerImpl.getWorkInfoById(UUID.randomUUID()).get();
+        assertThat(workInfo, is(nullValue()));
     }
 
     @Test
     @SmallTest
     @SuppressWarnings("unchecked")
-    public void testGetStatusesById() {
+    public void testGetWorkInfoById() {
         OneTimeWorkRequest work0 = new OneTimeWorkRequest.Builder(TestWorker.class).build();
         OneTimeWorkRequest work1 = new OneTimeWorkRequest.Builder(TestWorker.class).build();
         insertWorkSpecAndTags(work0);
         insertWorkSpecAndTags(work1);
 
-        Observer<List<WorkStatus>> mockObserver = mock(Observer.class);
+        Observer<List<WorkInfo>> mockObserver = mock(Observer.class);
 
         TestLifecycleOwner testLifecycleOwner = new TestLifecycleOwner();
-        LiveData<List<WorkStatus>> liveData = mWorkManagerImpl.getStatusesById(
+        LiveData<List<WorkInfo>> liveData = mWorkManagerImpl.getWorkInfosById(
                 Arrays.asList(work0.getStringId(), work1.getStringId()));
         liveData.observe(testLifecycleOwner, mockObserver);
 
-        ArgumentCaptor<List<WorkStatus>> captor = ArgumentCaptor.forClass(List.class);
+        ArgumentCaptor<List<WorkInfo>> captor = ArgumentCaptor.forClass(List.class);
         verify(mockObserver).onChanged(captor.capture());
         assertThat(captor.getValue(), is(not(nullValue())));
         assertThat(captor.getValue().size(), is(2));
 
-        WorkStatus workStatus0 = new WorkStatus(
+        WorkInfo workInfo0 = new WorkInfo(
                 work0.getId(),
                 ENQUEUED,
                 Data.EMPTY,
                 Collections.singletonList(TestWorker.class.getName()));
-        WorkStatus workStatus1 = new WorkStatus(
+        WorkInfo workInfo1 = new WorkInfo(
                 work1.getId(),
                 ENQUEUED,
                 Data.EMPTY,
                 Collections.singletonList(TestWorker.class.getName()));
-        assertThat(captor.getValue(), containsInAnyOrder(workStatus0, workStatus1));
+        assertThat(captor.getValue(), containsInAnyOrder(workInfo0, workInfo1));
 
         WorkSpecDao workSpecDao = mDatabase.workSpecDao();
         workSpecDao.setState(RUNNING, work0.getStringId());
@@ -968,12 +977,12 @@
         assertThat(captor.getValue(), is(not(nullValue())));
         assertThat(captor.getValue().size(), is(2));
 
-        workStatus0 = new WorkStatus(
+        workInfo0 = new WorkInfo(
                 work0.getId(),
                 RUNNING,
                 Data.EMPTY,
                 Collections.singletonList(TestWorker.class.getName()));
-        assertThat(captor.getValue(), containsInAnyOrder(workStatus0, workStatus1));
+        assertThat(captor.getValue(), containsInAnyOrder(workInfo0, workInfo1));
 
         clearInvocations(mockObserver);
         workSpecDao.setState(RUNNING, work1.getStringId());
@@ -982,19 +991,19 @@
         assertThat(captor.getValue(), is(not(nullValue())));
         assertThat(captor.getValue().size(), is(2));
 
-        workStatus1 = new WorkStatus(
+        workInfo1 = new WorkInfo(
                 work1.getId(),
                 RUNNING,
                 Data.EMPTY,
                 Collections.singletonList(TestWorker.class.getName()));
-        assertThat(captor.getValue(), containsInAnyOrder(workStatus0, workStatus1));
+        assertThat(captor.getValue(), containsInAnyOrder(workInfo0, workInfo1));
 
         liveData.removeObservers(testLifecycleOwner);
     }
 
     @Test
     @SmallTest
-    public void testGetStatusesByTagSync() throws ExecutionException, InterruptedException {
+    public void testGetWorkInfosByTagSync() throws ExecutionException, InterruptedException {
         final String firstTag = "first_tag";
         final String secondTag = "second_tag";
 
@@ -1015,35 +1024,35 @@
         insertWorkSpecAndTags(work1);
         insertWorkSpecAndTags(work2);
 
-        WorkStatus workStatus0 = new WorkStatus(
+        WorkInfo workInfo0 = new WorkInfo(
                 work0.getId(),
                 RUNNING,
                 Data.EMPTY,
                 Arrays.asList(TestWorker.class.getName(), firstTag, secondTag));
-        WorkStatus workStatus1 = new WorkStatus(
+        WorkInfo workInfo1 = new WorkInfo(
                 work1.getId(),
                 BLOCKED,
                 Data.EMPTY,
                 Arrays.asList(TestWorker.class.getName(), firstTag));
-        WorkStatus workStatus2 = new WorkStatus(
+        WorkInfo workInfo2 = new WorkInfo(
                 work2.getId(),
                 SUCCEEDED,
                 Data.EMPTY,
                 Arrays.asList(TestWorker.class.getName(), secondTag));
 
-        List<WorkStatus> workStatuses = mWorkManagerImpl.getStatusesByTag(firstTag).get();
-        assertThat(workStatuses, containsInAnyOrder(workStatus0, workStatus1));
+        List<WorkInfo> workInfos = mWorkManagerImpl.getWorkInfosByTag(firstTag).get();
+        assertThat(workInfos, containsInAnyOrder(workInfo0, workInfo1));
 
-        workStatuses = mWorkManagerImpl.getStatusesByTag(secondTag).get();
-        assertThat(workStatuses, containsInAnyOrder(workStatus0, workStatus2));
+        workInfos = mWorkManagerImpl.getWorkInfosByTag(secondTag).get();
+        assertThat(workInfos, containsInAnyOrder(workInfo0, workInfo2));
 
-        workStatuses = mWorkManagerImpl.getStatusesByTag("dummy").get();
-        assertThat(workStatuses.size(), is(0));
+        workInfos = mWorkManagerImpl.getWorkInfosByTag("dummy").get();
+        assertThat(workInfos.size(), is(0));
     }
 
     @Test
     @SmallTest
-    public void getStatusByNameSync() throws ExecutionException, InterruptedException {
+    public void getWorkInfosByNameSync() throws ExecutionException, InterruptedException {
         final String uniqueName = "myname";
 
         OneTimeWorkRequest work0 = new OneTimeWorkRequest.Builder(InfiniteTestWorker.class)
@@ -1059,33 +1068,33 @@
         insertDependency(work1, work0);
         insertDependency(work2, work1);
 
-        WorkStatus workStatus0 = new WorkStatus(
+        WorkInfo workInfo0 = new WorkInfo(
                 work0.getId(),
                 RUNNING,
                 Data.EMPTY,
                 Collections.singletonList(InfiniteTestWorker.class.getName()));
-        WorkStatus workStatus1 = new WorkStatus(
+        WorkInfo workInfo1 = new WorkInfo(
                 work1.getId(),
                 BLOCKED,
                 Data.EMPTY,
                 Collections.singletonList(InfiniteTestWorker.class.getName()));
-        WorkStatus workStatus2 = new WorkStatus(
+        WorkInfo workInfo2 = new WorkInfo(
                 work2.getId(),
                 BLOCKED,
                 Data.EMPTY,
                 Collections.singletonList(InfiniteTestWorker.class.getName()));
 
-        List<WorkStatus> workStatuses = mWorkManagerImpl.getStatusesForUniqueWork(uniqueName).get();
-        assertThat(workStatuses, containsInAnyOrder(workStatus0, workStatus1, workStatus2));
+        List<WorkInfo> workInfos = mWorkManagerImpl.getWorkInfosForUniqueWork(uniqueName).get();
+        assertThat(workInfos, containsInAnyOrder(workInfo0, workInfo1, workInfo2));
 
-        workStatuses = mWorkManagerImpl.getStatusesForUniqueWork("dummy").get();
-        assertThat(workStatuses.size(), is(0));
+        workInfos = mWorkManagerImpl.getWorkInfosForUniqueWork("dummy").get();
+        assertThat(workInfos.size(), is(0));
     }
 
     @Test
     @SmallTest
     @SuppressWarnings("unchecked")
-    public void testGetStatusesByName() {
+    public void testGetWorkInfosByName() {
         final String uniqueName = "myname";
         WorkSpecDao workSpecDao = mDatabase.workSpecDao();
 
@@ -1102,34 +1111,34 @@
         insertDependency(work1, work0);
         insertDependency(work2, work1);
 
-        Observer<List<WorkStatus>> mockObserver = mock(Observer.class);
+        Observer<List<WorkInfo>> mockObserver = mock(Observer.class);
 
         TestLifecycleOwner testLifecycleOwner = new TestLifecycleOwner();
-        LiveData<List<WorkStatus>> liveData =
-                mWorkManagerImpl.getStatusesForUniqueWorkLiveData(uniqueName);
+        LiveData<List<WorkInfo>> liveData =
+                mWorkManagerImpl.getWorkInfosForUniqueWorkLiveData(uniqueName);
         liveData.observe(testLifecycleOwner, mockObserver);
 
-        ArgumentCaptor<List<WorkStatus>> captor = ArgumentCaptor.forClass(List.class);
+        ArgumentCaptor<List<WorkInfo>> captor = ArgumentCaptor.forClass(List.class);
         verify(mockObserver).onChanged(captor.capture());
         assertThat(captor.getValue(), is(not(nullValue())));
         assertThat(captor.getValue().size(), is(3));
 
-        WorkStatus workStatus0 = new WorkStatus(
+        WorkInfo workInfo0 = new WorkInfo(
                 work0.getId(),
                 RUNNING,
                 Data.EMPTY,
                 Collections.singletonList(InfiniteTestWorker.class.getName()));
-        WorkStatus workStatus1 = new WorkStatus(
+        WorkInfo workInfo1 = new WorkInfo(
                 work1.getId(),
                 BLOCKED,
                 Data.EMPTY,
                 Collections.singletonList(InfiniteTestWorker.class.getName()));
-        WorkStatus workStatus2 = new WorkStatus(
+        WorkInfo workInfo2 = new WorkInfo(
                 work2.getId(),
                 BLOCKED,
                 Data.EMPTY,
                 Collections.singletonList(InfiniteTestWorker.class.getName()));
-        assertThat(captor.getValue(), containsInAnyOrder(workStatus0, workStatus1, workStatus2));
+        assertThat(captor.getValue(), containsInAnyOrder(workInfo0, workInfo1, workInfo2));
 
         workSpecDao.setState(ENQUEUED, work0.getStringId());
 
@@ -1137,12 +1146,12 @@
         assertThat(captor.getValue(), is(not(nullValue())));
         assertThat(captor.getValue().size(), is(3));
 
-        workStatus0 = new WorkStatus(
+        workInfo0 = new WorkInfo(
                 work0.getId(),
                 ENQUEUED,
                 Data.EMPTY,
                 Collections.singletonList(InfiniteTestWorker.class.getName()));
-        assertThat(captor.getValue(), containsInAnyOrder(workStatus0, workStatus1, workStatus2));
+        assertThat(captor.getValue(), containsInAnyOrder(workInfo0, workInfo1, workInfo2));
 
         liveData.removeObservers(testLifecycleOwner);
     }
@@ -1157,7 +1166,7 @@
         insertWorkSpecAndTags(work0);
         insertWorkSpecAndTags(work1);
 
-        mWorkManagerImpl.cancelWorkByIdInternal(work0.getId()).get();
+        mWorkManagerImpl.cancelWorkById(work0.getId()).getResult().get();
         assertThat(workSpecDao.getState(work0.getStringId()), is(CANCELLED));
         assertThat(workSpecDao.getState(work1.getStringId()), is(not(CANCELLED)));
     }
@@ -1177,7 +1186,7 @@
         insertWorkSpecAndTags(work1);
         insertDependency(work1, work0);
 
-        mWorkManagerImpl.cancelWorkByIdInternal(work0.getId()).get();
+        mWorkManagerImpl.cancelWorkById(work0.getId()).getResult().get();
 
         assertThat(workSpecDao.getState(work0.getStringId()), is(CANCELLED));
         assertThat(workSpecDao.getState(work1.getStringId()), is(CANCELLED));
@@ -1200,7 +1209,7 @@
         insertWorkSpecAndTags(work1);
         insertDependency(work1, work0);
 
-        mWorkManagerImpl.cancelWorkByIdInternal(work0.getId()).get();
+        mWorkManagerImpl.cancelWorkById(work0.getId()).getResult().get();
 
         assertThat(workSpecDao.getState(work0.getStringId()), is(SUCCEEDED));
         assertThat(workSpecDao.getState(work1.getStringId()), is(CANCELLED));
@@ -1231,7 +1240,7 @@
         insertWorkSpecAndTags(work2);
         insertWorkSpecAndTags(work3);
 
-        mWorkManagerImpl.cancelAllWorkByTagInternal(tagToClear).get();
+        mWorkManagerImpl.cancelAllWorkByTag(tagToClear).getResult().get();
 
         assertThat(workSpecDao.getState(work0.getStringId()), is(CANCELLED));
         assertThat(workSpecDao.getState(work1.getStringId()), is(CANCELLED));
@@ -1276,7 +1285,7 @@
         insertDependency(work1, work0);
         insertDependency(work4, work0);
 
-        mWorkManagerImpl.cancelAllWorkByTagInternal(tag).get();
+        mWorkManagerImpl.cancelAllWorkByTag(tag).getResult().get();
 
         WorkSpecDao workSpecDao = mDatabase.workSpecDao();
         assertThat(workSpecDao.getState(work0.getStringId()), is(CANCELLED));
@@ -1295,7 +1304,7 @@
         OneTimeWorkRequest work1 = new OneTimeWorkRequest.Builder(InfiniteTestWorker.class).build();
         insertNamedWorks(uniqueName, work0, work1);
 
-        mWorkManagerImpl.cancelUniqueWorkInternal(uniqueName).get();
+        mWorkManagerImpl.cancelUniqueWork(uniqueName).getResult().get();
 
         WorkSpecDao workSpecDao = mDatabase.workSpecDao();
         assertThat(workSpecDao.getState(work0.getStringId()), is(CANCELLED));
@@ -1315,7 +1324,7 @@
         OneTimeWorkRequest work1 = new OneTimeWorkRequest.Builder(InfiniteTestWorker.class).build();
         insertNamedWorks(uniqueName, work0, work1);
 
-        mWorkManagerImpl.cancelUniqueWorkInternal(uniqueName).get();
+        mWorkManagerImpl.cancelUniqueWork(uniqueName).getResult().get();
 
         WorkSpecDao workSpecDao = mDatabase.workSpecDao();
         assertThat(workSpecDao.getState(work0.getStringId()), is(SUCCEEDED));
@@ -1339,7 +1348,7 @@
         assertThat(workSpecDao.getState(work1.getStringId()), is(ENQUEUED));
         assertThat(workSpecDao.getState(work2.getStringId()), is(SUCCEEDED));
 
-        mWorkManagerImpl.cancelAllWorkInternal().get();
+        mWorkManagerImpl.cancelAllWork().getResult().get();
         assertThat(workSpecDao.getState(work0.getStringId()), is(CANCELLED));
         assertThat(workSpecDao.getState(work1.getStringId()), is(CANCELLED));
         assertThat(workSpecDao.getState(work2.getStringId()), is(SUCCEEDED));
@@ -1409,7 +1418,7 @@
 
         insertDependency(enqueuedWork, finishedWorkWithUnfinishedDependent);
 
-        mWorkManagerImpl.pruneWorkInternal().get();
+        mWorkManagerImpl.pruneWork().getResult().get();
 
         WorkSpecDao workSpecDao = mDatabase.workSpecDao();
         assertThat(workSpecDao.getWorkSpec(enqueuedWork.getStringId()), is(notNullValue()));
@@ -1422,7 +1431,7 @@
 
     @Test
     @SmallTest
-    public void testSynchronousCancelAndGetStatus()
+    public void testSynchronousCancelAndGetWorkInfo()
             throws ExecutionException, InterruptedException {
 
         OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(TestWorker.class).build();
@@ -1431,8 +1440,8 @@
         WorkSpecDao workSpecDao = mDatabase.workSpecDao();
         assertThat(workSpecDao.getState(work.getStringId()), is(ENQUEUED));
 
-        mWorkManagerImpl.cancelWorkByIdInternal(work.getId()).get();
-        assertThat(mWorkManagerImpl.getStatusById(work.getId()).get().getState(), is(CANCELLED));
+        mWorkManagerImpl.cancelWorkById(work.getId()).getResult().get();
+        assertThat(mWorkManagerImpl.getWorkInfoById(work.getId()).get().getState(), is(CANCELLED));
     }
 
     @Test
@@ -1542,7 +1551,9 @@
                 new OneTimeWorkRequest.Builder(StopAwareWorker.class)
                         .build();
 
-        mWorkManagerImpl.enqueueInternal(Collections.singletonList(stopAwareWorkRequest)).get();
+        mWorkManagerImpl.enqueue(Collections.singletonList(stopAwareWorkRequest))
+                .getResult().get();
+
         ComponentName componentName = new ComponentName(mContext, RescheduleReceiver.class);
         verify(packageManager, times(1))
                 .setComponentEnabledSetting(eq(componentName),
@@ -1550,7 +1561,9 @@
                         eq(PackageManager.DONT_KILL_APP));
 
         reset(packageManager);
-        mWorkManagerImpl.cancelWorkByIdInternal(stopAwareWorkRequest.getId()).get();
+        mWorkManagerImpl.cancelWorkById(stopAwareWorkRequest.getId())
+                .getResult()
+                .get();
         // Sleeping for a little bit, to give the listeners a chance to catch up.
         Thread.sleep(SLEEP_DURATION_SMALL_MILLIS);
         // There is a small chance that we will call this method twice. Once when the Worker was
@@ -1574,7 +1587,7 @@
                         .setRequiresBatteryNotLow(true)
                         .build())
                 .build();
-        mWorkManagerImpl.beginWith(work).enqueue().get();
+        mWorkManagerImpl.beginWith(work).enqueue().getResult().get();
 
         WorkSpec workSpec = mDatabase.workSpecDao().getWorkSpec(work.getStringId());
         assertThat(workSpec.workerClassName, is(TestWorker.class.getName()));
@@ -1591,7 +1604,7 @@
                         .setRequiresStorageNotLow(true)
                         .build())
                 .build();
-        mWorkManagerImpl.beginWith(work).enqueue().get();
+        mWorkManagerImpl.beginWith(work).enqueue().getResult().get();
 
         WorkSpec workSpec = mDatabase.workSpecDao().getWorkSpec(work.getStringId());
         assertThat(workSpec.workerClassName, is(TestWorker.class.getName()));
@@ -1608,7 +1621,7 @@
                 .setRequiresBatteryNotLow(true)
                 .build())
                 .build();
-        mWorkManagerImpl.beginWith(work).enqueue().get();
+        mWorkManagerImpl.beginWith(work).enqueue().getResult().get();
 
         WorkSpec workSpec = mDatabase.workSpecDao().getWorkSpec(work.getStringId());
         assertThat(workSpec.workerClassName, is(ConstraintTrackingWorker.class.getName()));
@@ -1628,7 +1641,7 @@
                         .setRequiresStorageNotLow(true)
                         .build())
                 .build();
-        mWorkManagerImpl.beginWith(work).enqueue().get();
+        mWorkManagerImpl.beginWith(work).enqueue().getResult().get();
 
         WorkSpec workSpec = mDatabase.workSpecDao().getWorkSpec(work.getStringId());
         assertThat(workSpec.workerClassName, is(ConstraintTrackingWorker.class.getName()));
@@ -1648,7 +1661,7 @@
                         .setRequiresBatteryNotLow(true)
                         .build())
                 .build();
-        mWorkManagerImpl.beginWith(work).enqueue().get();
+        mWorkManagerImpl.beginWith(work).enqueue().getResult().get();
 
         WorkSpec workSpec = mDatabase.workSpecDao().getWorkSpec(work.getStringId());
         assertThat(workSpec.workerClassName, is(TestWorker.class.getName()));
@@ -1665,7 +1678,7 @@
                         .setRequiresStorageNotLow(true)
                         .build())
                 .build();
-        mWorkManagerImpl.beginWith(work).enqueue().get();
+        mWorkManagerImpl.beginWith(work).enqueue().getResult().get();
 
         WorkSpec workSpec = mDatabase.workSpecDao().getWorkSpec(work.getStringId());
         assertThat(workSpec.workerClassName, is(TestWorker.class.getName()));
@@ -1693,10 +1706,10 @@
         final CountDownLatch latch = new CountDownLatch(3);
         TestLifecycleOwner testLifecycleOwner = new TestLifecycleOwner();
 
-        LiveData<WorkStatus> status = mWorkManagerImpl.getStatusByIdLiveData(work.getId());
-        status.observe(testLifecycleOwner, new Observer<WorkStatus>() {
+        LiveData<WorkInfo> status = mWorkManagerImpl.getWorkInfoByIdLiveData(work.getId());
+        status.observe(testLifecycleOwner, new Observer<WorkInfo>() {
             @Override
-            public void onChanged(@Nullable WorkStatus workStatus) {
+            public void onChanged(@Nullable WorkInfo workStatus) {
                 if (workStatus != null) {
                     if (workStatus.getState() == RUNNING) {
                         latch.countDown();
diff --git a/work/workmanager/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java b/work/workmanager/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java
index 9022fab..363710b 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java
@@ -16,12 +16,12 @@
 
 package androidx.work.impl;
 
-import static androidx.work.State.BLOCKED;
-import static androidx.work.State.CANCELLED;
-import static androidx.work.State.ENQUEUED;
-import static androidx.work.State.FAILED;
-import static androidx.work.State.RUNNING;
-import static androidx.work.State.SUCCEEDED;
+import static androidx.work.WorkInfo.State.BLOCKED;
+import static androidx.work.WorkInfo.State.CANCELLED;
+import static androidx.work.WorkInfo.State.ENQUEUED;
+import static androidx.work.WorkInfo.State.FAILED;
+import static androidx.work.WorkInfo.State.RUNNING;
+import static androidx.work.WorkInfo.State.SUCCEEDED;
 
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.is;
@@ -879,7 +879,6 @@
         Executors.newSingleThreadExecutor().submit(workerWrapper);
         workerWrapper.interrupt(false);
         assertThat(worker.isStopped(), is(true));
-        assertThat(worker.isCancelled(), is(false));
     }
 
     @Test
@@ -912,7 +911,6 @@
         Executors.newSingleThreadExecutor().submit(workerWrapper);
         workerWrapper.interrupt(true);
         assertThat(worker.isStopped(), is(true));
-        assertThat(worker.isCancelled(), is(true));
     }
 
     @Test
diff --git a/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcherTest.java b/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcherTest.java
index 86ad91b..95f75e6 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcherTest.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcherTest.java
@@ -44,7 +44,7 @@
 import androidx.work.DatabaseTest;
 import androidx.work.Logger;
 import androidx.work.OneTimeWorkRequest;
-import androidx.work.State;
+import androidx.work.WorkInfo;
 import androidx.work.impl.Processor;
 import androidx.work.impl.Scheduler;
 import androidx.work.impl.WorkManagerImpl;
@@ -380,7 +380,7 @@
                         CommandHandler.ACTION_EXECUTION_COMPLETED,
                         CommandHandler.ACTION_CONSTRAINTS_CHANGED));
 
-        assertThat(workSpec.state, is(State.ENQUEUED));
+        assertThat(workSpec.state, is(WorkInfo.State.ENQUEUED));
     }
 
     @Test
@@ -430,7 +430,7 @@
                         CommandHandler.ACTION_EXECUTION_COMPLETED,
                         CommandHandler.ACTION_CONSTRAINTS_CHANGED));
 
-        assertThat(workSpec.state, is(State.SUCCEEDED));
+        assertThat(workSpec.state, is(WorkInfo.State.SUCCEEDED));
     }
 
     @Test
@@ -443,12 +443,12 @@
 
         OneTimeWorkRequest failed = new OneTimeWorkRequest.Builder(TestWorker.class)
                 .setPeriodStartTime(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
-                .setInitialState(State.FAILED)
+                .setInitialState(WorkInfo.State.FAILED)
                 .build();
 
         OneTimeWorkRequest succeeded = new OneTimeWorkRequest.Builder(TestWorker.class)
                 .setPeriodStartTime(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
-                .setInitialState(State.SUCCEEDED)
+                .setInitialState(WorkInfo.State.SUCCEEDED)
                 .build();
 
         OneTimeWorkRequest noConstraints = new OneTimeWorkRequest.Builder(TestWorker.class)
@@ -501,12 +501,12 @@
 
         OneTimeWorkRequest failed = new OneTimeWorkRequest.Builder(TestWorker.class)
                 .setPeriodStartTime(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
-                .setInitialState(State.FAILED)
+                .setInitialState(WorkInfo.State.FAILED)
                 .build();
 
         OneTimeWorkRequest succeeded = new OneTimeWorkRequest.Builder(TestWorker.class)
                 .setPeriodStartTime(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
-                .setInitialState(State.SUCCEEDED)
+                .setInitialState(WorkInfo.State.SUCCEEDED)
                 .build();
 
         OneTimeWorkRequest noConstraints = new OneTimeWorkRequest.Builder(TestWorker.class)
diff --git a/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobSchedulerTest.java b/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobSchedulerTest.java
index 6a6ff9e..05e8448 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobSchedulerTest.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobSchedulerTest.java
@@ -44,7 +44,7 @@
 import androidx.test.runner.AndroidJUnit4;
 import androidx.work.Configuration;
 import androidx.work.OneTimeWorkRequest;
-import androidx.work.State;
+import androidx.work.WorkInfo;
 import androidx.work.WorkManagerTest;
 import androidx.work.impl.WorkDatabase;
 import androidx.work.impl.WorkManagerImpl;
@@ -184,7 +184,7 @@
     @SdkSuppress(minSdkVersion = 23)
     public void testSystemJobScheduler_ignoresUnenqueuedWork() {
         OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(TestWorker.class)
-                .setInitialState(State.CANCELLED)
+                .setInitialState(WorkInfo.State.CANCELLED)
                 .build();
         WorkSpec workSpec = getWorkSpec(work);
         // Don't use addToWorkSpecDao and put it in the database mock.
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 91b2502..c5f693b 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
@@ -44,7 +44,7 @@
 import androidx.test.runner.AndroidJUnit4;
 import androidx.work.Configuration;
 import androidx.work.OneTimeWorkRequest;
-import androidx.work.State;
+import androidx.work.WorkInfo;
 import androidx.work.WorkManagerTest;
 import androidx.work.WorkRequest;
 import androidx.work.Worker;
@@ -153,12 +153,12 @@
         Thread.sleep(5000L);
 
         WorkSpecDao workSpecDao = mDatabase.workSpecDao();
-        assertThat(workSpecDao.getState(work.getStringId()), is(State.RUNNING));
+        assertThat(workSpecDao.getState(work.getStringId()), is(WorkInfo.State.RUNNING));
 
         mSystemJobServiceSpy.onStopJob(mockParams);
         // TODO(rahulrav): Figure out why this test is flaky.
         Thread.sleep(5000L);
-        assertThat(workSpecDao.getState(work.getStringId()), is(State.ENQUEUED));
+        assertThat(workSpecDao.getState(work.getStringId()), is(WorkInfo.State.ENQUEUED));
     }
 
     @Test
diff --git a/work/workmanager/src/androidTest/java/androidx/work/impl/workers/ConstraintTrackingWorkerTest.java b/work/workmanager/src/androidTest/java/androidx/work/impl/workers/ConstraintTrackingWorkerTest.java
index 10aa979..04d65e9 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/impl/workers/ConstraintTrackingWorkerTest.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/impl/workers/ConstraintTrackingWorkerTest.java
@@ -38,7 +38,7 @@
 import androidx.work.DatabaseTest;
 import androidx.work.ListenableWorker;
 import androidx.work.OneTimeWorkRequest;
-import androidx.work.State;
+import androidx.work.WorkInfo;
 import androidx.work.WorkerFactory;
 import androidx.work.WorkerParameters;
 import androidx.work.impl.Scheduler;
@@ -134,7 +134,7 @@
         mWorkTaskExecutor.getBackgroundExecutor().execute(mWorkerWrapper);
 
         WorkSpec workSpec = mDatabase.workSpecDao().getWorkSpec(mWork.getStringId());
-        assertThat(workSpec.state, is(State.SUCCEEDED));
+        assertThat(workSpec.state, is(WorkInfo.State.SUCCEEDED));
         Data output = workSpec.output;
         assertThat(output.getBoolean(TEST_ARGUMENT_NAME, false), is(true));
     }
@@ -151,7 +151,7 @@
         mWorkTaskExecutor.getBackgroundExecutor().execute(mWorkerWrapper);
 
         WorkSpec workSpec = mDatabase.workSpecDao().getWorkSpec(mWork.getStringId());
-        assertThat(workSpec.state, is(State.ENQUEUED));
+        assertThat(workSpec.state, is(WorkInfo.State.ENQUEUED));
     }
 
     @Test
@@ -174,7 +174,7 @@
 
         Thread.sleep(TEST_TIMEOUT_IN_MS);
         WorkSpec workSpec = mDatabase.workSpecDao().getWorkSpec(mWork.getStringId());
-        assertThat(workSpec.state, is(State.ENQUEUED));
+        assertThat(workSpec.state, is(WorkInfo.State.ENQUEUED));
     }
 
     @Test
@@ -207,7 +207,7 @@
 
         Thread.sleep(TEST_TIMEOUT_IN_MS);
         WorkSpec workSpec = mDatabase.workSpecDao().getWorkSpec(mWork.getStringId());
-        assertThat(workSpec.state, is(State.ENQUEUED));
+        assertThat(workSpec.state, is(WorkInfo.State.ENQUEUED));
     }
 
     @Test
@@ -227,11 +227,8 @@
         mWorkerWrapper.interrupt(true);
 
         assertThat(mWorker.isStopped(), is(true));
-        assertThat(mWorker.isCancelled(), is(true));
         assertThat(mWorker.getDelegate(), is(notNullValue()));
         assertThat(mWorker.getDelegate().isStopped(), is(true));
-        assertThat(mWorker.getDelegate().isCancelled(), is(true));
-
     }
 
     private void setupDelegateForExecution(@NonNull String delegateName, Executor executor) {
diff --git a/work/workmanager/src/main/java/androidx/work/Configuration.java b/work/workmanager/src/main/java/androidx/work/Configuration.java
index a31fae9..fb989f0 100644
--- a/work/workmanager/src/main/java/androidx/work/Configuration.java
+++ b/work/workmanager/src/main/java/androidx/work/Configuration.java
@@ -18,6 +18,7 @@
 
 import static androidx.work.impl.Scheduler.MAX_SCHEDULER_LIMIT;
 
+import android.content.Context;
 import android.os.Build;
 import android.support.annotation.IntRange;
 import android.support.annotation.NonNull;
@@ -31,7 +32,12 @@
 import java.util.concurrent.Executors;
 
 /**
- * Configuration for {@link WorkManager}.
+ * The Configuration object used to initialize {@link WorkManager}.  This class contains various
+ * arguments used to setup WorkManager.  For example, it is possible to customize the
+ * {@link Executor} used by {@link Worker}s here.
+ * <p>
+ * To set a custom Configuration for WorkManager, see
+ * {@link WorkManager#initialize(Context, Configuration)}.
  */
 public final class Configuration {
 
@@ -176,6 +182,11 @@
         /**
          * Specifies the range of {@link android.app.job.JobInfo} IDs that can be used by
          * {@link WorkManager}. {@link WorkManager} needs a range of at least {@code 1000} IDs.
+         * <p>
+         * JobScheduler uses integers as identifiers for jobs, and WorkManager delegates to
+         * JobScheduler on certain API levels.  In order to not clash job codes used in the rest of
+         * your app, you can use this method to tell WorkManager the valid range of job IDs that it
+         * can use.
          *
          * @param minJobSchedulerId The first valid {@link android.app.job.JobInfo} ID inclusive.
          * @param maxJobSchedulerId The last valid {@link android.app.job.JobInfo} ID inclusive.
diff --git a/work/workmanager/src/main/java/androidx/work/Data.java b/work/workmanager/src/main/java/androidx/work/Data.java
index f300833..558d69f 100644
--- a/work/workmanager/src/main/java/androidx/work/Data.java
+++ b/work/workmanager/src/main/java/androidx/work/Data.java
@@ -41,10 +41,16 @@
 
 public final class Data {
 
+    /**
+     * An empty Data object with no elements.
+     */
     public static final Data EMPTY = new Data.Builder().build();
-    public static final int MAX_DATA_BYTES = 10 * 1024;    // 10KB
 
-    private static final String TAG = "Data";
+    /**
+     * The maximum number of bytes for Data when it is serialized (converted to a byte array).
+     * Please see the class-level Javadoc for more information.
+     */
+    public static final int MAX_DATA_BYTES = 10 * 1024;    // 10KB
 
     @SuppressWarnings("WeakerAccess") /* synthetic access */
     Map<String, Object> mValues;
@@ -56,7 +62,7 @@
         mValues = new HashMap<>(other.mValues);
     }
 
-    Data(Map<String, ?> values) {
+    Data(@NonNull Map<String, ?> values) {
         mValues = new HashMap<>(values);
     }
 
@@ -282,9 +288,11 @@
     }
 
     /**
-     * @return The number of arguments
+     * @return The number of elements in this Data object.
+     * @hide
      */
     @VisibleForTesting
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     public int size() {
         return mValues.size();
     }
@@ -395,7 +403,7 @@
     }
 
     @SuppressWarnings("WeakerAccess") /* synthetic access */
-    static Boolean[] convertPrimitiveBooleanArray(boolean[] value) {
+    static @NonNull Boolean[] convertPrimitiveBooleanArray(@NonNull boolean[] value) {
         Boolean[] returnValue = new Boolean[value.length];
         for (int i = 0; i < value.length; ++i) {
             returnValue[i] = value[i];
@@ -404,7 +412,7 @@
     }
 
     @SuppressWarnings("WeakerAccess") /* synthetic access */
-    static Integer[] convertPrimitiveIntArray(int[] value) {
+    static @NonNull Integer[] convertPrimitiveIntArray(@NonNull int[] value) {
         Integer[] returnValue = new Integer[value.length];
         for (int i = 0; i < value.length; ++i) {
             returnValue[i] = value[i];
@@ -413,7 +421,7 @@
     }
 
     @SuppressWarnings("WeakerAccess") /* synthetic access */
-    static Long[] convertPrimitiveLongArray(long[] value) {
+    static @NonNull Long[] convertPrimitiveLongArray(@NonNull long[] value) {
         Long[] returnValue = new Long[value.length];
         for (int i = 0; i < value.length; ++i) {
             returnValue[i] = value[i];
@@ -422,7 +430,7 @@
     }
 
     @SuppressWarnings("WeakerAccess") /* synthetic access */
-    static Float[] convertPrimitiveFloatArray(float[] value) {
+    static @NonNull Float[] convertPrimitiveFloatArray(@NonNull float[] value) {
         Float[] returnValue = new Float[value.length];
         for (int i = 0; i < value.length; ++i) {
             returnValue[i] = value[i];
@@ -431,7 +439,7 @@
     }
 
     @SuppressWarnings("WeakerAccess") /* synthetic access */
-    static Double[] convertPrimitiveDoubleArray(double[] value) {
+    static @NonNull Double[] convertPrimitiveDoubleArray(@NonNull double[] value) {
         Double[] returnValue = new Double[value.length];
         for (int i = 0; i < value.length; ++i) {
             returnValue[i] = value[i];
@@ -465,7 +473,7 @@
          * @param value The value for this argument
          * @return The {@link Builder}
          */
-        public @NonNull Builder putBooleanArray(@NonNull String key, boolean[] value) {
+        public @NonNull Builder putBooleanArray(@NonNull String key, @NonNull boolean[] value) {
             mValues.put(key, convertPrimitiveBooleanArray(value));
             return this;
         }
@@ -489,7 +497,7 @@
          * @param value The value for this argument
          * @return The {@link Builder}
          */
-        public @NonNull Builder putIntArray(@NonNull String key, int[] value) {
+        public @NonNull Builder putIntArray(@NonNull String key, @NonNull int[] value) {
             mValues.put(key, convertPrimitiveIntArray(value));
             return this;
         }
@@ -513,7 +521,7 @@
          * @param value The value for this argument
          * @return The {@link Builder}
          */
-        public @NonNull Builder putLongArray(@NonNull String key, long[] value) {
+        public @NonNull Builder putLongArray(@NonNull String key, @NonNull long[] value) {
             mValues.put(key, convertPrimitiveLongArray(value));
             return this;
         }
@@ -537,7 +545,7 @@
          * @param value The value for this argument
          * @return The {@link Builder}
          */
-        public @NonNull Builder putFloatArray(String key, float[] value) {
+        public @NonNull Builder putFloatArray(@NonNull String key, @NonNull float[] value) {
             mValues.put(key, convertPrimitiveFloatArray(value));
             return this;
         }
@@ -561,7 +569,7 @@
          * @param value The value for this argument
          * @return The {@link Builder}
          */
-        public @NonNull Builder putDoubleArray(@NonNull String key, double[] value) {
+        public @NonNull Builder putDoubleArray(@NonNull String key, @NonNull double[] value) {
             mValues.put(key, convertPrimitiveDoubleArray(value));
             return this;
         }
@@ -573,7 +581,7 @@
          * @param value The value for this argument
          * @return The {@link Builder}
          */
-        public @NonNull Builder putString(@NonNull String key, String value) {
+        public @NonNull Builder putString(@NonNull String key, @Nullable String value) {
             mValues.put(key, value);
             return this;
         }
@@ -585,7 +593,7 @@
          * @param value The value for this argument
          * @return The {@link Builder}
          */
-        public @NonNull Builder putStringArray(@NonNull String key, String[] value) {
+        public @NonNull Builder putStringArray(@NonNull String key, @NonNull String[] value) {
             mValues.put(key, value);
             return this;
         }
diff --git a/work/workmanager/src/main/java/androidx/work/ListenableWorker.java b/work/workmanager/src/main/java/androidx/work/ListenableWorker.java
index 25108466..60d6386 100644
--- a/work/workmanager/src/main/java/androidx/work/ListenableWorker.java
+++ b/work/workmanager/src/main/java/androidx/work/ListenableWorker.java
@@ -26,6 +26,8 @@
 import android.support.annotation.RequiresApi;
 import android.support.annotation.RestrictTo;
 
+import androidx.work.impl.utils.taskexecutor.TaskExecutor;
+
 import com.google.common.util.concurrent.ListenableFuture;
 
 import java.util.List;
@@ -71,7 +73,6 @@
     private @NonNull WorkerParameters mWorkerParams;
 
     private volatile boolean mStopped;
-    private volatile boolean mCancelled;
 
     private boolean mUsed;
 
@@ -197,27 +198,12 @@
     }
 
     /**
-     * Returns {@code true} if this Worker has been told to stop and explicitly informed that it is
-     * cancelled and will never execute again.  If {@link #isStopped()} returns {@code true} but
-     * this method returns {@code false}, that means the system has decided to preempt the task.
-     * <p>
-     * Note that it is almost never sufficient to check only this method; its value is only
-     * meaningful when {@link #isStopped()} returns {@code true}.
-     *
-     * @return {@code true} if this work operation has been cancelled
-     */
-    public final boolean isCancelled() {
-        return mCancelled;
-    }
-
-    /**
      * @hide
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public final void stop(boolean cancelled) {
+    public final void stop() {
         mStopped = true;
-        mCancelled = cancelled;
-        onStopped(cancelled);
+        onStopped();
     }
 
     /**
@@ -227,10 +213,8 @@
      * processing in this method should be lightweight - there are no contractual guarantees about
      * which thread will invoke this call, so this should not be a long-running or blocking
      * operation.
-     *
-     * @param cancelled If {@code true}, the work has been explicitly cancelled
      */
-    public void onStopped(boolean cancelled) {
+    public void onStopped() {
         // Do nothing by default.
     }
 
@@ -267,6 +251,14 @@
      * @hide
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public @NonNull TaskExecutor getTaskExecutor() {
+        return mWorkerParams.getTaskExecutor();
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     public @NonNull WorkerFactory getWorkerFactory() {
         return mWorkerParams.getWorkerFactory();
     }
diff --git a/work/workmanager/src/main/java/androidx/work/Operation.java b/work/workmanager/src/main/java/androidx/work/Operation.java
new file mode 100644
index 0000000..5fc9a3a
--- /dev/null
+++ b/work/workmanager/src/main/java/androidx/work/Operation.java
@@ -0,0 +1,137 @@
+/*
+ * 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.work;
+
+import android.arch.lifecycle.LiveData;
+import android.support.annotation.NonNull;
+import android.support.annotation.RestrictTo;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+/**
+ * Information about an operation being performed by {@link WorkManager}.
+ */
+public interface Operation {
+
+    /**
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    State.SUCCESS SUCCESS = new State.SUCCESS();
+
+    /**
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    State.IN_PROGRESS IN_PROGRESS = new State.IN_PROGRESS();
+
+    /**
+     * Gets a {@link LiveData} of the Operation {@link State}.
+     *
+     * @return A {@link LiveData} of the Operation {@link State}.
+     */
+    @NonNull
+    LiveData<State> getState();
+
+    /**
+     * Gets a {@link ListenableFuture} which will only resolve with a {@link State.SUCCESS}.
+     * FAILURE {@link Operation}'s will come through as a {@link Throwable} on the
+     * {@link ListenableFuture}.
+     *
+     * <p>
+     * Call {@link ListenableFuture#get()} to block until the {@link Operation} reaches a
+     * terminal state.
+     * </p>
+     *
+     * @return a {@link ListenableFuture} with information about {@link Operation}'s
+     * {@link State.SUCCESS} state.
+     */
+    @NonNull
+    ListenableFuture<State.SUCCESS> getResult();
+
+    /**
+     * The {@link Operation} state.
+     */
+    abstract class State {
+
+        /**
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        State() {
+            // Restricting access to the constructor, to give Operation.State a sealed class
+            // like behavior.
+        }
+
+        /**
+         * This represents an {@link Operation} which is successful.
+         */
+        public static final class SUCCESS extends Operation.State {
+            private SUCCESS() {
+                super();
+            }
+
+            @Override
+            @NonNull
+            public String toString() {
+                return "SUCCESS";
+            }
+        }
+
+        /**
+         * This represents an {@link Operation} which is in progress.
+         */
+        public static final class IN_PROGRESS extends Operation.State {
+            private IN_PROGRESS() {
+                super();
+            }
+
+            @Override
+            @NonNull
+            public String toString() {
+                return "IN_PROGRESS";
+            }
+        }
+
+        /**
+         * This represents an {@link Operation} which has failed.
+         */
+        public static final class FAILURE extends Operation.State {
+
+            private final Throwable mThrowable;
+
+            public FAILURE(@NonNull Throwable exception) {
+                super();
+                mThrowable = exception;
+            }
+
+            /**
+             * @return The {@link Throwable} which caused the {@link Operation} to fail.
+             */
+            @NonNull
+            public Throwable getException() {
+                return mThrowable;
+            }
+
+            @Override
+            @NonNull
+            public String toString() {
+                return String.format("FAILURE (%s)", mThrowable.getMessage());
+            }
+        }
+    }
+}
diff --git a/work/workmanager/src/main/java/androidx/work/State.java b/work/workmanager/src/main/java/androidx/work/State.java
deleted file mode 100644
index 3541fac..0000000
--- a/work/workmanager/src/main/java/androidx/work/State.java
+++ /dev/null
@@ -1,63 +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.work;
-
-/**
- * The current state of a unit of work.
- */
-public enum State {
-
-    /**
-     * The state for work that is enqueued (hasn't completed and isn't running)
-     */
-    ENQUEUED,
-
-    /**
-     * The state for work that is currently being executed
-     */
-    RUNNING,
-
-    /**
-     * The state for work that has completed successfully
-     */
-    SUCCEEDED,
-
-    /**
-     * The state for work that has completed in a failure state
-     */
-    FAILED,
-
-    /**
-     * The state for work that is currently blocked because its prerequisites haven't finished
-     * successfully
-     */
-    BLOCKED,
-
-    /**
-     * The state for work that has been cancelled and will not execute
-     */
-    CANCELLED;
-
-    /**
-     * Returns {@code true} if this State is considered finished.
-     *
-     * @return {@code true} for {@link #SUCCEEDED}, {@link #FAILED}, and {@link #CANCELLED} states
-     */
-    public boolean isFinished() {
-        return (this == SUCCEEDED || this == FAILED || this == CANCELLED);
-    }
-}
diff --git a/work/workmanager/src/main/java/androidx/work/WorkContinuation.java b/work/workmanager/src/main/java/androidx/work/WorkContinuation.java
index d127d0b..052e411 100644
--- a/work/workmanager/src/main/java/androidx/work/WorkContinuation.java
+++ b/work/workmanager/src/main/java/androidx/work/WorkContinuation.java
@@ -54,29 +54,29 @@
     public abstract @NonNull WorkContinuation then(@NonNull List<OneTimeWorkRequest> work);
 
     /**
-     * Returns a {@link LiveData} list of {@link WorkStatus} that provides information about work,
+     * Returns a {@link LiveData} list of {@link WorkInfo} that provides information about work,
      * their progress, and any resulting output.  If state or outputs of any of the jobs in this
      * chain changes, any attached {@link android.arch.lifecycle.Observer}s will trigger.
      *
-     * @return A {@link LiveData} containing a list of {@link WorkStatus}es
+     * @return A {@link LiveData} containing a list of {@link WorkInfo}es
      */
-    public abstract @NonNull LiveData<List<WorkStatus>> getStatusesLiveData();
+    public abstract @NonNull LiveData<List<WorkInfo>> getWorkInfosLiveData();
 
     /**
-     * Returns a {@link ListenableFuture} of a {@link List} of {@link WorkStatus} that provides
+     * Returns a {@link ListenableFuture} of a {@link List} of {@link WorkInfo} that provides
      * information about work, their progress, and any resulting output in the
      * {@link WorkContinuation}.
      *
-     * @return A {@link  ListenableFuture} of a {@link List} of {@link WorkStatus}es
+     * @return A {@link  ListenableFuture} of a {@link List} of {@link WorkInfo}es
      */
-    public abstract @NonNull ListenableFuture<List<WorkStatus>> getStatuses();
+    public abstract @NonNull ListenableFuture<List<WorkInfo>> getWorkInfos();
 
     /**
      * Enqueues the instance of {@link WorkContinuation} on the background thread.
      *
-     * @return A {@link ListenableFuture} that completes when the enqueue operation is completed
+     * @return An {@link Operation} that can be used to determine when the enqueue has completed
      */
-    public abstract @NonNull ListenableFuture<Void> enqueue();
+    public abstract @NonNull Operation enqueue();
 
     /**
      * Combines multiple {@link WorkContinuation}s to allow for complex chaining.
diff --git a/work/workmanager/src/main/java/androidx/work/WorkStatus.java b/work/workmanager/src/main/java/androidx/work/WorkInfo.java
similarity index 69%
rename from work/workmanager/src/main/java/androidx/work/WorkStatus.java
rename to work/workmanager/src/main/java/androidx/work/WorkInfo.java
index 66870e3..43a0570 100644
--- a/work/workmanager/src/main/java/androidx/work/WorkStatus.java
+++ b/work/workmanager/src/main/java/androidx/work/WorkInfo.java
@@ -30,7 +30,7 @@
  * {@link State#FAILED}).
  */
 
-public final class WorkStatus {
+public final class WorkInfo {
 
     private @NonNull UUID mId;
     private @NonNull State mState;
@@ -41,7 +41,7 @@
      * @hide
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public WorkStatus(
+    public WorkInfo(
             @NonNull UUID id,
             @NonNull State state,
             @NonNull Data outputData,
@@ -73,7 +73,7 @@
         if (this == o) return true;
         if (o == null || getClass() != o.getClass()) return false;
 
-        WorkStatus that = (WorkStatus) o;
+        WorkInfo that = (WorkInfo) o;
 
         if (mId != null ? !mId.equals(that.mId) : that.mId != null) return false;
         if (mState != that.mState) return false;
@@ -95,11 +95,58 @@
 
     @Override
     public String toString() {
-        return "WorkStatus{"
+        return "WorkInfo{"
                 +   "mId='" + mId + '\''
                 +   ", mState=" + mState
                 +   ", mOutputData=" + mOutputData
                 +   ", mTags=" + mTags
                 + '}';
     }
+
+    /**
+     * The current state of a unit of work.
+     */
+    public enum State {
+
+        /**
+         * The state for work that is enqueued (hasn't completed and isn't running)
+         */
+        ENQUEUED,
+
+        /**
+         * The state for work that is currently being executed
+         */
+        RUNNING,
+
+        /**
+         * The state for work that has completed successfully
+         */
+        SUCCEEDED,
+
+        /**
+         * The state for work that has completed in a failure state
+         */
+        FAILED,
+
+        /**
+         * The state for work that is currently blocked because its prerequisites haven't finished
+         * successfully
+         */
+        BLOCKED,
+
+        /**
+         * The state for work that has been cancelled and will not execute
+         */
+        CANCELLED;
+
+        /**
+         * Returns {@code true} if this State is considered finished.
+         *
+         * @return {@code true} for {@link #SUCCEEDED}, {@link #FAILED}, and
+         * {@link #CANCELLED} states
+         */
+        public boolean isFinished() {
+            return (this == SUCCEEDED || this == FAILED || this == CANCELLED);
+        }
+    }
 }
diff --git a/work/workmanager/src/main/java/androidx/work/WorkManager.java b/work/workmanager/src/main/java/androidx/work/WorkManager.java
index 6a335a7..a21eae0 100644
--- a/work/workmanager/src/main/java/androidx/work/WorkManager.java
+++ b/work/workmanager/src/main/java/androidx/work/WorkManager.java
@@ -64,7 +64,7 @@
  * {@code
  * WorkRequest request = new OneTimeWorkRequest.Builder(FooWorker.class).build();
  * workManager.enqueue(request);
- * LiveData<WorkStatus> status = workManager.getStatusByIdLiveData(request.getId());
+ * LiveData<WorkInfo> status = workManager.getWorkInfoByIdLiveData(request.getId());
  * status.observe(...);}</pre>
  *
  * You can also use the id for cancellation:
@@ -114,6 +114,12 @@
  * (see {@link WorkRequest.Builder#addTag(String)}), and chains of work can be given a
  * uniquely-identifiable name (see
  * {@link #beginUniqueWork(String, ExistingWorkPolicy, OneTimeWorkRequest...)}).
+ *
+ * <p>
+ * <b>Manually initializing WorkManager</b>
+ * <p>
+ * You can manually initialize WorkManager and provide a custom {@link Configuration} for it.
+ * Please see {@link #initialize(Context, Configuration)}.
  */
 public abstract class WorkManager {
 
@@ -144,11 +150,13 @@
      * {@link Configuration}.  By default, this method should not be called because WorkManager is
      * automatically initialized.  To initialize WorkManager yourself, please follow these steps:
      * <p><ul>
-     * <li>Disable {@code androidx.work.impl.WorkManagerInitializer} in your manifest
-     * <li>In {@code Application#onCreate} or a {@code ContentProvider}, call this method before
-     * calling {@link WorkManager#getInstance()}
+     * <li>Disable {@code androidx.work.impl.WorkManagerInitializer} in your manifest.
+     * <li>Invoke this method in {@code Application#onCreate} or a {@code ContentProvider}. Note
+     * that this method <b>must</b> be invoked in one of these two places or you risk getting a
+     * {@code NullPointerException} in {@link #getInstance()}.
      * </ul></p>
-     * This method has no effect if WorkManager is already initialized.
+     * <p>
+     * This method throws an exception if it is called multiple times.
      *
      * @param context A {@link Context} object for configuration purposes. Internally, this class
      *                will call {@link Context#getApplicationContext()}, so you may safely pass in
@@ -163,33 +171,21 @@
      * Enqueues one or more items for background processing.
      *
      * @param workRequest One or more {@link WorkRequest} to enqueue
+     * @return An {@link Operation} that can be used to determine when the enqueue has completed
      */
-    @SuppressWarnings("FutureReturnValueIgnored")
-    public final void enqueue(@NonNull WorkRequest workRequest) {
-        enqueueInternal(Collections.singletonList(workRequest));
-    }
-
-    /**
-     * Enqueues one or more items for background processing.
-     *
-     * @param requests One or more {@link WorkRequest} to enqueue
-     */
-    @SuppressWarnings("FutureReturnValueIgnored")
-    public void enqueue(@NonNull List<? extends WorkRequest> requests) {
-        enqueueInternal(requests);
-    }
-
-    /**
-     * Enqueues one or more items for background processing.
-     *
-     * @param requests One or more {@link WorkRequest} to enqueue
-     * @return A {@link ListenableFuture} that completes when the enqueue operation is completed
-     * @hide
-     */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     @NonNull
-    public abstract ListenableFuture<Void> enqueueInternal(
-            @NonNull List<? extends WorkRequest> requests);
+    public final Operation enqueue(@NonNull WorkRequest workRequest) {
+        return enqueue(Collections.singletonList(workRequest));
+    }
+
+    /**
+     * Enqueues one or more items for background processing.
+     *
+     * @param requests One or more {@link WorkRequest} to enqueue
+     * @return An {@link Operation} that can be used to determine when the enqueue has completed
+     */
+    @NonNull
+    public abstract Operation enqueue(@NonNull List<? extends WorkRequest> requests);
 
     /**
      * Begins a chain with one or more {@link OneTimeWorkRequest}s, which can be enqueued together
@@ -290,13 +286,14 @@
      *                     new OneTimeWorkRequests only if there is no pending work labelled with
      *                     {@code uniqueWorkName}. {@code APPEND} will append the
      *                     OneTimeWorkRequests as leaf nodes labelled with {@code uniqueWorkName}.
+     * @return An {@link Operation} that can be used to determine when the enqueue has completed
      */
-    @SuppressWarnings("FutureReturnValueIgnored")
-    public void enqueueUniqueWork(
+    @NonNull
+    public Operation enqueueUniqueWork(
             @NonNull String uniqueWorkName,
             @NonNull ExistingWorkPolicy existingWorkPolicy,
             @NonNull OneTimeWorkRequest...work) {
-        enqueueUniqueWorkInternal(uniqueWorkName, existingWorkPolicy, Arrays.asList(work));
+        return enqueueUniqueWork(uniqueWorkName, existingWorkPolicy, Arrays.asList(work));
     }
 
     /**
@@ -317,39 +314,10 @@
      *                     new OneTimeWorkRequests only if there is no pending work labelled with
      *                     {@code uniqueWorkName}. {@code APPEND} will append the
      *                     OneTimeWorkRequests as leaf nodes labelled with {@code uniqueWorkName}.
+     * @return An {@link Operation} that can be used to determine when the enqueue has completed
      */
-    @SuppressWarnings("FutureReturnValueIgnored")
-    public void enqueueUniqueWork(
-            @NonNull String uniqueWorkName,
-            @NonNull ExistingWorkPolicy existingWorkPolicy,
-            @NonNull List<OneTimeWorkRequest> work) {
-        enqueueUniqueWorkInternal(uniqueWorkName, existingWorkPolicy, work);
-    }
-
-    /**
-     * This method allows you to enqueue {@code work} requests to a uniquely-named
-     * {@link WorkContinuation}, where only one continuation of a particular name can be active at
-     * a time. For example, you may only want one sync operation to be active. If there is one
-     * pending, you can choose to let it run or replace it with your new work.
-     *
-     * <p>
-     * The {@code uniqueWorkName} uniquely identifies this {@link WorkContinuation}.
-     * </p>
-     *
-     * @param uniqueWorkName A unique name which for this operation
-     * @param existingWorkPolicy An {@link ExistingWorkPolicy}
-     * @param work {@link OneTimeWorkRequest}s to enqueue. {@code REPLACE} ensures
-     *                     that if there is pending work labelled with {@code uniqueWorkName}, it
-     *                     will be cancelled and the new work will run. {@code KEEP} will run the
-     *                     new OneTimeWorkRequests only if there is no pending work labelled with
-     *                     {@code uniqueWorkName}. {@code APPEND} will append the
-     *                     OneTimeWorkRequests as leaf nodes labelled with {@code uniqueWorkName}.
-     * @return A {@link ListenableFuture} that completes when the enqueue operation is completed
-     * @hide
-     */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     @NonNull
-    public abstract ListenableFuture<Void> enqueueUniqueWorkInternal(
+    public abstract Operation enqueueUniqueWork(
             @NonNull String uniqueWorkName,
             @NonNull ExistingWorkPolicy existingWorkPolicy,
             @NonNull List<OneTimeWorkRequest> work);
@@ -371,38 +339,10 @@
      *                     cancelled and the new work will run. {@code KEEP} will run the new
      *                     PeriodicWorkRequest only if there is no pending work labelled with
      *                     {@code uniqueWorkName}.
+     * @return An {@link Operation} that can be used to determine when the enqueue has completed
      */
-    @SuppressWarnings("FutureReturnValueIgnored")
-    public void enqueueUniquePeriodicWork(
-            @NonNull String uniqueWorkName,
-            @NonNull ExistingPeriodicWorkPolicy existingPeriodicWorkPolicy,
-            @NonNull PeriodicWorkRequest periodicWork) {
-        enqueueUniquePeriodicWorkInternal(uniqueWorkName, existingPeriodicWorkPolicy, periodicWork);
-    }
-
-    /**
-     * This method allows you to enqueue a uniquely-named {@link PeriodicWorkRequest}, where only
-     * one PeriodicWorkRequest of a particular name can be active at a time.  For example, you may
-     * only want one sync operation to be active.  If there is one pending, you can choose to let it
-     * run or replace it with your new work.
-     *
-     * <p>
-     * The {@code uniqueWorkName} uniquely identifies this PeriodicWorkRequest.
-     * </p>
-     *
-     * @param uniqueWorkName A unique name which for this operation
-     * @param existingPeriodicWorkPolicy An {@link ExistingPeriodicWorkPolicy}
-     * @param periodicWork A {@link PeriodicWorkRequest} to enqueue. {@code REPLACE} ensures that if
-     *                     there is pending work labelled with {@code uniqueWorkName}, it will be
-     *                     cancelled and the new work will run. {@code KEEP} will run the new
-     *                     PeriodicWorkRequest only if there is no pending work labelled with
-     *                     {@code uniqueWorkName}.
-     * @return A {@link ListenableFuture} that completes when the enqueue operation is completed
-     * @hide
-     */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     @NonNull
-    public abstract ListenableFuture<Void> enqueueUniquePeriodicWorkInternal(
+    public abstract Operation enqueueUniquePeriodicWork(
             @NonNull String uniqueWorkName,
             @NonNull ExistingPeriodicWorkPolicy existingPeriodicWorkPolicy,
             @NonNull PeriodicWorkRequest periodicWork);
@@ -412,126 +352,56 @@
      * policy and work that is already executing may continue to run.
      *
      * @param id The id of the work
-     */
-    @SuppressWarnings("FutureReturnValueIgnored")
-    public void cancelWorkById(@NonNull UUID id) {
-        cancelWorkByIdInternal(id);
-    }
-
-    /**
-     * Cancels work with the given id if it isn't finished.  Note that cancellation is a best-effort
-     * policy and work that is already executing may continue to run.
-     *
-     * @param id The id of the work
-     * @return A {@link ListenableFuture} that completes when the cancelWorkById operation is
+     * @return An {@link Operation} that can be used to determine when the cancelWorkById has
      * completed
-     * @hide
      */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public abstract @NonNull ListenableFuture<Void> cancelWorkByIdInternal(@NonNull UUID id);
+    public abstract @NonNull Operation cancelWorkById(@NonNull UUID id);
 
     /**
      * Cancels all unfinished work with the given tag.  Note that cancellation is a best-effort
      * policy and work that is already executing may continue to run.
      *
      * @param tag The tag used to identify the work
-     */
-    @SuppressWarnings("FutureReturnValueIgnored")
-    public void cancelAllWorkByTag(@NonNull String tag) {
-        cancelAllWorkByTagInternal(tag);
-    }
-
-    /**
-     * Cancels all unfinished work with the given tag.  Note that cancellation is a best-effort
-     * policy and work that is already executing may continue to run.
-     *
-     * @param tag The tag used to identify the work
-     * @return A {@link ListenableFuture} that completes when the cancelAllWorkByTag operation is
+     * @return An {@link Operation} that can be used to determine when the cancelAllWorkByTag has
      * completed
-     * @hide
      */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public abstract @NonNull ListenableFuture<Void> cancelAllWorkByTagInternal(@NonNull String tag);
-
-    /**
-     * Cancels all unfinished work in the work chain with the given name.  Note that cancellation is
-     * a best-effort policy and work that is already executing may continue to run.
-     *
-     * @param uniqueWorkName The unique name used to identify the chain of wor
-     */
-    @SuppressWarnings("FutureReturnValueIgnored")
-    public void cancelUniqueWork(@NonNull String uniqueWorkName) {
-        cancelUniqueWorkInternal(uniqueWorkName);
-    }
+    public abstract @NonNull Operation cancelAllWorkByTag(@NonNull String tag);
 
     /**
      * Cancels all unfinished work in the work chain with the given name.  Note that cancellation is
      * a best-effort policy and work that is already executing may continue to run.
      *
      * @param uniqueWorkName The unique name used to identify the chain of work
-     * @return A {@link ListenableFuture} that completes when the cancelUniqueWork operation is
+     * @return An {@link Operation} that can be used to determine when the cancelUniqueWork has
      * completed
-     * @hide
      */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public abstract @NonNull
-            ListenableFuture<Void> cancelUniqueWorkInternal(@NonNull String uniqueWorkName);
-
-    /**
-     * Cancels all unfinished work.  <b>Use this method with extreme caution!</b>  By invoking it,
-     * you will potentially affect other modules or libraries in your codebase.  It is strongly
-     * recommended that you use one of the other cancellation methods at your disposal.
-     */
-    @SuppressWarnings("FutureReturnValueIgnored")
-    public void cancelAllWork() {
-        cancelAllWorkInternal();
-    }
+    public abstract @NonNull Operation cancelUniqueWork(@NonNull String uniqueWorkName);
 
     /**
      * Cancels all unfinished work.  <b>Use this method with extreme caution!</b>  By invoking it,
      * you will potentially affect other modules or libraries in your codebase.  It is strongly
      * recommended that you use one of the other cancellation methods at your disposal.
      *
-     * @return A {@link ListenableFuture} that completes when the cancelAllWork operation is
+     * @return An {@link Operation} that can be used to determine when the cancelAllWork has
      * completed
-     * @hide
      */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public abstract @NonNull ListenableFuture<Void> cancelAllWorkInternal();
+    public abstract @NonNull Operation cancelAllWork();
 
     /**
      * Prunes all eligible finished work from the internal database.  Eligible work must be finished
-     * ({@link State#SUCCEEDED}, {@link State#FAILED}, or {@link State#CANCELLED}), with zero
-     * unfinished dependents.
+     * ({@link WorkInfo.State#SUCCEEDED}, {@link WorkInfo.State#FAILED}, or
+     * {@link WorkInfo.State#CANCELLED}), with zero unfinished dependents.
      * <p>
      * <b>Use this method with caution</b>; by invoking it, you (and any modules and libraries in
-     * your codebase) will no longer be able to observe the {@link WorkStatus} of the pruned work.
-     * You do not normally need to call this method - WorkManager takes care to auto-prune its work
-     * after a sane period of time.  This method also ignores the
-     * {@link OneTimeWorkRequest.Builder#keepResultsForAtLeast(long, TimeUnit)} policy.
-     */
-    @SuppressWarnings("FutureReturnValueIgnored")
-    public void pruneWork() {
-        pruneWorkInternal();
-    }
-
-    /**
-     * Prunes all eligible finished work from the internal database.  Eligible work must be finished
-     * ({@link State#SUCCEEDED}, {@link State#FAILED}, or {@link State#CANCELLED}), with zero
-     * unfinished dependents.
-     * <p>
-     * <b>Use this method with caution</b>; by invoking it, you (and any modules and libraries in
-     * your codebase) will no longer be able to observe the {@link WorkStatus} of the pruned work.
+     * your codebase) will no longer be able to observe the {@link WorkInfo} of the pruned work.
      * You do not normally need to call this method - WorkManager takes care to auto-prune its work
      * after a sane period of time.  This method also ignores the
      * {@link OneTimeWorkRequest.Builder#keepResultsForAtLeast(long, TimeUnit)} policy.
      *
-     * @return A {@link ListenableFuture} that completes when the pruneWork operation is
+     * @return An {@link Operation} that can be used to determine when the pruneWork has
      * completed
-     * @hide
      */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public abstract @NonNull ListenableFuture<Void> pruneWorkInternal();
+    public abstract @NonNull Operation pruneWork();
 
     /**
      * Gets a {@link LiveData} of the last time all work was cancelled.  This method is intended for
@@ -555,64 +425,64 @@
     public abstract @NonNull ListenableFuture<Long> getLastCancelAllTimeMillis();
 
     /**
-     * Gets a {@link LiveData} of the {@link WorkStatus} for a given work id.
+     * Gets a {@link LiveData} of the {@link WorkInfo} for a given work id.
      *
      * @param id The id of the work
-     * @return A {@link LiveData} of the {@link WorkStatus} associated with {@code id}; note that
-     *         this {@link WorkStatus} may be {@code null} if {@code id} is not known to
+     * @return A {@link LiveData} of the {@link WorkInfo} associated with {@code id}; note that
+     *         this {@link WorkInfo} may be {@code null} if {@code id} is not known to
      *         WorkManager.
      */
-    public abstract @NonNull LiveData<WorkStatus> getStatusByIdLiveData(@NonNull UUID id);
+    public abstract @NonNull LiveData<WorkInfo> getWorkInfoByIdLiveData(@NonNull UUID id);
 
     /**
-     * Gets a {@link ListenableFuture} of the {@link WorkStatus} for a given work id.
+     * Gets a {@link ListenableFuture} of the {@link WorkInfo} for a given work id.
      *
      * @param id The id of the work
-     * @return A {@link ListenableFuture} of the {@link WorkStatus} associated with {@code id};
-     * note that this {@link WorkStatus} may be {@code null} if {@code id} is not known to
+     * @return A {@link ListenableFuture} of the {@link WorkInfo} associated with {@code id};
+     * note that this {@link WorkInfo} may be {@code null} if {@code id} is not known to
      * WorkManager
      */
-    public abstract @NonNull ListenableFuture<WorkStatus> getStatusById(@NonNull UUID id);
+    public abstract @NonNull ListenableFuture<WorkInfo> getWorkInfoById(@NonNull UUID id);
 
     /**
-     * Gets a {@link LiveData} of the {@link WorkStatus} for all work for a given tag.
+     * Gets a {@link LiveData} of the {@link WorkInfo} for all work for a given tag.
      *
      * @param tag The tag of the work
-     * @return A {@link LiveData} list of {@link WorkStatus} for work tagged with {@code tag}
+     * @return A {@link LiveData} list of {@link WorkInfo} for work tagged with {@code tag}
      */
-    public abstract @NonNull LiveData<List<WorkStatus>> getStatusesByTagLiveData(
+    public abstract @NonNull LiveData<List<WorkInfo>> getWorkInfosByTagLiveData(
             @NonNull String tag);
 
     /**
-     * Gets a {@link ListenableFuture} of the {@link WorkStatus} for all work for a given tag.
+     * Gets a {@link ListenableFuture} of the {@link WorkInfo} for all work for a given tag.
      *
      * @param tag The tag of the work
-     * @return A {@link ListenableFuture} list of {@link WorkStatus} for work tagged with
+     * @return A {@link ListenableFuture} list of {@link WorkInfo} for work tagged with
      * {@code tag}
      */
-    public abstract @NonNull ListenableFuture<List<WorkStatus>> getStatusesByTag(
+    public abstract @NonNull ListenableFuture<List<WorkInfo>> getWorkInfosByTag(
             @NonNull String tag);
 
     /**
-     * Gets a {@link LiveData} of the {@link WorkStatus} for all work in a work chain with a given
+     * Gets a {@link LiveData} of the {@link WorkInfo} for all work in a work chain with a given
      * unique name.
      *
      * @param uniqueWorkName The unique name used to identify the chain of work
-     * @return A {@link LiveData} of the {@link WorkStatus} for work in the chain named
+     * @return A {@link LiveData} of the {@link WorkInfo} for work in the chain named
      *         {@code uniqueWorkName}
      */
-    public abstract @NonNull LiveData<List<WorkStatus>> getStatusesForUniqueWorkLiveData(
+    public abstract @NonNull LiveData<List<WorkInfo>> getWorkInfosForUniqueWorkLiveData(
             @NonNull String uniqueWorkName);
 
     /**
-     * Gets a {@link ListenableFuture} of the {@link WorkStatus} for all work in a work chain
+     * Gets a {@link ListenableFuture} of the {@link WorkInfo} for all work in a work chain
      * with a given unique name.
      *
      * @param uniqueWorkName The unique name used to identify the chain of work
-     * @return A {@link ListenableFuture} of the {@link WorkStatus} for work in the chain named
+     * @return A {@link ListenableFuture} of the {@link WorkInfo} for work in the chain named
      *         {@code uniqueWorkName}
      */
-    public abstract @NonNull ListenableFuture<List<WorkStatus>> getStatusesForUniqueWork(
+    public abstract @NonNull ListenableFuture<List<WorkInfo>> getWorkInfosForUniqueWork(
             @NonNull String uniqueWorkName);
 
     /**
diff --git a/work/workmanager/src/main/java/androidx/work/WorkRequest.java b/work/workmanager/src/main/java/androidx/work/WorkRequest.java
index 7e5b7eb..b17385f 100644
--- a/work/workmanager/src/main/java/androidx/work/WorkRequest.java
+++ b/work/workmanager/src/main/java/androidx/work/WorkRequest.java
@@ -185,7 +185,7 @@
          * WorkManager when there are no pending dependent jobs.
          *
          * When the results of a work are pruned, it becomes impossible to query for its
-         * {@link WorkStatus}.
+         * {@link WorkInfo}.
          *
          * Specifying a long duration here may adversely affect performance in terms of app storage
          * and database query time.
@@ -206,7 +206,7 @@
          * WorkManager when there are no pending dependent jobs.
          *
          * When the results of a work are pruned, it becomes impossible to query for its
-         * {@link WorkStatus}.
+         * {@link WorkInfo}.
          *
          * Specifying a long duration here may adversely affect performance in terms of app storage
          * and database query time.
@@ -241,13 +241,13 @@
         /**
          * Set the initial state for this work.  Used in testing only.
          *
-         * @param state The {@link State} to set
+         * @param state The {@link WorkInfo.State} to set
          * @return The current {@link Builder}
          * @hide
          */
         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
         @VisibleForTesting
-        public final @NonNull B setInitialState(@NonNull State state) {
+        public final @NonNull B setInitialState(@NonNull WorkInfo.State state) {
             mWorkSpec.state = state;
             return getThis();
         }
diff --git a/work/workmanager/src/main/java/androidx/work/impl/OperationImpl.java b/work/workmanager/src/main/java/androidx/work/impl/OperationImpl.java
new file mode 100644
index 0000000..270f3bb
--- /dev/null
+++ b/work/workmanager/src/main/java/androidx/work/impl/OperationImpl.java
@@ -0,0 +1,73 @@
+/*
+ * 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.work.impl;
+
+import android.arch.lifecycle.LiveData;
+import android.arch.lifecycle.MutableLiveData;
+import android.support.annotation.NonNull;
+import android.support.annotation.RestrictTo;
+
+import androidx.work.Operation;
+import androidx.work.impl.utils.futures.SettableFuture;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+/**
+ * A concrete implementation of a {@link Operation}.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class OperationImpl implements Operation {
+
+    private final MutableLiveData<State> mOperationState;
+    private final SettableFuture<State.SUCCESS> mOperationFuture;
+
+    public OperationImpl() {
+        mOperationState = new MutableLiveData<>();
+        mOperationFuture = SettableFuture.create();
+        // Mark the operation as in progress.
+        setState(Operation.IN_PROGRESS);
+    }
+
+    @Override
+    public @NonNull ListenableFuture<State.SUCCESS> getResult() {
+        return mOperationFuture;
+    }
+
+    @Override
+    public @NonNull LiveData<State> getState() {
+        return mOperationState;
+    }
+
+    /**
+     * Updates the {@link androidx.work.Operation.State} of the {@link Operation}.
+     *
+     * @param state The current {@link Operation.State}
+     */
+    public void setState(@NonNull State state) {
+        mOperationState.postValue(state);
+
+        // Only terminal state get updates to the future.
+        if (state instanceof State.SUCCESS) {
+            mOperationFuture.set((State.SUCCESS) state);
+        } else if (state instanceof State.FAILURE) {
+            State.FAILURE failed = (State.FAILURE) state;
+            mOperationFuture.setException(failed.getException());
+        }
+    }
+}
diff --git a/work/workmanager/src/main/java/androidx/work/impl/WorkContinuationImpl.java b/work/workmanager/src/main/java/androidx/work/impl/WorkContinuationImpl.java
index d3137e3..f121d46 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/WorkContinuationImpl.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/WorkContinuationImpl.java
@@ -26,9 +26,10 @@
 import androidx.work.ExistingWorkPolicy;
 import androidx.work.Logger;
 import androidx.work.OneTimeWorkRequest;
+import androidx.work.Operation;
 import androidx.work.WorkContinuation;
+import androidx.work.WorkInfo;
 import androidx.work.WorkRequest;
-import androidx.work.WorkStatus;
 import androidx.work.impl.utils.EnqueueRunnable;
 import androidx.work.impl.utils.StatusRunnable;
 import androidx.work.impl.workers.CombineContinuationsWorker;
@@ -60,7 +61,7 @@
     private final List<WorkContinuationImpl> mParents;
 
     private boolean mEnqueued;
-    private ListenableFuture<Void> mFuture;
+    private Operation mOperation;
 
     @NonNull
     public WorkManagerImpl getWorkManagerImpl() {
@@ -160,14 +161,14 @@
     }
 
     @Override
-    public @NonNull LiveData<List<WorkStatus>> getStatusesLiveData() {
-        return mWorkManagerImpl.getStatusesById(mAllIds);
+    public @NonNull LiveData<List<WorkInfo>> getWorkInfosLiveData() {
+        return mWorkManagerImpl.getWorkInfosById(mAllIds);
     }
 
     @NonNull
     @Override
-    public ListenableFuture<List<WorkStatus>> getStatuses() {
-        StatusRunnable<List<WorkStatus>> runnable =
+    public ListenableFuture<List<WorkInfo>> getWorkInfos() {
+        StatusRunnable<List<WorkInfo>> runnable =
                 StatusRunnable.forStringIds(mWorkManagerImpl, mAllIds);
 
         mWorkManagerImpl.getWorkTaskExecutor().executeOnBackgroundThread(runnable);
@@ -175,19 +176,19 @@
     }
 
     @Override
-    public ListenableFuture<Void> enqueue() {
+    public @NonNull Operation enqueue() {
         // Only enqueue if not already enqueued.
         if (!mEnqueued) {
             // The runnable walks the hierarchy of the continuations
             // and marks them enqueued using the markEnqueued() method, parent first.
             EnqueueRunnable runnable = new EnqueueRunnable(this);
             mWorkManagerImpl.getWorkTaskExecutor().executeOnBackgroundThread(runnable);
-            mFuture = runnable.getFuture();
+            mOperation = runnable.getOperation();
         } else {
             Logger.warning(TAG,
                     String.format("Already enqueued work ids (%s)", TextUtils.join(", ", mIds)));
         }
-        return mFuture;
+        return mOperation;
     }
 
     @Override
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 f5b7b97..e585285 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/WorkManagerImpl.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/WorkManagerImpl.java
@@ -31,12 +31,13 @@
 import androidx.work.ExistingWorkPolicy;
 import androidx.work.Logger;
 import androidx.work.OneTimeWorkRequest;
+import androidx.work.Operation;
 import androidx.work.PeriodicWorkRequest;
 import androidx.work.R;
 import androidx.work.WorkContinuation;
+import androidx.work.WorkInfo;
 import androidx.work.WorkManager;
 import androidx.work.WorkRequest;
-import androidx.work.WorkStatus;
 import androidx.work.WorkerParameters;
 import androidx.work.impl.background.greedy.GreedyScheduler;
 import androidx.work.impl.background.systemjob.SystemJobScheduler;
@@ -117,7 +118,9 @@
     }
 
     /**
-     * Initializes the singleton instance of {@link WorkManagerImpl}.
+     * Initializes the singleton instance of {@link WorkManagerImpl}.  You should only do this if
+     * you want to use a custom {@link Configuration} object and have disabled
+     * WorkManagerInitializer.
      *
      * @param context A {@link Context} object for configuration purposes. Internally, this class
      *                will call {@link Context#getApplicationContext()}, so you may safely pass in
@@ -129,6 +132,14 @@
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     public static void initialize(@NonNull Context context, @NonNull Configuration configuration) {
         synchronized (sLock) {
+            if (sDelegatedInstance != null && sDefaultInstance != null) {
+                throw new IllegalStateException("WorkManager is already initialized.  Did you "
+                        + "try to initialize it manually without disabling "
+                        + "WorkManagerInitializer? See "
+                        + "WorkManager#initialize(Context, Configuration) or the class level"
+                        + "Javadoc for more information.");
+            }
+
             if (sDelegatedInstance == null) {
                 context = context.getApplicationContext();
                 if (sDefaultInstance == null) {
@@ -280,10 +291,10 @@
 
     @Override
     @NonNull
-    public ListenableFuture<Void> enqueueInternal(
+    public Operation enqueue(
             @NonNull List<? extends WorkRequest> workRequests) {
 
-        // This error is not being propagated as part of the ListenableFuture, as we want the
+        // This error is not being propagated as part of the Operation, as we want the
         // app to crash during development. Having no workRequests is always a developer error.
         if (workRequests.isEmpty()) {
             throw new IllegalArgumentException(
@@ -315,7 +326,7 @@
 
     @NonNull
     @Override
-    public ListenableFuture<Void> enqueueUniqueWorkInternal(@NonNull String uniqueWorkName,
+    public Operation enqueueUniqueWork(@NonNull String uniqueWorkName,
             @NonNull ExistingWorkPolicy existingWorkPolicy,
             @NonNull List<OneTimeWorkRequest> work) {
         return new WorkContinuationImpl(this, uniqueWorkName, existingWorkPolicy, work).enqueue();
@@ -323,7 +334,7 @@
 
     @Override
     @NonNull
-    public ListenableFuture<Void> enqueueUniquePeriodicWorkInternal(
+    public Operation enqueueUniquePeriodicWork(
             @NonNull String uniqueWorkName,
             @NonNull ExistingPeriodicWorkPolicy existingPeriodicWorkPolicy,
             @NonNull PeriodicWorkRequest periodicWork) {
@@ -353,32 +364,32 @@
     }
 
     @Override
-    public @NonNull ListenableFuture<Void> cancelWorkByIdInternal(@NonNull UUID id) {
+    public @NonNull Operation cancelWorkById(@NonNull UUID id) {
         CancelWorkRunnable runnable = CancelWorkRunnable.forId(id, this);
         mWorkTaskExecutor.executeOnBackgroundThread(runnable);
-        return runnable.getFuture();
+        return runnable.getOperation();
     }
 
     @Override
-    public @NonNull ListenableFuture<Void> cancelAllWorkByTagInternal(@NonNull final String tag) {
+    public @NonNull Operation cancelAllWorkByTag(@NonNull final String tag) {
         CancelWorkRunnable runnable = CancelWorkRunnable.forTag(tag, this);
         mWorkTaskExecutor.executeOnBackgroundThread(runnable);
-        return runnable.getFuture();
+        return runnable.getOperation();
     }
 
     @Override
     @NonNull
-    public ListenableFuture<Void> cancelUniqueWorkInternal(@NonNull String uniqueWorkName) {
+    public Operation cancelUniqueWork(@NonNull String uniqueWorkName) {
         CancelWorkRunnable runnable = CancelWorkRunnable.forName(uniqueWorkName, this, true);
         mWorkTaskExecutor.executeOnBackgroundThread(runnable);
-        return runnable.getFuture();
+        return runnable.getOperation();
     }
 
     @Override
-    public @NonNull ListenableFuture<Void> cancelAllWorkInternal() {
+    public @NonNull Operation cancelAllWork() {
         CancelWorkRunnable runnable = CancelWorkRunnable.forAll(this);
         mWorkTaskExecutor.executeOnBackgroundThread(runnable);
-        return runnable.getFuture();
+        return runnable.getOperation();
     }
 
     @Override
@@ -405,84 +416,84 @@
     }
 
     @Override
-    public @NonNull ListenableFuture<Void> pruneWorkInternal() {
+    public @NonNull Operation pruneWork() {
         PruneWorkRunnable runnable = new PruneWorkRunnable(this);
         mWorkTaskExecutor.executeOnBackgroundThread(runnable);
-        return runnable.getFuture();
+        return runnable.getOperation();
     }
 
     @Override
-    public @NonNull LiveData<WorkStatus> getStatusByIdLiveData(@NonNull UUID id) {
+    public @NonNull LiveData<WorkInfo> getWorkInfoByIdLiveData(@NonNull UUID id) {
         WorkSpecDao dao = mWorkDatabase.workSpecDao();
-        LiveData<List<WorkSpec.WorkStatusPojo>> inputLiveData =
+        LiveData<List<WorkSpec.WorkInfoPojo>> inputLiveData =
                 dao.getWorkStatusPojoLiveDataForIds(Collections.singletonList(id.toString()));
         return LiveDataUtils.dedupedMappedLiveDataFor(inputLiveData,
-                new Function<List<WorkSpec.WorkStatusPojo>, WorkStatus>() {
+                new Function<List<WorkSpec.WorkInfoPojo>, WorkInfo>() {
                     @Override
-                    public WorkStatus apply(List<WorkSpec.WorkStatusPojo> input) {
-                        WorkStatus workStatus = null;
+                    public WorkInfo apply(List<WorkSpec.WorkInfoPojo> input) {
+                        WorkInfo workInfo = null;
                         if (input != null && input.size() > 0) {
-                            workStatus = input.get(0).toWorkStatus();
+                            workInfo = input.get(0).toWorkInfo();
                         }
-                        return workStatus;
+                        return workInfo;
                     }
                 },
                 mWorkTaskExecutor);
     }
 
     @Override
-    public @NonNull ListenableFuture<WorkStatus> getStatusById(@NonNull UUID id) {
-        StatusRunnable<WorkStatus> runnable = StatusRunnable.forUUID(this, id);
+    public @NonNull ListenableFuture<WorkInfo> getWorkInfoById(@NonNull UUID id) {
+        StatusRunnable<WorkInfo> runnable = StatusRunnable.forUUID(this, id);
         mWorkTaskExecutor.getBackgroundExecutor().execute(runnable);
         return runnable.getFuture();
     }
 
     @Override
-    public @NonNull LiveData<List<WorkStatus>> getStatusesByTagLiveData(@NonNull String tag) {
+    public @NonNull LiveData<List<WorkInfo>> getWorkInfosByTagLiveData(@NonNull String tag) {
         WorkSpecDao workSpecDao = mWorkDatabase.workSpecDao();
-        LiveData<List<WorkSpec.WorkStatusPojo>> inputLiveData =
+        LiveData<List<WorkSpec.WorkInfoPojo>> inputLiveData =
                 workSpecDao.getWorkStatusPojoLiveDataForTag(tag);
         return LiveDataUtils.dedupedMappedLiveDataFor(
                 inputLiveData,
-                WorkSpec.WORK_STATUS_MAPPER,
+                WorkSpec.WORK_INFO_MAPPER,
                 mWorkTaskExecutor);
     }
 
     @Override
-    public @NonNull ListenableFuture<List<WorkStatus>> getStatusesByTag(@NonNull String tag) {
-        StatusRunnable<List<WorkStatus>> runnable = StatusRunnable.forTag(this, tag);
+    public @NonNull ListenableFuture<List<WorkInfo>> getWorkInfosByTag(@NonNull String tag) {
+        StatusRunnable<List<WorkInfo>> runnable = StatusRunnable.forTag(this, tag);
         mWorkTaskExecutor.getBackgroundExecutor().execute(runnable);
         return runnable.getFuture();
     }
 
     @Override
-    public @NonNull LiveData<List<WorkStatus>> getStatusesForUniqueWorkLiveData(
-            @NonNull String name) {
+    @NonNull
+    public LiveData<List<WorkInfo>> getWorkInfosForUniqueWorkLiveData(@NonNull String name) {
         WorkSpecDao workSpecDao = mWorkDatabase.workSpecDao();
-        LiveData<List<WorkSpec.WorkStatusPojo>> inputLiveData =
+        LiveData<List<WorkSpec.WorkInfoPojo>> inputLiveData =
                 workSpecDao.getWorkStatusPojoLiveDataForName(name);
         return LiveDataUtils.dedupedMappedLiveDataFor(
                 inputLiveData,
-                WorkSpec.WORK_STATUS_MAPPER,
+                WorkSpec.WORK_INFO_MAPPER,
                 mWorkTaskExecutor);
     }
 
     @Override
-    public @NonNull ListenableFuture<List<WorkStatus>> getStatusesForUniqueWork(
-            @NonNull String name) {
-        StatusRunnable<List<WorkStatus>> runnable =
+    @NonNull
+    public ListenableFuture<List<WorkInfo>> getWorkInfosForUniqueWork(@NonNull String name) {
+        StatusRunnable<List<WorkInfo>> runnable =
                 StatusRunnable.forUniqueWork(this, name);
         mWorkTaskExecutor.getBackgroundExecutor().execute(runnable);
         return runnable.getFuture();
     }
 
-    LiveData<List<WorkStatus>> getStatusesById(@NonNull List<String> workSpecIds) {
+    LiveData<List<WorkInfo>> getWorkInfosById(@NonNull List<String> workSpecIds) {
         WorkSpecDao dao = mWorkDatabase.workSpecDao();
-        LiveData<List<WorkSpec.WorkStatusPojo>> inputLiveData =
+        LiveData<List<WorkSpec.WorkInfoPojo>> inputLiveData =
                 dao.getWorkStatusPojoLiveDataForIds(workSpecIds);
         return LiveDataUtils.dedupedMappedLiveDataFor(
                 inputLiveData,
-                WorkSpec.WORK_STATUS_MAPPER,
+                WorkSpec.WORK_INFO_MAPPER,
                 mWorkTaskExecutor);
     }
 
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 ac9e818..f989803 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/WorkerWrapper.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/WorkerWrapper.java
@@ -16,11 +16,11 @@
 
 package androidx.work.impl;
 
-import static androidx.work.State.CANCELLED;
-import static androidx.work.State.ENQUEUED;
-import static androidx.work.State.FAILED;
-import static androidx.work.State.RUNNING;
-import static androidx.work.State.SUCCEEDED;
+import static androidx.work.WorkInfo.State.CANCELLED;
+import static androidx.work.WorkInfo.State.ENQUEUED;
+import static androidx.work.WorkInfo.State.FAILED;
+import static androidx.work.WorkInfo.State.RUNNING;
+import static androidx.work.WorkInfo.State.SUCCEEDED;
 import static androidx.work.impl.model.WorkSpec.SCHEDULE_NOT_REQUESTED_YET;
 
 import android.content.Context;
@@ -37,7 +37,7 @@
 import androidx.work.ListenableWorker;
 import androidx.work.ListenableWorker.Result;
 import androidx.work.Logger;
-import androidx.work.State;
+import androidx.work.WorkInfo;
 import androidx.work.Worker;
 import androidx.work.WorkerParameters;
 import androidx.work.impl.background.systemalarm.RescheduleReceiver;
@@ -264,7 +264,7 @@
         if (!tryCheckForInterruptionAndResolve()) {
             try {
                 mWorkDatabase.beginTransaction();
-                State state = mWorkSpecDao.getState(mWorkSpecId);
+                WorkInfo.State state = mWorkSpecDao.getState(mWorkSpecId);
                 if (state == null) {
                     // state can be null here with a REPLACE on beginUniqueWork().
                     // Treat it as a failure, and rescheduleAndResolve() will
@@ -318,12 +318,12 @@
         }
         // Worker can be null if run() hasn't been called yet.
         if (mWorker != null) {
-            mWorker.stop(cancelled);
+            mWorker.stop();
         }
     }
 
     private void resolveIncorrectStatus() {
-        State status = mWorkSpecDao.getState(mWorkSpecId);
+        WorkInfo.State status = mWorkSpecDao.getState(mWorkSpecId);
         if (status == RUNNING) {
             Logger.debug(TAG, String.format("Status for %s is RUNNING;"
                     + "not doing any work and rescheduling for later execution", mWorkSpecId));
@@ -338,7 +338,7 @@
     private boolean tryCheckForInterruptionAndResolve() {
         if (mInterrupted) {
             Logger.info(TAG, String.format("Work interrupted for %s", mWorkDescription));
-            State currentState = mWorkSpecDao.getState(mWorkSpecId);
+            WorkInfo.State currentState = mWorkSpecDao.getState(mWorkSpecId);
             if (currentState == null) {
                 // This can happen because of a beginUniqueWork(..., REPLACE, ...).  Notify the
                 // listeners so we can clean up any wake locks, etc.
@@ -408,7 +408,7 @@
         boolean setToRunning = false;
         mWorkDatabase.beginTransaction();
         try {
-            State currentState = mWorkSpecDao.getState(mWorkSpecId);
+            WorkInfo.State currentState = mWorkSpecDao.getState(mWorkSpecId);
             if (currentState == ENQUEUED) {
                 mWorkSpecDao.setState(RUNNING, mWorkSpecId);
                 mWorkSpecDao.incrementWorkSpecRunAttemptCount(mWorkSpecId);
diff --git a/work/workmanager/src/main/java/androidx/work/impl/background/greedy/GreedyScheduler.java b/work/workmanager/src/main/java/androidx/work/impl/background/greedy/GreedyScheduler.java
index 5e695ba..b174ab1 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/background/greedy/GreedyScheduler.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/background/greedy/GreedyScheduler.java
@@ -24,7 +24,7 @@
 import android.text.TextUtils;
 
 import androidx.work.Logger;
-import androidx.work.State;
+import androidx.work.WorkInfo;
 import androidx.work.impl.ExecutionListener;
 import androidx.work.impl.Scheduler;
 import androidx.work.impl.WorkManagerImpl;
@@ -78,7 +78,7 @@
         List<WorkSpec> constrainedWorkSpecs = new ArrayList<>();
         List<String> constrainedWorkSpecIds = new ArrayList<>();
         for (WorkSpec workSpec: workSpecs) {
-            if (workSpec.state == State.ENQUEUED
+            if (workSpec.state == WorkInfo.State.ENQUEUED
                     && !workSpec.isPeriodic()
                     && workSpec.initialDelay == 0L
                     && !workSpec.isBackedOff()) {
diff --git a/work/workmanager/src/main/java/androidx/work/impl/background/systemalarm/CommandHandler.java b/work/workmanager/src/main/java/androidx/work/impl/background/systemalarm/CommandHandler.java
index daa5b96..8ac6d01 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/background/systemalarm/CommandHandler.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/background/systemalarm/CommandHandler.java
@@ -25,7 +25,7 @@
 import android.support.annotation.WorkerThread;
 
 import androidx.work.Logger;
-import androidx.work.State;
+import androidx.work.WorkInfo;
 import androidx.work.impl.ExecutionListener;
 import androidx.work.impl.WorkDatabase;
 import androidx.work.impl.WorkManagerImpl;
@@ -211,7 +211,7 @@
                         "Skipping scheduling " + workSpecId + " because it's no longer in "
                         + "the DB");
                 return;
-            } else if (workSpec.state != State.ENQUEUED) {
+            } else if (workSpec.state != WorkInfo.State.ENQUEUED) {
                 Logger.warning(TAG,
                         "Skipping scheduling " + workSpecId + " because it is no longer "
                         + "enqueued");
diff --git a/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java b/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java
index 67ecd38..cdb7aef 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java
@@ -26,7 +26,7 @@
 import android.support.annotation.VisibleForTesting;
 
 import androidx.work.Logger;
-import androidx.work.State;
+import androidx.work.WorkInfo;
 import androidx.work.impl.Scheduler;
 import androidx.work.impl.WorkDatabase;
 import androidx.work.impl.WorkManagerImpl;
@@ -92,7 +92,7 @@
                             "Skipping scheduling " + workSpec.id
                                     + " because it's no longer in the DB");
                     continue;
-                } else if (currentDbWorkSpec.state != State.ENQUEUED) {
+                } else if (currentDbWorkSpec.state != WorkInfo.State.ENQUEUED) {
                     Logger.warning(
                             TAG,
                             "Skipping scheduling " + workSpec.id
diff --git a/work/workmanager/src/main/java/androidx/work/impl/model/WorkSpec.java b/work/workmanager/src/main/java/androidx/work/impl/model/WorkSpec.java
index c501aad..d99e5b9 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/model/WorkSpec.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/model/WorkSpec.java
@@ -18,7 +18,7 @@
 
 import static androidx.work.PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS;
 import static androidx.work.PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS;
-import static androidx.work.State.ENQUEUED;
+import static androidx.work.WorkInfo.State.ENQUEUED;
 import static androidx.work.WorkRequest.MAX_BACKOFF_MILLIS;
 import static androidx.work.WorkRequest.MIN_BACKOFF_MILLIS;
 
@@ -36,9 +36,8 @@
 import androidx.work.Constraints;
 import androidx.work.Data;
 import androidx.work.Logger;
-import androidx.work.State;
+import androidx.work.WorkInfo;
 import androidx.work.WorkRequest;
-import androidx.work.WorkStatus;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -64,7 +63,7 @@
 
     @ColumnInfo(name = "state")
     @NonNull
-    public State state = ENQUEUED;
+    public WorkInfo.State state = ENQUEUED;
 
     @ColumnInfo(name = "worker_class_name")
     @NonNull
@@ -326,7 +325,7 @@
         public String id;
 
         @ColumnInfo(name = "state")
-        public State state;
+        public WorkInfo.State state;
 
         @Override
         public boolean equals(Object o) {
@@ -350,13 +349,13 @@
     /**
      * A POJO containing the ID, state, output, and tags of a WorkSpec.
      */
-    public static class WorkStatusPojo {
+    public static class WorkInfoPojo {
 
         @ColumnInfo(name = "id")
         public String id;
 
         @ColumnInfo(name = "state")
-        public State state;
+        public WorkInfo.State state;
 
         @ColumnInfo(name = "output")
         public Data output;
@@ -369,12 +368,12 @@
         public List<String> tags;
 
         /**
-         * Converts this POJO to a {@link WorkStatus}.
+         * Converts this POJO to a {@link WorkInfo}.
          *
-         * @return The {@link WorkStatus} represented by this POJO
+         * @return The {@link WorkInfo} represented by this POJO
          */
-        public WorkStatus toWorkStatus() {
-            return new WorkStatus(UUID.fromString(id), state, output, tags);
+        public WorkInfo toWorkInfo() {
+            return new WorkInfo(UUID.fromString(id), state, output, tags);
         }
 
         @Override
@@ -382,7 +381,7 @@
             if (this == o) return true;
             if (o == null || getClass() != o.getClass()) return false;
 
-            WorkStatusPojo that = (WorkStatusPojo) o;
+            WorkInfoPojo that = (WorkInfoPojo) o;
 
             if (id != null ? !id.equals(that.id) : that.id != null) return false;
             if (state != that.state) return false;
@@ -400,16 +399,16 @@
         }
     }
 
-    public static final Function<List<WorkStatusPojo>, List<WorkStatus>> WORK_STATUS_MAPPER =
-            new Function<List<WorkStatusPojo>, List<WorkStatus>>() {
+    public static final Function<List<WorkInfoPojo>, List<WorkInfo>> WORK_INFO_MAPPER =
+            new Function<List<WorkInfoPojo>, List<WorkInfo>>() {
                 @Override
-                public List<WorkStatus> apply(List<WorkStatusPojo> input) {
+                public List<WorkInfo> apply(List<WorkInfoPojo> input) {
                     if (input == null) {
                         return null;
                     }
-                    List<WorkStatus> output = new ArrayList<>(input.size());
-                    for (WorkStatusPojo in : input) {
-                        output.add(in.toWorkStatus());
+                    List<WorkInfo> output = new ArrayList<>(input.size());
+                    for (WorkInfoPojo in : input) {
+                        output.add(in.toWorkInfo());
                     }
                     return output;
                 }
diff --git a/work/workmanager/src/main/java/androidx/work/impl/model/WorkSpecDao.java b/work/workmanager/src/main/java/androidx/work/impl/model/WorkSpecDao.java
index dcade1f..b53caf7 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/model/WorkSpecDao.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/model/WorkSpecDao.java
@@ -28,7 +28,7 @@
 import android.support.annotation.NonNull;
 
 import androidx.work.Data;
-import androidx.work.State;
+import androidx.work.WorkInfo;
 
 import java.util.List;
 
@@ -94,7 +94,7 @@
      * @return The number of rows that were updated
      */
     @Query("UPDATE workspec SET state=:state WHERE id IN (:ids)")
-    int setState(State state, String... ids);
+    int setState(WorkInfo.State state, String... ids);
 
     /**
      * Updates the output of a {@link WorkSpec}.
@@ -139,85 +139,85 @@
      * @return The state of the {@link WorkSpec}
      */
     @Query("SELECT state FROM workspec WHERE id=:id")
-    State getState(String id);
+    WorkInfo.State getState(String id);
 
     /**
-     * For a {@link WorkSpec} identifier, retrieves its {@link WorkSpec.WorkStatusPojo}.
+     * For a {@link WorkSpec} identifier, retrieves its {@link WorkSpec.WorkInfoPojo}.
      *
      * @param id The identifier of the {@link WorkSpec}
-     * @return A list of {@link WorkSpec.WorkStatusPojo}
+     * @return A list of {@link WorkSpec.WorkInfoPojo}
      */
     @Transaction
     @Query("SELECT id, state, output FROM workspec WHERE id=:id")
-    WorkSpec.WorkStatusPojo getWorkStatusPojoForId(String id);
+    WorkSpec.WorkInfoPojo getWorkStatusPojoForId(String id);
 
     /**
      * For a list of {@link WorkSpec} identifiers, retrieves a {@link List} of their
-     * {@link WorkSpec.WorkStatusPojo}.
+     * {@link WorkSpec.WorkInfoPojo}.
      *
      * @param ids The identifier of the {@link WorkSpec}s
-     * @return A {@link List} of {@link WorkSpec.WorkStatusPojo}
+     * @return A {@link List} of {@link WorkSpec.WorkInfoPojo}
      */
     @Transaction
     @Query("SELECT id, state, output FROM workspec WHERE id IN (:ids)")
-    List<WorkSpec.WorkStatusPojo> getWorkStatusPojoForIds(List<String> ids);
+    List<WorkSpec.WorkInfoPojo> getWorkStatusPojoForIds(List<String> ids);
 
     /**
      * For a list of {@link WorkSpec} identifiers, retrieves a {@link LiveData} list of their
-     * {@link WorkSpec.WorkStatusPojo}.
+     * {@link WorkSpec.WorkInfoPojo}.
      *
      * @param ids The identifier of the {@link WorkSpec}s
-     * @return A {@link LiveData} list of {@link WorkSpec.WorkStatusPojo}
+     * @return A {@link LiveData} list of {@link WorkSpec.WorkInfoPojo}
      */
     @Transaction
     @Query("SELECT id, state, output FROM workspec WHERE id IN (:ids)")
-    LiveData<List<WorkSpec.WorkStatusPojo>> getWorkStatusPojoLiveDataForIds(List<String> ids);
+    LiveData<List<WorkSpec.WorkInfoPojo>> getWorkStatusPojoLiveDataForIds(List<String> ids);
 
     /**
-     * Retrieves a list of {@link WorkSpec.WorkStatusPojo} for all work with a given tag.
+     * Retrieves a list of {@link WorkSpec.WorkInfoPojo} for all work with a given tag.
      *
      * @param tag The tag for the {@link WorkSpec}s
-     * @return A list of {@link WorkSpec.WorkStatusPojo}
+     * @return A list of {@link WorkSpec.WorkInfoPojo}
      */
     @Transaction
     @Query("SELECT id, state, output FROM workspec WHERE id IN "
             + "(SELECT work_spec_id FROM worktag WHERE tag=:tag)")
-    List<WorkSpec.WorkStatusPojo> getWorkStatusPojoForTag(String tag);
+    List<WorkSpec.WorkInfoPojo> getWorkStatusPojoForTag(String tag);
 
     /**
-     * Retrieves a {@link LiveData} list of {@link WorkSpec.WorkStatusPojo} for all work with a
+     * Retrieves a {@link LiveData} list of {@link WorkSpec.WorkInfoPojo} for all work with a
      * given tag.
      *
      * @param tag The tag for the {@link WorkSpec}s
-     * @return A {@link LiveData} list of {@link WorkSpec.WorkStatusPojo}
+     * @return A {@link LiveData} list of {@link WorkSpec.WorkInfoPojo}
      */
     @Transaction
     @Query("SELECT id, state, output FROM workspec WHERE id IN "
             + "(SELECT work_spec_id FROM worktag WHERE tag=:tag)")
-    LiveData<List<WorkSpec.WorkStatusPojo>> getWorkStatusPojoLiveDataForTag(String tag);
+    LiveData<List<WorkSpec.WorkInfoPojo>> getWorkStatusPojoLiveDataForTag(String tag);
 
     /**
-     * Retrieves a list of {@link WorkSpec.WorkStatusPojo} for all work with a given name.
+     * Retrieves a list of {@link WorkSpec.WorkInfoPojo} for all work with a given name.
      *
      * @param name The name of the {@link WorkSpec}s
-     * @return A list of {@link WorkSpec.WorkStatusPojo}
+     * @return A list of {@link WorkSpec.WorkInfoPojo}
      */
     @Transaction
     @Query("SELECT id, state, output FROM workspec WHERE id IN "
             + "(SELECT work_spec_id FROM workname WHERE name=:name)")
-    List<WorkSpec.WorkStatusPojo> getWorkStatusPojoForName(String name);
+    List<WorkSpec.WorkInfoPojo> getWorkStatusPojoForName(String name);
 
     /**
-     * Retrieves a {@link LiveData} list of {@link WorkSpec.WorkStatusPojo} for all work with a
+     * Retrieves a {@link LiveData} list of {@link WorkSpec.WorkInfoPojo} for all work with a
      * given name.
      *
      * @param name The name for the {@link WorkSpec}s
-     * @return A {@link LiveData} list of {@link WorkSpec.WorkStatusPojo}
+     * @return A {@link LiveData} list of {@link WorkSpec.WorkInfoPojo}
      */
     @Transaction
     @Query("SELECT id, state, output FROM workspec WHERE id IN "
             + "(SELECT work_spec_id FROM workname WHERE name=:name)")
-    LiveData<List<WorkSpec.WorkStatusPojo>> getWorkStatusPojoLiveDataForName(String name);
+    LiveData<List<WorkSpec.WorkInfoPojo>> getWorkStatusPojoLiveDataForName(String name);
 
     /**
      * Gets all inputs coming from prerequisites for a particular {@link WorkSpec}.  These are
diff --git a/work/workmanager/src/main/java/androidx/work/impl/model/WorkTypeConverters.java b/work/workmanager/src/main/java/androidx/work/impl/model/WorkTypeConverters.java
index afa348c..3f7eb90 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/model/WorkTypeConverters.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/model/WorkTypeConverters.java
@@ -18,12 +18,12 @@
 
 import static androidx.work.BackoffPolicy.EXPONENTIAL;
 import static androidx.work.BackoffPolicy.LINEAR;
-import static androidx.work.State.BLOCKED;
-import static androidx.work.State.CANCELLED;
-import static androidx.work.State.ENQUEUED;
-import static androidx.work.State.FAILED;
-import static androidx.work.State.RUNNING;
-import static androidx.work.State.SUCCEEDED;
+import static androidx.work.WorkInfo.State.BLOCKED;
+import static androidx.work.WorkInfo.State.CANCELLED;
+import static androidx.work.WorkInfo.State.ENQUEUED;
+import static androidx.work.WorkInfo.State.FAILED;
+import static androidx.work.WorkInfo.State.RUNNING;
+import static androidx.work.WorkInfo.State.SUCCEEDED;
 
 import android.arch.persistence.room.TypeConverter;
 import android.net.Uri;
@@ -31,7 +31,7 @@
 import androidx.work.BackoffPolicy;
 import androidx.work.ContentUriTriggers;
 import androidx.work.NetworkType;
-import androidx.work.State;
+import androidx.work.WorkInfo;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
@@ -46,7 +46,7 @@
 public class WorkTypeConverters {
 
     /**
-     * Integer identifiers that map to {@link State}.
+     * Integer identifiers that map to {@link WorkInfo.State}.
      */
     public interface StateIds {
         int ENQUEUED = 0;
@@ -85,7 +85,7 @@
      * @return The associated int constant
      */
     @TypeConverter
-    public static int stateToInt(State state) {
+    public static int stateToInt(WorkInfo.State state) {
         switch (state) {
             case ENQUEUED:
                 return StateIds.ENQUEUED;
@@ -118,7 +118,7 @@
      * @return The associated State enum value
      */
     @TypeConverter
-    public static State intToState(int value) {
+    public static WorkInfo.State intToState(int value) {
         switch (value) {
             case StateIds.ENQUEUED:
                 return ENQUEUED;
diff --git a/work/workmanager/src/main/java/androidx/work/impl/utils/CancelWorkRunnable.java b/work/workmanager/src/main/java/androidx/work/impl/utils/CancelWorkRunnable.java
index 8fa72a7..5dcd0d0 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/utils/CancelWorkRunnable.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/utils/CancelWorkRunnable.java
@@ -16,15 +16,17 @@
 
 package androidx.work.impl.utils;
 
-import static androidx.work.State.CANCELLED;
-import static androidx.work.State.FAILED;
-import static androidx.work.State.SUCCEEDED;
+import static androidx.work.WorkInfo.State.CANCELLED;
+import static androidx.work.WorkInfo.State.FAILED;
+import static androidx.work.WorkInfo.State.SUCCEEDED;
 
 import android.support.annotation.NonNull;
 import android.support.annotation.RestrictTo;
 import android.support.annotation.WorkerThread;
 
-import androidx.work.State;
+import androidx.work.Operation;
+import androidx.work.WorkInfo;
+import androidx.work.impl.OperationImpl;
 import androidx.work.impl.Processor;
 import androidx.work.impl.Scheduler;
 import androidx.work.impl.Schedulers;
@@ -32,9 +34,6 @@
 import androidx.work.impl.WorkManagerImpl;
 import androidx.work.impl.model.DependencyDao;
 import androidx.work.impl.model.WorkSpecDao;
-import androidx.work.impl.utils.futures.SettableFuture;
-
-import com.google.common.util.concurrent.ListenableFuture;
 
 import java.util.List;
 import java.util.UUID;
@@ -47,22 +46,22 @@
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 public abstract class CancelWorkRunnable implements Runnable {
 
-    private final SettableFuture<Void> mFuture = SettableFuture.create();
+    private final OperationImpl mOperation = new OperationImpl();
 
     /**
-     * @return A {@link ListenableFuture} that completes when the cancel operation is completed
+     * @return The {@link Operation} that encapsulates the state of the {@link CancelWorkRunnable}.
      */
-    public ListenableFuture<Void> getFuture() {
-        return mFuture;
+    public Operation getOperation() {
+        return mOperation;
     }
 
     @Override
     public void run() {
         try {
             runInternal();
-            mFuture.set(null);
+            mOperation.setState(Operation.SUCCESS);
         } catch (Throwable throwable) {
-            mFuture.setException(throwable);
+            mOperation.setState(new Operation.State.FAILURE(throwable));
         }
     }
 
@@ -95,7 +94,7 @@
             recursivelyCancelWorkAndDependents(workDatabase, id);
         }
 
-        State state = workSpecDao.getState(workSpecId);
+        WorkInfo.State state = workSpecDao.getState(workSpecId);
         if (state != SUCCEEDED && state != FAILED) {
             workSpecDao.setState(CANCELLED, workSpecId);
         }
diff --git a/work/workmanager/src/main/java/androidx/work/impl/utils/EnqueueRunnable.java b/work/workmanager/src/main/java/androidx/work/impl/utils/EnqueueRunnable.java
index d6adfb8..cc02bcb 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/utils/EnqueueRunnable.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/utils/EnqueueRunnable.java
@@ -18,12 +18,12 @@
 
 import static androidx.work.ExistingWorkPolicy.APPEND;
 import static androidx.work.ExistingWorkPolicy.KEEP;
-import static androidx.work.State.BLOCKED;
-import static androidx.work.State.CANCELLED;
-import static androidx.work.State.ENQUEUED;
-import static androidx.work.State.FAILED;
-import static androidx.work.State.RUNNING;
-import static androidx.work.State.SUCCEEDED;
+import static androidx.work.WorkInfo.State.BLOCKED;
+import static androidx.work.WorkInfo.State.CANCELLED;
+import static androidx.work.WorkInfo.State.ENQUEUED;
+import static androidx.work.WorkInfo.State.FAILED;
+import static androidx.work.WorkInfo.State.RUNNING;
+import static androidx.work.WorkInfo.State.SUCCEEDED;
 import static androidx.work.impl.workers.ConstraintTrackingWorker.ARGUMENT_CLASS_NAME;
 
 import android.content.Context;
@@ -37,8 +37,10 @@
 import androidx.work.Data;
 import androidx.work.ExistingWorkPolicy;
 import androidx.work.Logger;
-import androidx.work.State;
+import androidx.work.Operation;
+import androidx.work.WorkInfo;
 import androidx.work.WorkRequest;
+import androidx.work.impl.OperationImpl;
 import androidx.work.impl.Schedulers;
 import androidx.work.impl.WorkContinuationImpl;
 import androidx.work.impl.WorkDatabase;
@@ -50,11 +52,8 @@
 import androidx.work.impl.model.WorkSpec;
 import androidx.work.impl.model.WorkSpecDao;
 import androidx.work.impl.model.WorkTag;
-import androidx.work.impl.utils.futures.SettableFuture;
 import androidx.work.impl.workers.ConstraintTrackingWorker;
 
-import com.google.common.util.concurrent.ListenableFuture;
-
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
@@ -70,11 +69,11 @@
     private static final String TAG = "EnqueueRunnable";
 
     private final WorkContinuationImpl mWorkContinuation;
-    private final SettableFuture<Void> mFuture;
+    private final OperationImpl mOperation;
 
     public EnqueueRunnable(@NonNull WorkContinuationImpl workContinuation) {
         mWorkContinuation = workContinuation;
-        mFuture = SettableFuture.create();
+        mOperation = new OperationImpl();
     }
 
     @Override
@@ -92,14 +91,17 @@
                 PackageManagerHelper.setComponentEnabled(context, RescheduleReceiver.class, true);
                 scheduleWorkInBackground();
             }
-            mFuture.set(null);
+            mOperation.setState(Operation.SUCCESS);
         } catch (Throwable exception) {
-            mFuture.setException(exception);
+            mOperation.setState(new Operation.State.FAILURE(exception));
         }
     }
 
-    public ListenableFuture<Void> getFuture() {
-        return mFuture;
+    /**
+     * @return The {@link Operation} that encapsulates the state of the {@link EnqueueRunnable}.
+     */
+    public Operation getOperation() {
+        return mOperation;
     }
 
     /**
@@ -199,7 +201,7 @@
                     return false;
                 }
 
-                State prerequisiteState = prerequisiteWorkSpec.state;
+                WorkInfo.State prerequisiteState = prerequisiteWorkSpec.state;
                 hasCompletedAllPrerequisites &= (prerequisiteState == SUCCEEDED);
                 if (prerequisiteState == FAILED) {
                     hasFailedPrerequisites = true;
diff --git a/work/workmanager/src/main/java/androidx/work/impl/utils/PruneWorkRunnable.java b/work/workmanager/src/main/java/androidx/work/impl/utils/PruneWorkRunnable.java
index 7a7fa43..3783fcf 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/utils/PruneWorkRunnable.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/utils/PruneWorkRunnable.java
@@ -18,12 +18,11 @@
 
 import android.support.annotation.RestrictTo;
 
+import androidx.work.Operation;
+import androidx.work.impl.OperationImpl;
 import androidx.work.impl.WorkDatabase;
 import androidx.work.impl.WorkManagerImpl;
 import androidx.work.impl.model.WorkSpecDao;
-import androidx.work.impl.utils.futures.SettableFuture;
-
-import com.google.common.util.concurrent.ListenableFuture;
 
 /**
  * A Runnable that prunes work in the background.  Pruned work meets the following criteria:
@@ -36,26 +35,30 @@
 public class PruneWorkRunnable implements Runnable {
 
     private final WorkManagerImpl mWorkManagerImpl;
-    private final SettableFuture<Void> mFuture;
+    private final OperationImpl mOperation;
 
     public PruneWorkRunnable(WorkManagerImpl workManagerImpl) {
         mWorkManagerImpl = workManagerImpl;
-        mFuture = SettableFuture.create();
+        mOperation = new OperationImpl();
     }
 
-    public ListenableFuture<Void> getFuture() {
-        return mFuture;
+    /**
+     * @return The {@link Operation} that encapsulates the state of the {@link PruneWorkRunnable}.
+     */
+    public Operation getOperation() {
+        return mOperation;
     }
 
+
     @Override
     public void run() {
         try {
             WorkDatabase workDatabase = mWorkManagerImpl.getWorkDatabase();
             WorkSpecDao workSpecDao = workDatabase.workSpecDao();
             workSpecDao.pruneFinishedWorkWithZeroDependentsIgnoringKeepForAtLeast();
-            mFuture.set(null);
+            mOperation.setState(Operation.SUCCESS);
         } catch (Throwable exception) {
-            mFuture.setException(exception);
+            mOperation.setState(new Operation.State.FAILURE(exception));
         }
     }
 }
diff --git a/work/workmanager/src/main/java/androidx/work/impl/utils/StatusRunnable.java b/work/workmanager/src/main/java/androidx/work/impl/utils/StatusRunnable.java
index 1b9b0dc..ef275e5 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/utils/StatusRunnable.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/utils/StatusRunnable.java
@@ -20,7 +20,7 @@
 import android.support.annotation.RestrictTo;
 import android.support.annotation.WorkerThread;
 
-import androidx.work.WorkStatus;
+import androidx.work.WorkInfo;
 import androidx.work.impl.WorkDatabase;
 import androidx.work.impl.WorkManagerImpl;
 import androidx.work.impl.model.WorkSpec;
@@ -32,7 +32,7 @@
 import java.util.UUID;
 
 /**
- * A {@link Runnable} to get {@link WorkStatus}es.
+ * A {@link Runnable} to get {@link WorkInfo}es.
  *
  * @param <T> The expected return type for the {@link ListenableFuture}.
  * @hide
@@ -66,18 +66,18 @@
      * @param ids         The {@link List} of {@link String} ids
      * @return an instance of {@link StatusRunnable}
      */
-    public static StatusRunnable<List<WorkStatus>> forStringIds(
+    public static StatusRunnable<List<WorkInfo>> forStringIds(
             @NonNull final WorkManagerImpl workManager,
             @NonNull final List<String> ids) {
 
-        return new StatusRunnable<List<WorkStatus>>() {
+        return new StatusRunnable<List<WorkInfo>>() {
             @Override
-            public List<WorkStatus> runInternal() {
+            public List<WorkInfo> runInternal() {
                 WorkDatabase workDatabase = workManager.getWorkDatabase();
-                List<WorkSpec.WorkStatusPojo> workStatusPojos =
+                List<WorkSpec.WorkInfoPojo> workInfoPojos =
                         workDatabase.workSpecDao().getWorkStatusPojoForIds(ids);
 
-                return WorkSpec.WORK_STATUS_MAPPER.apply(workStatusPojos);
+                return WorkSpec.WORK_INFO_MAPPER.apply(workInfoPojos);
             }
         };
     }
@@ -90,18 +90,18 @@
      * @param id          The workSpec {@link UUID}
      * @return an instance of {@link StatusRunnable}
      */
-    public static StatusRunnable<WorkStatus> forUUID(
+    public static StatusRunnable<WorkInfo> forUUID(
             @NonNull final WorkManagerImpl workManager,
             @NonNull final UUID id) {
 
-        return new StatusRunnable<WorkStatus>() {
+        return new StatusRunnable<WorkInfo>() {
             @Override
-            WorkStatus runInternal() {
+            WorkInfo runInternal() {
                 WorkDatabase workDatabase = workManager.getWorkDatabase();
-                WorkSpec.WorkStatusPojo workStatusPojo =
+                WorkSpec.WorkInfoPojo workInfoPojo =
                         workDatabase.workSpecDao().getWorkStatusPojoForId(id.toString());
 
-                return workStatusPojo != null ? workStatusPojo.toWorkStatus() : null;
+                return workInfoPojo != null ? workInfoPojo.toWorkInfo() : null;
             }
         };
     }
@@ -114,18 +114,18 @@
      * @param tag The {@link String} tag
      * @return an instance of {@link StatusRunnable}
      */
-    public static StatusRunnable<List<WorkStatus>> forTag(
+    public static StatusRunnable<List<WorkInfo>> forTag(
             @NonNull final WorkManagerImpl workManager,
             @NonNull final String tag) {
 
-        return new StatusRunnable<List<WorkStatus>>() {
+        return new StatusRunnable<List<WorkInfo>>() {
             @Override
-            List<WorkStatus> runInternal() {
+            List<WorkInfo> runInternal() {
                 WorkDatabase workDatabase = workManager.getWorkDatabase();
-                List<WorkSpec.WorkStatusPojo> workStatusPojos =
+                List<WorkSpec.WorkInfoPojo> workInfoPojos =
                         workDatabase.workSpecDao().getWorkStatusPojoForTag(tag);
 
-                return WorkSpec.WORK_STATUS_MAPPER.apply(workStatusPojos);
+                return WorkSpec.WORK_INFO_MAPPER.apply(workInfoPojos);
             }
         };
     }
@@ -138,18 +138,18 @@
      * @param name The {@link String} unique name
      * @return an instance of {@link StatusRunnable}
      */
-    public static StatusRunnable<List<WorkStatus>> forUniqueWork(
+    public static StatusRunnable<List<WorkInfo>> forUniqueWork(
             @NonNull final WorkManagerImpl workManager,
             @NonNull final String name) {
 
-        return new StatusRunnable<List<WorkStatus>>() {
+        return new StatusRunnable<List<WorkInfo>>() {
             @Override
-            List<WorkStatus> runInternal() {
+            List<WorkInfo> runInternal() {
                 WorkDatabase workDatabase = workManager.getWorkDatabase();
-                List<WorkSpec.WorkStatusPojo> workStatusPojos =
+                List<WorkSpec.WorkInfoPojo> workInfoPojos =
                         workDatabase.workSpecDao().getWorkStatusPojoForName(name);
 
-                return WorkSpec.WORK_STATUS_MAPPER.apply(workStatusPojos);
+                return WorkSpec.WORK_INFO_MAPPER.apply(workInfoPojos);
             }
         };
     }
diff --git a/work/workmanager/src/main/java/androidx/work/impl/utils/StopWorkRunnable.java b/work/workmanager/src/main/java/androidx/work/impl/utils/StopWorkRunnable.java
index a86ecad..64e7bfc 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/utils/StopWorkRunnable.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/utils/StopWorkRunnable.java
@@ -19,14 +19,14 @@
 import android.support.annotation.RestrictTo;
 
 import androidx.work.Logger;
-import androidx.work.State;
+import androidx.work.WorkInfo;
 import androidx.work.impl.WorkDatabase;
 import androidx.work.impl.WorkManagerImpl;
 import androidx.work.impl.model.WorkSpecDao;
 
 /**
- * A {@link Runnable} that can stop work and set the {@link State} to {@link State#ENQUEUED} if it's
- * in {@link State#RUNNING}.
+ * A {@link Runnable} that can stop work and set the {@link WorkInfo.State} to
+ * {@link WorkInfo.State#ENQUEUED} if it's in {@link WorkInfo.State#RUNNING}.
  * @hide
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -48,8 +48,8 @@
         WorkSpecDao workSpecDao = workDatabase.workSpecDao();
         workDatabase.beginTransaction();
         try {
-            if (workSpecDao.getState(mWorkSpecId) == State.RUNNING) {
-                workSpecDao.setState(State.ENQUEUED, mWorkSpecId);
+            if (workSpecDao.getState(mWorkSpecId) == WorkInfo.State.RUNNING) {
+                workSpecDao.setState(WorkInfo.State.ENQUEUED, mWorkSpecId);
             }
             boolean isStopped = mWorkManagerImpl.getProcessor().stopWork(mWorkSpecId);
             Logger.debug(
diff --git a/work/workmanager/src/main/java/androidx/work/impl/workers/ConstraintTrackingWorker.java b/work/workmanager/src/main/java/androidx/work/impl/workers/ConstraintTrackingWorker.java
index 522937c..f31e463 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/workers/ConstraintTrackingWorker.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/workers/ConstraintTrackingWorker.java
@@ -177,11 +177,11 @@
     }
 
     @Override
-    public void onStopped(boolean cancelled) {
-        super.onStopped(cancelled);
+    public void onStopped() {
+        super.onStopped();
         if (mDelegate != null) {
             // Stop is the method that sets the stopped and cancelled bits and invokes onStopped.
-            mDelegate.stop(cancelled);
+            mDelegate.stop();
         }
     }