Merge "A few API naming/annotation tweaks:" into androidx-master-dev
diff --git a/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatCheckBoxSpy.java b/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatCheckBoxSpy.java
deleted file mode 100644
index e66492e..0000000
--- a/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatCheckBoxSpy.java
+++ /dev/null
@@ -1,43 +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.appcompat.widget;
-
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-
-import androidx.annotation.VisibleForTesting;
-
-/**
- * CompoundDrawable.getButtonDrawable() method was only added in API23. This class exposes the
- * mButton drawable for testing.
- */
-public class AppCompatCheckBoxSpy extends AppCompatCheckBox {
-
-    @VisibleForTesting
-    Drawable mButton;
-
-    public AppCompatCheckBoxSpy(Context context, AttributeSet attrs) {
-        super(context, attrs);
-    }
-
-    @Override
-    public void setButtonDrawable(Drawable buttonDrawable) {
-        super.setButtonDrawable(buttonDrawable);
-        mButton = buttonDrawable;
-    }
-}
diff --git a/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatCheckBoxTest.java b/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatCheckBoxTest.java
index a3bc312..a427189 100644
--- a/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatCheckBoxTest.java
+++ b/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatCheckBoxTest.java
@@ -28,6 +28,7 @@
 import androidx.appcompat.graphics.drawable.AnimatedStateListDrawableCompat;
 import androidx.appcompat.test.R;
 import androidx.core.content.res.ResourcesCompat;
+import androidx.core.widget.CompoundButtonCompat;
 import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
 import androidx.test.rule.ActivityTestRule;
@@ -68,8 +69,8 @@
     @Test
     public void testDefaultButton_isAnimated() {
         // Given an ACCB with the theme's button drawable
-        final AppCompatCheckBoxSpy checkBox = mContainer.findViewById(R.id.checkbox_button_compat);
-        final Drawable button = checkBox.mButton;
+        final AppCompatCheckBox checkBox = mContainer.findViewById(R.id.checkbox_button_compat);
+        final Drawable button = CompoundButtonCompat.getButtonDrawable(checkBox);
 
         // Then this drawable should be an animated-selector
         assertTrue(button instanceof AnimatedStateListDrawableCompat
@@ -82,9 +83,9 @@
     @Test
     public void testNullCompatButton() {
         // Given an ACCB which specifies a null app:buttonCompat
-        final AppCompatCheckBoxSpy checkBox = mContainer.findViewById(
+        final AppCompatCheckBox checkBox = mContainer.findViewById(
                 R.id.checkbox_null_button_compat);
-        final Drawable button = checkBox.mButton;
+        final Drawable button = CompoundButtonCompat.getButtonDrawable(checkBox);
         boolean isAnimated = button instanceof AnimatedStateListDrawableCompat;
 
         // Then the drawable should be present but not animated
diff --git a/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatRadioButtonSpy.java b/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatRadioButtonSpy.java
deleted file mode 100644
index 7f0ce6d..0000000
--- a/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatRadioButtonSpy.java
+++ /dev/null
@@ -1,43 +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.appcompat.widget;
-
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-
-import androidx.annotation.VisibleForTesting;
-
-/**
- * CompoundDrawable.getButtonDrawable() method was only added in API23. This class exposes the
- * mButton drawable for testing.
- */
-public class AppCompatRadioButtonSpy extends AppCompatRadioButton {
-
-    @VisibleForTesting
-    Drawable mButton;
-
-    public AppCompatRadioButtonSpy(Context context, AttributeSet attrs) {
-        super(context, attrs);
-    }
-
-    @Override
-    public void setButtonDrawable(Drawable buttonDrawable) {
-        super.setButtonDrawable(buttonDrawable);
-        mButton = buttonDrawable;
-    }
-}
diff --git a/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatRadioButtonTest.java b/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatRadioButtonTest.java
index ee60157..6d8b6b2 100644
--- a/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatRadioButtonTest.java
+++ b/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatRadioButtonTest.java
@@ -28,6 +28,7 @@
 import androidx.appcompat.graphics.drawable.AnimatedStateListDrawableCompat;
 import androidx.appcompat.test.R;
 import androidx.core.content.res.ResourcesCompat;
+import androidx.core.widget.CompoundButtonCompat;
 import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
 import androidx.test.rule.ActivityTestRule;
@@ -68,9 +69,9 @@
     @Test
     public void testDefaultButton_isAnimated() {
         // Given an ACRB with the theme's button drawable
-        final AppCompatRadioButtonSpy radio = mContainer.findViewById(
+        final AppCompatRadioButton radio = mContainer.findViewById(
                 R.id.radiobutton_button_compat);
-        final Drawable button = radio.mButton;
+        final Drawable button = CompoundButtonCompat.getButtonDrawable(radio);
 
         // Then this drawable should be an animated-selector
         assertTrue(button instanceof AnimatedStateListDrawableCompat
@@ -83,9 +84,9 @@
     @Test
     public void testNullCompatButton() {
         // Given an ACRB which specifies a null app:buttonCompat
-        final AppCompatRadioButtonSpy radio = mContainer.findViewById(
+        final AppCompatRadioButton radio = mContainer.findViewById(
                 R.id.radiobutton_null_button_compat);
-        final Drawable button = radio.mButton;
+        final Drawable button = CompoundButtonCompat.getButtonDrawable(radio);
         boolean isAnimated = button instanceof AnimatedStateListDrawableCompat;
 
         // Then the drawable should be present but not animated
diff --git a/appcompat/src/androidTest/res/layout/appcompat_checkbox_activity.xml b/appcompat/src/androidTest/res/layout/appcompat_checkbox_activity.xml
index 81187e1..73d9d16 100644
--- a/appcompat/src/androidTest/res/layout/appcompat_checkbox_activity.xml
+++ b/appcompat/src/androidTest/res/layout/appcompat_checkbox_activity.xml
@@ -29,12 +29,12 @@
         android:text="@string/sample_text1"
         app:fontFamily="@font/samplefont" />
 
-    <androidx.appcompat.widget.AppCompatCheckBoxSpy
+    <androidx.appcompat.widget.AppCompatCheckBox
         android:id="@+id/checkbox_button_compat"
         android:layout_width="match_parent"
         android:layout_height="wrap_content" />
 
-    <androidx.appcompat.widget.AppCompatCheckBoxSpy
+    <androidx.appcompat.widget.AppCompatCheckBox
         android:id="@+id/checkbox_null_button_compat"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
diff --git a/appcompat/src/androidTest/res/layout/appcompat_radiobutton_activity.xml b/appcompat/src/androidTest/res/layout/appcompat_radiobutton_activity.xml
index 82f6a18..a2627dd 100644
--- a/appcompat/src/androidTest/res/layout/appcompat_radiobutton_activity.xml
+++ b/appcompat/src/androidTest/res/layout/appcompat_radiobutton_activity.xml
@@ -29,12 +29,12 @@
             android:text="@string/sample_text1"
             app:fontFamily="@font/samplefont"/>
 
-    <androidx.appcompat.widget.AppCompatRadioButtonSpy
+    <androidx.appcompat.widget.AppCompatRadioButton
             android:id="@+id/radiobutton_button_compat"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"/>
 
-    <androidx.appcompat.widget.AppCompatRadioButtonSpy
+    <androidx.appcompat.widget.AppCompatRadioButton
             android:id="@+id/radiobutton_null_button_compat"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
diff --git a/fragment/test/api/current.txt b/fragment/test/api/current.txt
deleted file mode 100644
index 4e7c46b..0000000
--- a/fragment/test/api/current.txt
+++ /dev/null
@@ -1,21 +0,0 @@
-// Signature format: 2.0
-package androidx.fragment.app.test {
-
-  public final class FragmentScenario<F extends androidx.fragment.app.Fragment> {
-    method public static <F extends androidx.fragment.app.Fragment> androidx.fragment.app.test.FragmentScenario<F> launch(Class<F>);
-    method public static <F extends androidx.fragment.app.Fragment> androidx.fragment.app.test.FragmentScenario<F> launch(Class<F>, android.os.Bundle?);
-    method public static <F extends androidx.fragment.app.Fragment> androidx.fragment.app.test.FragmentScenario<F> launch(Class<F>, android.os.Bundle?, androidx.fragment.app.FragmentFactory?);
-    method public static <F extends androidx.fragment.app.Fragment> androidx.fragment.app.test.FragmentScenario<F> launchInContainer(Class<F>);
-    method public static <F extends androidx.fragment.app.Fragment> androidx.fragment.app.test.FragmentScenario<F> launchInContainer(Class<F>, android.os.Bundle?);
-    method public static <F extends androidx.fragment.app.Fragment> androidx.fragment.app.test.FragmentScenario<F> launchInContainer(Class<F>, android.os.Bundle?, androidx.fragment.app.FragmentFactory?);
-    method public androidx.fragment.app.test.FragmentScenario<F> moveToState(androidx.lifecycle.Lifecycle.State);
-    method public androidx.fragment.app.test.FragmentScenario<F> onFragment(androidx.fragment.app.test.FragmentScenario.FragmentAction<F>);
-    method public androidx.fragment.app.test.FragmentScenario<F> recreate();
-  }
-
-  public static interface FragmentScenario.FragmentAction<F extends androidx.fragment.app.Fragment> {
-    method public void perform(F);
-  }
-
-}
-
diff --git a/fragment/testing/api/current.txt b/fragment/testing/api/current.txt
new file mode 100644
index 0000000..db0c92e
--- /dev/null
+++ b/fragment/testing/api/current.txt
@@ -0,0 +1,21 @@
+// Signature format: 2.0
+package androidx.fragment.app.testing {
+
+  public final class FragmentScenario<F extends androidx.fragment.app.Fragment> {
+    method public static <F extends androidx.fragment.app.Fragment> androidx.fragment.app.testing.FragmentScenario<F> launch(Class<F>);
+    method public static <F extends androidx.fragment.app.Fragment> androidx.fragment.app.testing.FragmentScenario<F> launch(Class<F>, android.os.Bundle?);
+    method public static <F extends androidx.fragment.app.Fragment> androidx.fragment.app.testing.FragmentScenario<F> launch(Class<F>, android.os.Bundle?, androidx.fragment.app.FragmentFactory?);
+    method public static <F extends androidx.fragment.app.Fragment> androidx.fragment.app.testing.FragmentScenario<F> launchInContainer(Class<F>);
+    method public static <F extends androidx.fragment.app.Fragment> androidx.fragment.app.testing.FragmentScenario<F> launchInContainer(Class<F>, android.os.Bundle?);
+    method public static <F extends androidx.fragment.app.Fragment> androidx.fragment.app.testing.FragmentScenario<F> launchInContainer(Class<F>, android.os.Bundle?, androidx.fragment.app.FragmentFactory?);
+    method public androidx.fragment.app.testing.FragmentScenario<F> moveToState(androidx.lifecycle.Lifecycle.State);
+    method public androidx.fragment.app.testing.FragmentScenario<F> onFragment(androidx.fragment.app.testing.FragmentScenario.FragmentAction<F>);
+    method public androidx.fragment.app.testing.FragmentScenario<F> recreate();
+  }
+
+  public static interface FragmentScenario.FragmentAction<F extends androidx.fragment.app.Fragment> {
+    method public void perform(F);
+  }
+
+}
+
diff --git a/fragment/test/build.gradle b/fragment/testing/build.gradle
similarity index 100%
rename from fragment/test/build.gradle
rename to fragment/testing/build.gradle
diff --git a/fragment/test/src/androidTest/AndroidManifest.xml b/fragment/testing/src/androidTest/AndroidManifest.xml
similarity index 93%
rename from fragment/test/src/androidTest/AndroidManifest.xml
rename to fragment/testing/src/androidTest/AndroidManifest.xml
index 4c52e72..215f70b 100644
--- a/fragment/test/src/androidTest/AndroidManifest.xml
+++ b/fragment/testing/src/androidTest/AndroidManifest.xml
@@ -15,6 +15,6 @@
   ~ limitations under the License.
   -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="androidx.fragment.test.test">
+          package="androidx.fragment.testing.test">
     <uses-sdk android:targetSdkVersion="${target-sdk-version}"/>
 </manifest>
diff --git a/fragment/test/src/androidTest/java/androidx/fragment/app/test/FragmentScenarioKotlinTest.kt b/fragment/testing/src/androidTest/java/androidx/fragment/app/testing/FragmentScenarioKotlinTest.kt
similarity index 97%
rename from fragment/test/src/androidTest/java/androidx/fragment/app/test/FragmentScenarioKotlinTest.kt
rename to fragment/testing/src/androidTest/java/androidx/fragment/app/testing/FragmentScenarioKotlinTest.kt
index 910bde2..132c344 100644
--- a/fragment/test/src/androidTest/java/androidx/fragment/app/test/FragmentScenarioKotlinTest.kt
+++ b/fragment/testing/src/androidTest/java/androidx/fragment/app/testing/FragmentScenarioKotlinTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.fragment.app.test
+package androidx.fragment.app.testing
 
 import androidx.lifecycle.Lifecycle.State
 import androidx.test.runner.AndroidJUnit4
diff --git a/fragment/test/src/androidTest/java/androidx/fragment/app/test/FragmentScenarioTest.java b/fragment/testing/src/androidTest/java/androidx/fragment/app/testing/FragmentScenarioTest.java
similarity index 99%
rename from fragment/test/src/androidTest/java/androidx/fragment/app/test/FragmentScenarioTest.java
rename to fragment/testing/src/androidTest/java/androidx/fragment/app/testing/FragmentScenarioTest.java
index eef02f1..bb2f2015 100644
--- a/fragment/test/src/androidTest/java/androidx/fragment/app/test/FragmentScenarioTest.java
+++ b/fragment/testing/src/androidTest/java/androidx/fragment/app/testing/FragmentScenarioTest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.fragment.app.test;
+package androidx.fragment.app.testing;
 
 import static com.google.common.truth.Truth.assertThat;
 
diff --git a/fragment/test/src/androidTest/java/androidx/fragment/app/test/StateRecordingFragment.java b/fragment/testing/src/androidTest/java/androidx/fragment/app/testing/StateRecordingFragment.java
similarity index 98%
rename from fragment/test/src/androidTest/java/androidx/fragment/app/test/StateRecordingFragment.java
rename to fragment/testing/src/androidTest/java/androidx/fragment/app/testing/StateRecordingFragment.java
index 85bc2b3..71c1dd4 100644
--- a/fragment/test/src/androidTest/java/androidx/fragment/app/test/StateRecordingFragment.java
+++ b/fragment/testing/src/androidTest/java/androidx/fragment/app/testing/StateRecordingFragment.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.fragment.app.test;
+package androidx.fragment.app.testing;
 
 import android.os.Bundle;
 import android.view.LayoutInflater;
diff --git a/fragment/test/src/main/AndroidManifest.xml b/fragment/testing/src/main/AndroidManifest.xml
similarity index 90%
rename from fragment/test/src/main/AndroidManifest.xml
rename to fragment/testing/src/main/AndroidManifest.xml
index 7c9f63f..0b3eb19 100644
--- a/fragment/test/src/main/AndroidManifest.xml
+++ b/fragment/testing/src/main/AndroidManifest.xml
@@ -16,13 +16,13 @@
 -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           xmlns:tools="http://schemas.android.com/tools"
-          package="androidx.fragment.test">
+          package="androidx.fragment.testing">
     <!-- TODO: Remove this override after androidx.test:core lowers the level -->
     <uses-sdk tools:overrideLibrary="androidx.test.core" />
     <uses-permission android:name="android.permission.REORDER_TASKS" />
     <application>
         <activity
-                android:name="androidx.fragment.app.test.FragmentScenario$EmptyFragmentActivity"
+                android:name="androidx.fragment.app.testing.FragmentScenario$EmptyFragmentActivity"
                 android:theme="@style/FragmentScenarioEmptyFragmentActivityTheme"
                 android:taskAffinity=""
                 android:multiprocess="true"
diff --git a/fragment/test/src/main/java/androidx/fragment/app/test/FragmentScenario.java b/fragment/testing/src/main/java/androidx/fragment/app/testing/FragmentScenario.java
similarity index 99%
rename from fragment/test/src/main/java/androidx/fragment/app/test/FragmentScenario.java
rename to fragment/testing/src/main/java/androidx/fragment/app/testing/FragmentScenario.java
index 355c7d8..4d05214 100644
--- a/fragment/test/src/main/java/androidx/fragment/app/test/FragmentScenario.java
+++ b/fragment/testing/src/main/java/androidx/fragment/app/testing/FragmentScenario.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.fragment.app.test;
+package androidx.fragment.app.testing;
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY;
 import static androidx.core.util.Preconditions.checkNotNull;
diff --git a/fragment/test/src/main/res/values/styles.xml b/fragment/testing/src/main/res/values/styles.xml
similarity index 100%
rename from fragment/test/src/main/res/values/styles.xml
rename to fragment/testing/src/main/res/values/styles.xml
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 d7a80e8..6bc07f6 100644
--- a/media-widget/src/androidTest/java/androidx/media/widget/VideoView2Test.java
+++ b/media-widget/src/androidTest/java/androidx/media/widget/VideoView2Test.java
@@ -36,6 +36,7 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.app.Activity;
 import android.app.Instrumentation;
@@ -45,7 +46,6 @@
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
-import android.os.ResultReceiver;
 import android.util.Log;
 import android.view.View;
 import android.view.WindowManager;
@@ -54,6 +54,7 @@
 import androidx.media.widget.test.R;
 import androidx.media2.FileMediaItem2;
 import androidx.media2.MediaController2;
+import androidx.media2.MediaController2.ControllerResult;
 import androidx.media2.MediaItem2;
 import androidx.media2.SessionCommand2;
 import androidx.media2.SessionCommandGroup2;
@@ -118,6 +119,11 @@
         checkAttachedToWindow();
 
         mControllerCallback = mock(MediaController2.ControllerCallback.class);
+        when(mControllerCallback.onCustomCommand(
+                nullable(MediaController2.class),
+                nullable(SessionCommand2.class),
+                nullable(Bundle.class))).thenReturn(
+                        new ControllerResult(ControllerResult.RESULT_CODE_SUCCESS, null));
         mController = new MediaController2(mVideoView.getContext(),
                 mVideoView.getMediaSessionToken2(), mMainHandlerExecutor, mControllerCallback);
     }
@@ -268,8 +274,7 @@
         verify(mControllerCallback, timeout(TIME_OUT).atLeastOnce()).onCustomCommand(
                 any(MediaController2.class),
                 argThat(new CommandMatcher(EVENT_UPDATE_TRACK_STATUS)),
-                argThat(new CommandArgumentMatcher(KEY_SUBTITLE_TRACK_COUNT, 2)),
-                nullable(ResultReceiver.class));
+                argThat(new CommandArgumentMatcher(KEY_SUBTITLE_TRACK_COUNT, 2)));
 
         // Select the first subtitle track
         Bundle extra = new Bundle();
@@ -279,8 +284,7 @@
         verify(mControllerCallback, timeout(TIME_OUT).atLeastOnce()).onCustomCommand(
                 any(MediaController2.class),
                 argThat(new CommandMatcher(EVENT_UPDATE_SUBTITLE_SELECTED)),
-                argThat(new CommandArgumentMatcher(KEY_SELECTED_SUBTITLE_INDEX, 0)),
-                nullable(ResultReceiver.class));
+                argThat(new CommandArgumentMatcher(KEY_SELECTED_SUBTITLE_INDEX, 0)));
 
         // Select the second subtitle track
         extra.putInt(KEY_SELECTED_SUBTITLE_INDEX, 1);
@@ -289,8 +293,7 @@
         verify(mControllerCallback, timeout(TIME_OUT).atLeastOnce()).onCustomCommand(
                 any(MediaController2.class),
                 argThat(new CommandMatcher(EVENT_UPDATE_SUBTITLE_SELECTED)),
-                argThat(new CommandArgumentMatcher(KEY_SELECTED_SUBTITLE_INDEX, 1)),
-                nullable(ResultReceiver.class));
+                argThat(new CommandArgumentMatcher(KEY_SELECTED_SUBTITLE_INDEX, 1)));
 
         // Deselect subtitle track
         mController.sendCustomCommand(
@@ -298,8 +301,7 @@
         verify(mControllerCallback, timeout(TIME_OUT).atLeastOnce()).onCustomCommand(
                 any(MediaController2.class),
                 argThat(new CommandMatcher(EVENT_UPDATE_SUBTITLE_DESELECTED)),
-                nullable(Bundle.class),
-                nullable(ResultReceiver.class));
+                nullable(Bundle.class));
     }
 
     class CommandMatcher implements ArgumentMatcher<SessionCommand2> {
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 74c4799..f473e96 100644
--- a/media-widget/src/main/java/androidx/media/widget/MediaControlView2.java
+++ b/media-widget/src/main/java/androidx/media/widget/MediaControlView2.java
@@ -16,6 +16,9 @@
 
 package androidx.media.widget;
 
+import static androidx.media2.MediaController2.ControllerResult.RESULT_CODE_NOT_SUPPORTED;
+import static androidx.media2.MediaController2.ControllerResult.RESULT_CODE_SUCCESS;
+
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
@@ -31,7 +34,6 @@
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
-import android.os.ResultReceiver;
 import android.util.AttributeSet;
 import android.util.DisplayMetrics;
 import android.util.Log;
@@ -2426,9 +2428,9 @@
             }
 
             @Override
-            public void onCustomCommand(@NonNull MediaController2 controller,
-                    @NonNull SessionCommand2 command, @Nullable Bundle args,
-                    @Nullable ResultReceiver receiver) {
+            public MediaController2.ControllerResult onCustomCommand(
+                    @NonNull MediaController2 controller, @NonNull SessionCommand2 command,
+                    @Nullable Bundle args) {
                 if (DEBUG) {
                     Log.d(TAG, "onCustomCommand(): command: " + command);
                 }
@@ -2519,7 +2521,11 @@
                             mSubSettingsAdapter.setCheckPosition(mSelectedSubtitleTrackIndex);
                         }
                         break;
+                    default:
+                        return new MediaController2.ControllerResult(
+                                RESULT_CODE_NOT_SUPPORTED, null);
                 }
+                return new MediaController2.ControllerResult(RESULT_CODE_SUCCESS, null);
             }
         }
     }
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 6cbef60..eb25eea 100644
--- a/media-widget/src/main/java/androidx/media/widget/VideoView2ImplBase.java
+++ b/media-widget/src/main/java/androidx/media/widget/VideoView2ImplBase.java
@@ -739,7 +739,7 @@
             Bundle data = new Bundle();
             data.putInt(MediaControlView2.KEY_SELECTED_SUBTITLE_INDEX,
                     mSubtitleTracks.indexOfKey(trackIndex));
-            mMediaSession.sendCustomCommand(
+            mMediaSession.broadcastCustomCommand(
                     new SessionCommand2(MediaControlView2.EVENT_UPDATE_SUBTITLE_SELECTED, null),
                     data);
         }
@@ -753,7 +753,7 @@
         mSelectedSubtitleTrackIndex = INVALID_TRACK_INDEX;
         mSubtitleAnchorView.setVisibility(View.GONE);
 
-        mMediaSession.sendCustomCommand(
+        mMediaSession.broadcastCustomCommand(
                 new SessionCommand2(MediaControlView2.EVENT_UPDATE_SUBTITLE_DESELECTED, null),
                 null);
     }
@@ -996,7 +996,7 @@
                     if (what == MediaPlayer2.MEDIA_INFO_METADATA_UPDATE) {
                         Bundle data = extractTrackInfoData();
                         if (data != null) {
-                            mMediaSession.sendCustomCommand(
+                            mMediaSession.broadcastCustomCommand(
                                     new SessionCommand2(MediaControlView2.EVENT_UPDATE_TRACK_STATUS,
                                             null), data);
                         }
@@ -1081,7 +1081,7 @@
                     if (mMediaSession != null) {
                         Bundle data = extractTrackInfoData();
                         if (data != null) {
-                            mMediaSession.sendCustomCommand(
+                            mMediaSession.broadcastCustomCommand(
                                     new SessionCommand2(MediaControlView2.EVENT_UPDATE_TRACK_STATUS,
                                             null), data);
                         }
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 9e98fcd..a32ab12 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
@@ -39,6 +39,7 @@
 import androidx.versionedparcelable.ParcelImpl;
 import androidx.versionedparcelable.ParcelUtils;
 
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -180,6 +181,19 @@
         }
 
         @Override
+        public void setPlaylistWithSize(String controllerId, int size, Bundle metadata)
+                throws RemoteException {
+            MediaController2 controller2 = mMediaController2Map.get(controllerId);
+            List<MediaItem2> list = new ArrayList<>();
+            MediaItem2.Builder builder = new MediaItem2.Builder(0 /* flags */);
+            for (int i = 0; i < size; i++) {
+                // Make media ID of each item same with its index.
+                list.add(builder.setMediaId(Integer.toString(i)).build());
+            }
+            controller2.setPlaylist(list, MediaMetadata2.fromBundle(metadata));
+        }
+
+        @Override
         public void setMediaItem(String controllerId, Bundle item) throws RemoteException {
             MediaController2 controller2 = mMediaController2Map.get(controllerId);
             controller2.setMediaItem(MediaItem2.fromBundle(item));
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 232c817..7a4c4ba 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
@@ -41,7 +41,6 @@
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.RemoteException;
-import android.os.ResultReceiver;
 import android.support.mediacompat.testlib.IRemoteMediaSession2;
 import android.support.v4.media.session.MediaSessionCompat;
 import android.util.Log;
@@ -222,20 +221,19 @@
         }
     }
 
-    public void sendCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args) {
+    public void broadcastCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args) {
         try {
-            mBinder.sendCustomCommand(mSessionId, command.toBundle(), args);
+            mBinder.broadcastCustomCommand(mSessionId, command.toBundle(), args);
         } catch (RemoteException ex) {
-            Log.e(TAG, "Failed to call sendCustomCommand()");
+            Log.e(TAG, "Failed to call broadcastCustomCommand()");
         }
     }
 
     public void sendCustomCommand(@NonNull ControllerInfo controller,
-            @NonNull SessionCommand2 command, @Nullable Bundle args,
-            @Nullable ResultReceiver receiver) {
+            @NonNull SessionCommand2 command, @Nullable Bundle args) {
         try {
             // TODO: ControllerInfo should be handled.
-            mBinder.sendCustomCommand2(mSessionId, null, command.toBundle(), args, receiver);
+            mBinder.sendCustomCommand(mSessionId, null, command.toBundle(), args);
         } catch (RemoteException ex) {
             Log.e(TAG, "Failed to call sendCustomCommand2()");
         }
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 7df9a04..86230da 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
@@ -42,7 +42,6 @@
 import android.content.Context;
 import android.os.Build;
 import android.os.Bundle;
-import android.os.ResultReceiver;
 
 import androidx.annotation.CallSuper;
 import androidx.annotation.GuardedBy;
@@ -550,19 +549,19 @@
         }
 
         @Override
-        public void onCustomCommand(MediaController2 controller, SessionCommand2 command,
-                Bundle args, ResultReceiver receiver) {
-            mCallbackProxy.onCustomCommand(controller, command, args, receiver);
+        public MediaController2.ControllerResult onCustomCommand(MediaController2 controller,
+                SessionCommand2 command, Bundle args) {
             synchronized (this) {
                 if (mOnCustomCommandRunnable != null) {
                     mOnCustomCommandRunnable.run();
                 }
             }
+            return mCallbackProxy.onCustomCommand(controller, command, args);
         }
 
         @Override
-        public void onCustomLayoutChanged(MediaController2 controller, List<CommandButton> layout) {
-            mCallbackProxy.onCustomLayoutChanged(controller, layout);
+        public int onSetCustomLayout(MediaController2 controller, List<CommandButton> layout) {
+            return mCallbackProxy.onSetCustomLayout(controller, layout);
         }
 
         @Override
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 ca5f3e9..97a1c19 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
@@ -23,7 +23,9 @@
 import static androidx.media.test.lib.CommonConstants.INDEX_FOR_NULL_ITEM;
 import static androidx.media.test.lib.CommonConstants.INDEX_FOR_UNKONWN_ITEM;
 import static androidx.media.test.lib.CommonConstants.MOCK_MEDIA_LIBRARY_SERVICE;
-import static androidx.media.test.lib.MediaSession2Constants.TEST_CONTROLLER_CALLBACK_SESSION_REJECTS;
+import static androidx.media.test.lib.MediaSession2Constants
+        .TEST_CONTROLLER_CALLBACK_SESSION_REJECTS;
+import static androidx.media2.MediaController2.ControllerResult.RESULT_CODE_SUCCESS;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -34,7 +36,6 @@
 import android.media.AudioManager;
 import android.os.Build;
 import android.os.Bundle;
-import android.os.ResultReceiver;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -604,21 +605,21 @@
         final MediaController2.ControllerCallback callback =
                 new MediaController2.ControllerCallback() {
             @Override
-            public void onCustomCommand(MediaController2 controller, SessionCommand2 command,
-                    Bundle args, ResultReceiver receiver) {
+            public MediaController2.ControllerResult onCustomCommand(MediaController2 controller,
+                    SessionCommand2 command, Bundle args) {
                 assertEquals(testCommand, command);
                 assertTrue(TestUtils.equals(testArgs, args));
-                assertNull(receiver);
                 latch.countDown();
+                return new MediaController2.ControllerResult(RESULT_CODE_SUCCESS, null);
             }
         };
         MediaController2 controller = createController(mRemoteSession2.getToken(), true, callback);
 
         // TODO(jaewan): Test with multiple controllers
-        mRemoteSession2.sendCustomCommand(testCommand, testArgs);
+        mRemoteSession2.broadcastCustomCommand(testCommand, testArgs);
 
         // TODO(jaewan): Test receivers as well.
-        mRemoteSession2.sendCustomCommand(TEST_CONTROLLER_INFO, testCommand, testArgs, null);
+        mRemoteSession2.sendCustomCommand(TEST_CONTROLLER_INFO, testCommand, testArgs);
         assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
     }
 
@@ -637,7 +638,7 @@
         final MediaController2.ControllerCallback callback =
                 new MediaController2.ControllerCallback() {
             @Override
-            public void onCustomLayoutChanged(MediaController2 controller2,
+            public int onSetCustomLayout(MediaController2 controller2,
                     List<MediaSession2.CommandButton> layout) {
                 assertEquals(layout.size(), buttons.size());
                 for (int i = 0; i < layout.size(); i++) {
@@ -645,6 +646,7 @@
                     assertEquals(layout.get(i).getDisplayName(), buttons.get(i).getDisplayName());
                 }
                 latch.countDown();
+                return RESULT_CODE_SUCCESS;
             }
         };
         final MediaController2 controller =
@@ -701,7 +703,7 @@
         });
         SessionCommand2 customCommand = new SessionCommand2("testNoInteraction", null);
 
-        mRemoteSession2.sendCustomCommand(customCommand, null);
+        mRemoteSession2.broadcastCustomCommand(customCommand, null);
 
         assertFalse(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
         setRunnableForOnCustomCommand(mController, null);
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/tests/MediaController2Test_copied.java b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/tests/MediaController2Test_copied.java
index c186b6c..14ea357 100644
--- a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/tests/MediaController2Test_copied.java
+++ b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/tests/MediaController2Test_copied.java
@@ -94,7 +94,7 @@
 //        assertTrue(mPlayer.mPlayCalled);
 //
 //        // Test command from session service to controller.
-//        mSession.sendCustomCommand(testCommand, null);
+//        mSession.broadcastCustomCommand(testCommand, null);
 //        assertTrue(controllerLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
 //    }
 
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/tests/MediaSession2TestBase.java b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/tests/MediaSession2TestBase.java
index 60d7894b..b170a98 100644
--- a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/tests/MediaSession2TestBase.java
+++ b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/tests/MediaSession2TestBase.java
@@ -24,7 +24,6 @@
 import android.os.Bundle;
 import android.os.HandlerThread;
 import android.os.Looper;
-import android.os.ResultReceiver;
 import android.support.v4.media.session.MediaSessionCompat;
 
 import androidx.annotation.CallSuper;
@@ -306,14 +305,14 @@
         }
 
         @Override
-        public void onCustomCommand(MediaController2 controller, SessionCommand2 command,
-                Bundle args, ResultReceiver receiver) {
-            mCallbackProxy.onCustomCommand(controller, command, args, receiver);
+        public MediaController2.ControllerResult onCustomCommand(MediaController2 controller,
+                SessionCommand2 command, Bundle args) {
             synchronized (this) {
                 if (mOnCustomCommandRunnable != null) {
                     mOnCustomCommandRunnable.run();
                 }
             }
+            return mCallbackProxy.onCustomCommand(controller, command, args);
         }
 
         @Override
@@ -323,8 +322,8 @@
         }
 
         @Override
-        public void onCustomLayoutChanged(MediaController2 controller, List<CommandButton> layout) {
-            mCallbackProxy.onCustomLayoutChanged(controller, layout);
+        public int onSetCustomLayout(MediaController2 controller, List<CommandButton> layout) {
+            return mCallbackProxy.onSetCustomLayout(controller, layout);
         }
 
         @Override
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 60443542..8aa5815 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
@@ -31,9 +31,11 @@
 import static androidx.media.test.lib.CommonConstants.KEY_PLAYLIST;
 import static androidx.media.test.lib.CommonConstants.KEY_SPEED;
 import static androidx.media.test.lib.CommonConstants.KEY_VOLUME_CONTROL_TYPE;
-import static androidx.media.test.lib.MediaSession2Constants.TEST_CONTROLLER_CALLBACK_SESSION_REJECTS;
+import static androidx.media.test.lib.MediaSession2Constants
+        .TEST_CONTROLLER_CALLBACK_SESSION_REJECTS;
 import static androidx.media.test.lib.MediaSession2Constants.TEST_GET_SESSION_ACTIVITY;
-import static androidx.media.test.lib.MediaSession2Constants.TEST_ON_PLAYLIST_METADATA_CHANGED_SESSION_SET_PLAYLIST;
+import static androidx.media.test.lib.MediaSession2Constants
+        .TEST_ON_PLAYLIST_METADATA_CHANGED_SESSION_SET_PLAYLIST;
 
 import android.app.PendingIntent;
 import android.app.Service;
@@ -41,7 +43,6 @@
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.RemoteException;
-import android.os.ResultReceiver;
 import android.support.mediacompat.testlib.IRemoteMediaSession2;
 import android.util.Log;
 
@@ -225,18 +226,18 @@
         }
 
         @Override
-        public void sendCustomCommand(String sessionId, Bundle command, Bundle args)
+        public void broadcastCustomCommand(String sessionId, Bundle command, Bundle args)
                 throws RemoteException {
             MediaSession2 session2 = mSession2Map.get(sessionId);
-            session2.sendCustomCommand(SessionCommand2.fromBundle(command), args);
+            session2.broadcastCustomCommand(SessionCommand2.fromBundle(command), args);
         }
 
         @Override
-        public void sendCustomCommand2(String sessionId, Bundle controller, Bundle command,
-                Bundle args, ResultReceiver receiver) throws RemoteException {
+        public void sendCustomCommand(String sessionId, Bundle controller, Bundle command,
+                Bundle args) throws RemoteException {
             MediaSession2 session2 = mSession2Map.get(sessionId);
             ControllerInfo info = MediaTestUtils.getTestControllerInfo(session2);
-            session2.sendCustomCommand(info, SessionCommand2.fromBundle(command), args, receiver);
+            session2.sendCustomCommand(info, SessionCommand2.fromBundle(command), args);
         }
 
         @Override
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 aa13ba3..f168d89 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
@@ -148,6 +148,22 @@
         }
     }
 
+    /**
+     * Client app will automatically create a playlist of size {@param size},
+     * and call setPlaylist() with the list. Each item's media ID will be its index.
+     *
+     * Note: This is introduced for testing large data transaction. It can prevent test helper
+     *       classes from sending/receiving large data between them.
+     */
+    public void setPlaylistWithSize(int size, @Nullable MediaMetadata2 metadata) {
+        try {
+            mBinder.setPlaylistWithSize(mControllerId, size,
+                    metadata == null ? null : metadata.toBundle());
+        } catch (RemoteException ex) {
+            Log.e(TAG, "Failed to call setPlaylistWithSize()");
+        }
+    }
+
     public void setMediaItem(@NonNull MediaItem2 item) {
         try {
             mBinder.setMediaItem(mControllerId, item.toBundle());
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 b5a7eea..83b7b79 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
@@ -35,6 +35,7 @@
 import androidx.media2.MediaSession2;
 import androidx.media2.SessionCommandGroup2;
 import androidx.media2.SessionPlayer2;
+import androidx.test.filters.LargeTest;
 import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -211,6 +212,25 @@
     }
 
     @Test
+    @LargeTest
+    public void testSetPlaylistByControllerWithLongPlaylist() throws InterruptedException {
+        final int listSize = 5000;
+        // Make client app to generate a long list, and call setPlaylist() with it.
+        mController2.setPlaylistWithSize(listSize, null /* metadata */);
+        assertTrue(mPlayer.mCountDownLatch.await(10, TimeUnit.SECONDS));
+
+        assertTrue(mPlayer.mSetPlaylistCalled);
+        assertNull(mPlayer.mMetadata);
+
+        assertNotNull(mPlayer.mPlaylist);
+        assertEquals(listSize, mPlayer.mPlaylist.size());
+        for (int i = 0; i < listSize; i++) {
+            // Each item's media ID will be same as its index.
+            assertEquals(Integer.toString(i), mPlayer.mPlaylist.get(i).getMediaId());
+        }
+    }
+
+    @Test
     public void testUpdatePlaylistMetadataBySession() {
         prepareLooper();
         final MediaMetadata2 testMetadata = MediaTestUtils.createMetadata();
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 ff2094e..31610ba 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
@@ -34,6 +34,7 @@
     void seekTo(String controllerId, long pos);
     void setPlaybackSpeed(String controllerId, float speed);
     void setPlaylist(String controllerId, in List<Bundle> list, in Bundle metadata);
+    void setPlaylistWithSize(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);
diff --git a/media/version-compat-tests/lib/src/main/aidl/android/support/mediacompat/testlib/IRemoteMediaSession2.aidl b/media/version-compat-tests/lib/src/main/aidl/android/support/mediacompat/testlib/IRemoteMediaSession2.aidl
index f028d6b..05232b0 100644
--- a/media/version-compat-tests/lib/src/main/aidl/android/support/mediacompat/testlib/IRemoteMediaSession2.aidl
+++ b/media/version-compat-tests/lib/src/main/aidl/android/support/mediacompat/testlib/IRemoteMediaSession2.aidl
@@ -29,9 +29,9 @@
     ParcelImpl getToken(String sessionId);
     Bundle getCompatToken(String sessionId);
     void updatePlayer(String sessionId, in Bundle playerBundle);
-    void sendCustomCommand(String sessionId, in Bundle command, in Bundle args);
-    void sendCustomCommand2(String sessionId, in Bundle controller, in Bundle command,
-            in Bundle args, in ResultReceiver receiver);
+    void broadcastCustomCommand(String sessionId, in Bundle command, in Bundle args);
+    void sendCustomCommand(String sessionId, in Bundle controller, in Bundle command,
+            in Bundle args);
     void close(String sessionId);
     void setAllowedCommands(String sessionId, in Bundle controller, in Bundle commands);
     void notifyRoutesInfoChanged(String sessionId, in Bundle controller, in List<Bundle> routes);
diff --git a/media2/api/current.txt b/media2/api/current.txt
index 49c12cb..3c0964c 100644
--- a/media2/api/current.txt
+++ b/media2/api/current.txt
@@ -105,8 +105,7 @@
     method public void onBufferingStateChanged(androidx.media2.MediaController2, androidx.media2.MediaItem2, int);
     method public void onConnected(androidx.media2.MediaController2, androidx.media2.SessionCommandGroup2);
     method public void onCurrentMediaItemChanged(androidx.media2.MediaController2, androidx.media2.MediaItem2);
-    method public void onCustomCommand(androidx.media2.MediaController2, androidx.media2.SessionCommand2, android.os.Bundle, android.os.ResultReceiver);
-    method public void onCustomLayoutChanged(androidx.media2.MediaController2, java.util.List<androidx.media2.MediaSession2.CommandButton>);
+    method public androidx.media2.MediaController2.ControllerResult onCustomCommand(androidx.media2.MediaController2, androidx.media2.SessionCommand2, android.os.Bundle);
     method public void onDisconnected(androidx.media2.MediaController2);
     method public void onPlaybackCompleted(androidx.media2.MediaController2);
     method public void onPlaybackInfoChanged(androidx.media2.MediaController2, androidx.media2.MediaController2.PlaybackInfo);
@@ -116,10 +115,11 @@
     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);
+    method public int onSetCustomLayout(androidx.media2.MediaController2, java.util.List<androidx.media2.MediaSession2.CommandButton>);
     method public void onShuffleModeChanged(androidx.media2.MediaController2, int);
   }
 
-  public static class MediaController2.ControllerResult {
+  public static class MediaController2.ControllerResult implements androidx.versionedparcelable.VersionedParcelable {
     ctor public MediaController2.ControllerResult(int, android.os.Bundle);
     method public long getCompletionTime();
     method public android.os.Bundle getCustomCommandResult();
@@ -282,15 +282,15 @@
   }
 
   public class MediaSession2 implements java.lang.AutoCloseable {
+    method public void broadcastCustomCommand(androidx.media2.SessionCommand2, android.os.Bundle);
     method public void close();
     method public java.util.List<androidx.media2.MediaSession2.ControllerInfo> getConnectedControllers();
     method public java.lang.String getId();
     method public androidx.media2.SessionPlayer2 getPlayer();
     method public androidx.media2.SessionToken2 getToken();
-    method public void sendCustomCommand(androidx.media2.SessionCommand2, android.os.Bundle);
-    method public void sendCustomCommand(androidx.media2.MediaSession2.ControllerInfo, androidx.media2.SessionCommand2, android.os.Bundle, android.os.ResultReceiver);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaSession2.SessionResult> sendCustomCommand(androidx.media2.MediaSession2.ControllerInfo, androidx.media2.SessionCommand2, android.os.Bundle);
     method public void setAllowedCommands(androidx.media2.MediaSession2.ControllerInfo, androidx.media2.SessionCommandGroup2);
-    method public void setCustomLayout(androidx.media2.MediaSession2.ControllerInfo, java.util.List<androidx.media2.MediaSession2.CommandButton>);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.MediaSession2.SessionResult> setCustomLayout(androidx.media2.MediaSession2.ControllerInfo, java.util.List<androidx.media2.MediaSession2.CommandButton>);
     method public void updatePlayer(androidx.media2.SessionPlayer2);
   }
 
diff --git a/media2/src/androidTest/java/androidx/media2/MediaController2Test.java b/media2/src/androidTest/java/androidx/media2/MediaController2Test.java
index 84fc933..89c045f 100644
--- a/media2/src/androidTest/java/androidx/media2/MediaController2Test.java
+++ b/media2/src/androidTest/java/androidx/media2/MediaController2Test.java
@@ -36,7 +36,6 @@
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Process;
-import android.os.ResultReceiver;
 
 import androidx.annotation.NonNull;
 import androidx.media.AudioAttributesCompat;
@@ -1331,11 +1330,12 @@
         mController = createController(TestUtils.getServiceToken(mContext, id), true,
                 new ControllerCallback() {
                     @Override
-                    public void onCustomCommand(MediaController2 controller,
-                            SessionCommand2 command, Bundle args, ResultReceiver receiver) {
+                    public MediaController2.ControllerResult onCustomCommand(
+                            MediaController2 controller, SessionCommand2 command, Bundle args) {
                         if (testCommand.equals(command)) {
                             controllerLatch.countDown();
                         }
+                        return new MediaController2.ControllerResult(RESULT_CODE_SUCCESS);
                     }
                 }
         );
@@ -1347,7 +1347,7 @@
         assertTrue(mPlayer.mPlayCalled);
 
         // Test command from session service to controller.
-        mSession.sendCustomCommand(testCommand, null);
+        mSession.broadcastCustomCommand(testCommand, null);
         assertTrue(controllerLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
     }
 
@@ -1553,7 +1553,7 @@
             }
         });
         SessionCommand2 customCommand = new SessionCommand2("testNoInteraction", null);
-        mSession.sendCustomCommand(customCommand, null);
+        mSession.broadcastCustomCommand(customCommand, null);
         assertFalse(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
         setRunnableForOnCustomCommand(mController, null);
     }
diff --git a/media2/src/androidTest/java/androidx/media2/MediaSession2Test.java b/media2/src/androidTest/java/androidx/media2/MediaSession2Test.java
index b19e0d5..aa802b0 100644
--- a/media2/src/androidTest/java/androidx/media2/MediaSession2Test.java
+++ b/media2/src/androidTest/java/androidx/media2/MediaSession2Test.java
@@ -36,7 +36,6 @@
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Process;
-import android.os.ResultReceiver;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -510,8 +509,8 @@
 
             final ControllerCallback callback = new ControllerCallback() {
                 @Override
-                public void onCustomLayoutChanged(MediaController2 controller2,
-                        List<CommandButton> layout) {
+                public int onSetCustomLayout(
+                        MediaController2 controller2, List<CommandButton> layout) {
                     assertEquals(customLayout.size(), layout.size());
                     for (int i = 0; i < layout.size(); i++) {
                         assertEquals(customLayout.get(i).getCommand(), layout.get(i).getCommand());
@@ -519,6 +518,7 @@
                                 layout.get(i).getDisplayName());
                     }
                     latch.countDown();
+                    return RESULT_CODE_SUCCESS;
                 }
             };
             MediaController2 controller = createController(session.getToken(), true, callback);
@@ -572,23 +572,23 @@
         final CountDownLatch latch = new CountDownLatch(2);
         final ControllerCallback callback = new ControllerCallback() {
             @Override
-            public void onCustomCommand(MediaController2 controller, SessionCommand2 command,
-                    Bundle args, ResultReceiver receiver) {
+            public MediaController2.ControllerResult onCustomCommand(MediaController2 controller,
+                    SessionCommand2 command, Bundle args) {
                 assertEquals(testCommand, command);
                 assertTrue(TestUtils.equals(testArgs, args));
-                assertNull(receiver);
                 latch.countDown();
+                return new MediaController2.ControllerResult(RESULT_CODE_SUCCESS);
             }
         };
         final MediaController2 controller =
                 createController(mSession.getToken(), true, callback);
         // TODO(jaewan): Test with multiple controllers
-        mSession.sendCustomCommand(testCommand, testArgs);
+        mSession.broadcastCustomCommand(testCommand, testArgs);
 
         ControllerInfo controllerInfo = getTestControllerInfo();
         assertNotNull(controllerInfo);
         // TODO(jaewan): Test receivers as well.
-        mSession.sendCustomCommand(controllerInfo, testCommand, testArgs, null);
+        mSession.sendCustomCommand(controllerInfo, testCommand, testArgs);
         assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
     }
 
diff --git a/media2/src/androidTest/java/androidx/media2/MockBrowserCallback.java b/media2/src/androidTest/java/androidx/media2/MockBrowserCallback.java
index c9036fd..8230299 100644
--- a/media2/src/androidTest/java/androidx/media2/MockBrowserCallback.java
+++ b/media2/src/androidTest/java/androidx/media2/MockBrowserCallback.java
@@ -20,7 +20,6 @@
 import static junit.framework.Assert.assertTrue;
 
 import android.os.Bundle;
-import android.os.ResultReceiver;
 
 import androidx.annotation.CallSuper;
 import androidx.annotation.GuardedBy;
@@ -97,19 +96,19 @@
     }
 
     @Override
-    public void onCustomCommand(MediaController2 controller, SessionCommand2 command,
-            Bundle args, ResultReceiver receiver) {
-        mCallbackProxy.onCustomCommand(controller, command, args, receiver);
+    public MediaController2.ControllerResult onCustomCommand(
+            MediaController2 controller, SessionCommand2 command, Bundle args) {
         synchronized (this) {
             if (mOnCustomCommandRunnable != null) {
                 mOnCustomCommandRunnable.run();
             }
         }
+        return mCallbackProxy.onCustomCommand(controller, command, args);
     }
 
     @Override
-    public void onCustomLayoutChanged(MediaController2 controller, List<CommandButton> layout) {
-        mCallbackProxy.onCustomLayoutChanged(controller, layout);
+    public int onSetCustomLayout(MediaController2 controller, List<CommandButton> layout) {
+        return mCallbackProxy.onSetCustomLayout(controller, layout);
     }
 
     @Override
diff --git a/media2/src/androidTest/java/androidx/media2/MockControllerCallback.java b/media2/src/androidTest/java/androidx/media2/MockControllerCallback.java
index 2fa5678..181a30d 100644
--- a/media2/src/androidTest/java/androidx/media2/MockControllerCallback.java
+++ b/media2/src/androidTest/java/androidx/media2/MockControllerCallback.java
@@ -20,7 +20,6 @@
 import static junit.framework.Assert.assertTrue;
 
 import android.os.Bundle;
-import android.os.ResultReceiver;
 
 import androidx.annotation.CallSuper;
 import androidx.annotation.GuardedBy;
@@ -91,14 +90,14 @@
     }
 
     @Override
-    public void onCustomCommand(MediaController2 controller, SessionCommand2 command,
-            Bundle args, ResultReceiver receiver) {
-        mCallbackProxy.onCustomCommand(controller, command, args, receiver);
+    public MediaController2.ControllerResult onCustomCommand(MediaController2 controller,
+            SessionCommand2 command, Bundle args) {
         synchronized (this) {
             if (mOnCustomCommandRunnable != null) {
                 mOnCustomCommandRunnable.run();
             }
         }
+        return mCallbackProxy.onCustomCommand(controller, command, args);
     }
 
     @Override
@@ -108,8 +107,8 @@
     }
 
     @Override
-    public void onCustomLayoutChanged(MediaController2 controller, List<CommandButton> layout) {
-        mCallbackProxy.onCustomLayoutChanged(controller, layout);
+    public int onSetCustomLayout(MediaController2 controller, List<CommandButton> layout) {
+        return mCallbackProxy.onSetCustomLayout(controller, layout);
     }
 
     @Override
diff --git a/media2/src/main/aidl/androidx/media2/IMediaController2.aidl b/media2/src/main/aidl/androidx/media2/IMediaController2.aidl
index 88d00de..463fe29 100644
--- a/media2/src/main/aidl/androidx/media2/IMediaController2.aidl
+++ b/media2/src/main/aidl/androidx/media2/IMediaController2.aidl
@@ -18,7 +18,6 @@
 
 import android.app.PendingIntent;
 import android.os.Bundle;
-import android.os.ResultReceiver;
 
 import androidx.media2.IMediaSession2;
 import androidx.versionedparcelable.ParcelImpl;
@@ -31,8 +30,6 @@
  * @hide
  */
 oneway interface IMediaController2 {
-    void onSessionResult(int seq, in ParcelImpl sessionResult) = 24;
-
     void onCurrentMediaItemChanged(in ParcelImpl item) = 0;
     void onPlayerStateChanged(long eventTimeMs, long positionMs, int state) = 1;
     void onPlaybackSpeedChanged(long eventTimeMs, long positionMs, float speed) = 2;
@@ -52,10 +49,12 @@
         in List<ParcelImpl> playlist, in PendingIntent sessionActivity) = 12;
     void onDisconnected() = 13;
 
-    void onCustomLayoutChanged(in List<ParcelImpl> commandButtonlist) = 14;
+    void onSetCustomLayout(int seq, in List<ParcelImpl> commandButtonlist) = 14;
     void onAllowedCommandsChanged(in ParcelImpl commandGroup) = 15;
 
-    void onCustomCommand(in ParcelImpl command, in Bundle args, in ResultReceiver receiver) = 16;
+    void onCustomCommand(int seq, in ParcelImpl command, in Bundle args) = 16;
+
+    void onSessionResult(int seq, in ParcelImpl sessionResult) = 24;
 
     //////////////////////////////////////////////////////////////////////////////////////////////
     // Browser sepcific
diff --git a/media2/src/main/aidl/androidx/media2/IMediaSession2.aidl b/media2/src/main/aidl/androidx/media2/IMediaSession2.aidl
index e23473d..4b43cdc 100644
--- a/media2/src/main/aidl/androidx/media2/IMediaSession2.aidl
+++ b/media2/src/main/aidl/androidx/media2/IMediaSession2.aidl
@@ -17,10 +17,10 @@
 package androidx.media2;
 
 import android.os.Bundle;
-import android.os.ResultReceiver;
 import android.net.Uri;
 
 import androidx.media2.IMediaController2;
+import androidx.media2.ParcelImplListSlice;
 import androidx.versionedparcelable.ParcelImpl;
 
 /**
@@ -55,7 +55,7 @@
     void setRating(IMediaController2 caller, int seq, String mediaId, in ParcelImpl rating2) = 18;
     void setPlaybackSpeed(IMediaController2 caller, int seq, float speed) = 19;
 
-    void setPlaylist(IMediaController2 caller, int seq, in List<ParcelImpl> playlist,
+    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;
@@ -74,6 +74,9 @@
     void unsubscribeRoutesInfo(IMediaController2 caller, int seq) = 31;
     void selectRoute(IMediaController2 caller, int seq, in Bundle route) = 32;
 
+    void onControllerResult(IMediaController2 caller, int seq,
+            in ParcelImpl controllerResult) = 41;
+
     //////////////////////////////////////////////////////////////////////////////////////////////
     // library service specific
     //////////////////////////////////////////////////////////////////////////////////////////////
@@ -86,5 +89,5 @@
             in Bundle extras) = 37;
     void subscribe(IMediaController2 caller, String parentId, in Bundle extras) = 38;
     void unsubscribe(IMediaController2 caller, String parentId) = 39;
-    // Next Id : 41
+    // Next Id : 42
 }
diff --git a/media2/src/main/aidl/androidx/media2/ParcelImplListSlice.aidl b/media2/src/main/aidl/androidx/media2/ParcelImplListSlice.aidl
new file mode 100644
index 0000000..a69c19b
--- /dev/null
+++ b/media2/src/main/aidl/androidx/media2/ParcelImplListSlice.aidl
@@ -0,0 +1,3 @@
+package androidx.media2;
+
+parcelable ParcelImplListSlice;
diff --git a/media2/src/main/java/androidx/media2/ConnectedControllersManager.java b/media2/src/main/java/androidx/media2/ConnectedControllersManager.java
index d61a1b8..9174e1b5e 100644
--- a/media2/src/main/java/androidx/media2/ConnectedControllersManager.java
+++ b/media2/src/main/java/androidx/media2/ConnectedControllersManager.java
@@ -16,9 +16,12 @@
 
 package androidx.media2;
 
+import static androidx.media2.MediaSession2.SessionResult.RESULT_CODE_SKIPPED;
+
 import android.util.Log;
 
 import androidx.annotation.GuardedBy;
+import androidx.annotation.Nullable;
 import androidx.collection.ArrayMap;
 import androidx.media.MediaSessionManager.RemoteUserInfo;
 import androidx.media2.MediaSession2.ControllerInfo;
@@ -40,6 +43,9 @@
     private final ArrayMap<ControllerInfo, SessionCommandGroup2> mAllowedCommandGroupMap =
             new ArrayMap<>();
     @GuardedBy("mLock")
+    private final ArrayMap<ControllerInfo, SequencedFutureManager>
+            mControllerToSequencedFutureManager = new ArrayMap<>();
+    @GuardedBy("mLock")
     private final ArrayMap<T, ControllerInfo> mControllers = new ArrayMap<>();
     @GuardedBy("mLock")
     private final ArrayMap<ControllerInfo, T> mKeys = new ArrayMap<>();
@@ -60,6 +66,8 @@
         }
         synchronized (mLock) {
             mAllowedCommandGroupMap.put(controller, commands);
+            mControllerToSequencedFutureManager.put(controller, new SequencedFutureManager(
+                    new MediaSession2.SessionResult(RESULT_CODE_SKIPPED)));
             mControllers.put(key, controller);
             mKeys.put(controller, key);
         }
@@ -85,10 +93,15 @@
             return;
         }
         final ControllerInfo controller;
+        final SequencedFutureManager manager;
         synchronized (mLock) {
             controller = mControllers.remove(key);
             mKeys.remove(controller);
             mAllowedCommandGroupMap.remove(controller);
+            manager = mControllerToSequencedFutureManager.remove(controller);
+        }
+        if (manager != null) {
+            manager.close();
         }
         notifyDisconnected(controller);
     }
@@ -97,10 +110,15 @@
         if (controller == null) {
             return;
         }
+        final SequencedFutureManager manager;
         synchronized (mLock) {
             T key = mKeys.remove(controller);
             mControllers.remove(key);
             mAllowedCommandGroupMap.remove(controller);
+            manager = mControllerToSequencedFutureManager.remove(controller);
+        }
+        if (manager != null) {
+            manager.close();
         }
         notifyDisconnected(controller);
     }
@@ -136,7 +154,38 @@
 
     public boolean isConnected(ControllerInfo controller) {
         synchronized (mLock) {
-            return mKeys.get(controller) != null;
+            return controller != null && mKeys.get(controller) != null;
+        }
+    }
+
+    /**
+     * Gets the sequenced future manager.
+     *
+     * @param controller controller
+     * @return sequenced future manager. Can be {@code null} if the controller was null or
+     *         disconencted.
+     */
+    public @Nullable SequencedFutureManager getSequencedFutureManager(
+            @Nullable ControllerInfo controller) {
+        if (controller == null) {
+            return null;
+        }
+        synchronized (mLock) {
+            return isConnected(controller)
+                    ? mControllerToSequencedFutureManager.get(controller) : null;
+        }
+    }
+
+    /**
+     * Gets the sequenced future manager.
+     *
+     * @param key key
+     * @return sequenced future manager. Can be {@code null} if the controller was null or
+     *         disconencted.
+     */
+    public @Nullable SequencedFutureManager getSequencedFutureManager(@Nullable T key) {
+        synchronized (mLock) {
+            return getSequencedFutureManager(getController(key));
         }
     }
 
diff --git a/media2/src/main/java/androidx/media2/MediaController2.java b/media2/src/main/java/androidx/media2/MediaController2.java
index 4a36b8d..d1ff028 100644
--- a/media2/src/main/java/androidx/media2/MediaController2.java
+++ b/media2/src/main/java/androidx/media2/MediaController2.java
@@ -1110,12 +1110,16 @@
          * <p>
          * Can be called before {@link #onConnected(MediaController2, SessionCommandGroup2)}
          * is called.
+         * <p>
+         * Default implementation returns {@link ControllerResult#RESULT_CODE_NOT_SUPPORTED}.
          *
          * @param controller the controller for this event
          * @param layout
          */
-        public void onCustomLayoutChanged(@NonNull MediaController2 controller,
-                @NonNull List<CommandButton> layout) { }
+        public @ControllerResult.ResultCode int onSetCustomLayout(
+                @NonNull MediaController2 controller, @NonNull List<CommandButton> layout) {
+            return ControllerResult.RESULT_CODE_NOT_SUPPORTED;
+        }
 
         /**
          * Called when the session has changed anything related with the {@link PlaybackInfo}.
@@ -1150,16 +1154,21 @@
                 @NonNull SessionCommandGroup2 commands) { }
 
         /**
-         * Called when the session sent a custom command.
+         * Called when the session sent a custom command. Returns a {@link ControllerResult} for
+         * session to get notification back. If the {@code null} is returned,
+         * {@link ControllerResult#RESULT_CODE_UNKNOWN_ERROR} will be returned.
+         * <p>
+         * Default implementation returns {@link ControllerResult#RESULT_CODE_NOT_SUPPORTED}.
          *
          * @param controller the controller for this event
          * @param command
          * @param args
-         * @param receiver
+         * @return result of handling custom command
          */
-        public void onCustomCommand(@NonNull MediaController2 controller,
-                @NonNull SessionCommand2 command, @Nullable Bundle args,
-                @Nullable ResultReceiver receiver) { }
+        public @NonNull ControllerResult onCustomCommand(@NonNull MediaController2 controller,
+                @NonNull SessionCommand2 command, @Nullable Bundle args) {
+            return new ControllerResult(ControllerResult.RESULT_CODE_NOT_SUPPORTED);
+        }
 
         /**
          * Called when the player state is changed.
@@ -1423,7 +1432,7 @@
      * Result class to be used with {@link ListenableFuture} for asynchronous calls.
      */
     @VersionedParcelize
-    public static class ControllerResult implements RemoteResult2 {
+    public static class ControllerResult implements RemoteResult2, VersionedParcelable {
         /**
          * Result code representing that the command is successfully completed.
          * <p>
@@ -1467,6 +1476,13 @@
         @ParcelField(4)
         MediaItem2 mItem;
 
+        /**
+         * Constructor to be used by
+         * {@link ControllerCallback#onCustomCommand(MediaController2, SessionCommand2, Bundle)}.
+         *
+         * @param resultCode result code
+         * @param customCommandResult custom command result
+         */
         public ControllerResult(@ResultCode int resultCode, @Nullable Bundle customCommandResult) {
             this(resultCode, customCommandResult, null);
         }
@@ -1477,7 +1493,7 @@
         }
 
         ControllerResult(@ResultCode int resultCode) {
-            this(resultCode, null);
+            this(resultCode, null, null);
         }
 
         ControllerResult(@ResultCode int resultCode, @Nullable Bundle customCommandResult,
diff --git a/media2/src/main/java/androidx/media2/MediaController2ImplBase.java b/media2/src/main/java/androidx/media2/MediaController2ImplBase.java
index 3fb680d..6aadd37 100644
--- a/media2/src/main/java/androidx/media2/MediaController2ImplBase.java
+++ b/media2/src/main/java/androidx/media2/MediaController2ImplBase.java
@@ -19,6 +19,7 @@
 import static androidx.media2.MediaController2.ControllerResult.RESULT_CODE_DISCONNECTED;
 import static androidx.media2.MediaController2.ControllerResult.RESULT_CODE_PERMISSION_DENIED;
 import static androidx.media2.MediaController2.ControllerResult.RESULT_CODE_SKIPPED;
+import static androidx.media2.MediaController2.ControllerResult.RESULT_CODE_UNKNOWN_ERROR;
 import static androidx.media2.MediaMetadata2.METADATA_KEY_DURATION;
 import static androidx.media2.SessionCommand2.COMMAND_CODE_CUSTOM;
 import static androidx.media2.SessionCommand2.COMMAND_CODE_PLAYER_ADD_PLAYLIST_ITEM;
@@ -64,7 +65,6 @@
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.RemoteException;
-import android.os.ResultReceiver;
 import android.os.SystemClock;
 import android.support.v4.media.MediaBrowserCompat;
 import android.util.Log;
@@ -72,7 +72,6 @@
 import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.concurrent.futures.ResolvableFuture;
 import androidx.media2.MediaController2.ControllerCallback;
 import androidx.media2.MediaController2.ControllerResult;
 import androidx.media2.MediaController2.MediaController2Impl;
@@ -92,6 +91,8 @@
 import java.util.concurrent.Executor;
 
 class MediaController2ImplBase implements MediaController2Impl {
+    private static final boolean THROW_EXCEPTION_FOR_NULL_RESULT = true;
+
     static final String TAG = "MC2ImplBase";
     static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
@@ -275,9 +276,7 @@
             // Don't create Future with SequencedFutureManager.
             // Otherwise session would receive discontinued sequence number, and it would make
             // future work item 'keeping call sequence when session execute commands' impossible.
-            final ResolvableFuture<ControllerResult> result = ResolvableFuture.create();
-            result.set(new ControllerResult(RESULT_CODE_PERMISSION_DENIED));
-            return result;
+            return ControllerResult.createFutureWithResult(RESULT_CODE_PERMISSION_DENIED);
         }
     }
 
@@ -575,7 +574,8 @@
             @Override
             public void run(IMediaSession2 iSession2, int seq) throws RemoteException {
                 iSession2.setPlaylist(mControllerStub, seq,
-                        MediaUtils2.convertMediaItem2ListToParcelImplList(list),
+                        new ParcelImplListSlice(
+                                MediaUtils2.convertMediaItem2ListToParcelImplList(list)),
                         (metadata == null) ? null : metadata.toBundle());
             }
         });
@@ -1103,15 +1103,40 @@
         }
     }
 
-    void onCustomCommand(final SessionCommand2 command, final Bundle args,
-            final ResultReceiver receiver) {
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    void sendControllerResult(int seq, @NonNull ControllerResult result) {
+        final IMediaSession2 iSession2;
+        synchronized (mLock) {
+            iSession2 = mISession2;
+        }
+        if (iSession2 == null) {
+            return;
+        }
+        try {
+            iSession2.onControllerResult(mControllerStub, seq,
+                    (ParcelImpl) ParcelUtils.toParcelable(result));
+        } catch (RemoteException e) {
+            Log.w(TAG, "Error in sending");
+        }
+    }
+
+    void onCustomCommand(final int seq, final SessionCommand2 command, final Bundle args) {
         if (DEBUG) {
-            Log.d(TAG, "onCustomCommand cmd=" + command);
+            Log.d(TAG, "onCustomCommand cmd=" + command.getCustomCommand());
         }
         mCallbackExecutor.execute(new Runnable() {
             @Override
             public void run() {
-                mCallback.onCustomCommand(mInstance, command, args, receiver);
+                ControllerResult result = mCallback.onCustomCommand(mInstance, command, args);
+                if (result == null) {
+                    if (THROW_EXCEPTION_FOR_NULL_RESULT) {
+                        throw new RuntimeException("ControllerCallback#onCustomCommand() has"
+                                + " returned null, command=" + command.getCustomCommand());
+                    } else {
+                        result = new ControllerResult(RESULT_CODE_UNKNOWN_ERROR);
+                    }
+                }
+                sendControllerResult(seq, result);
             }
         });
     }
@@ -1125,11 +1150,13 @@
         });
     }
 
-    void onCustomLayoutChanged(final List<MediaSession2.CommandButton> layout) {
+    void onSetCustomLayout(final int seq, final List<MediaSession2.CommandButton> layout) {
         mCallbackExecutor.execute(new Runnable() {
             @Override
             public void run() {
-                mCallback.onCustomLayoutChanged(mInstance, layout);
+                int resultCode = mCallback.onSetCustomLayout(mInstance, layout);
+                ControllerResult result = new ControllerResult(resultCode);
+                sendControllerResult(seq, result);
             }
         });
     }
diff --git a/media2/src/main/java/androidx/media2/MediaController2ImplLegacy.java b/media2/src/main/java/androidx/media2/MediaController2ImplLegacy.java
index c40f0d0..f58ce66 100644
--- a/media2/src/main/java/androidx/media2/MediaController2ImplLegacy.java
+++ b/media2/src/main/java/androidx/media2/MediaController2ImplLegacy.java
@@ -1067,8 +1067,8 @@
             mCallbackExecutor.execute(new Runnable() {
                 @Override
                 public void run() {
-                    mCallback.onCustomCommand(mInstance, new SessionCommand2(event, null), extras,
-                            null);
+                    // Ignore return because legacy session cannot get result back.
+                    mCallback.onCustomCommand(mInstance, new SessionCommand2(event, null), extras);
                 }
             });
         }
@@ -1199,7 +1199,7 @@
                 public void run() {
                     mCallback.onCustomCommand(mInstance,
                             new SessionCommand2(SESSION_COMMAND_ON_EXTRA_CHANGED, null),
-                            extras, null);
+                            extras);
                 }
             });
         }
@@ -1221,7 +1221,7 @@
                 public void run() {
                     mCallback.onCustomCommand(mInstance,
                             new SessionCommand2(SESSION_COMMAND_ON_CAPTIONING_ENABLED_CHANGED,
-                                    null), null, null);
+                                    null), null);
                 }
             });
         }
diff --git a/media2/src/main/java/androidx/media2/MediaController2Stub.java b/media2/src/main/java/androidx/media2/MediaController2Stub.java
index 9644a40..36e67b9 100644
--- a/media2/src/main/java/androidx/media2/MediaController2Stub.java
+++ b/media2/src/main/java/androidx/media2/MediaController2Stub.java
@@ -18,7 +18,6 @@
 
 import android.app.PendingIntent;
 import android.os.Bundle;
-import android.os.ResultReceiver;
 import android.text.TextUtils;
 import android.util.Log;
 
@@ -272,9 +271,9 @@
     }
 
     @Override
-    public void onCustomLayoutChanged(List<ParcelImpl> commandButtonlist) {
-        if (commandButtonlist == null) {
-            Log.w(TAG, "onCustomLayoutChanged(): Ignoring null commandButtonlist");
+    public void onSetCustomLayout(int seq, List<ParcelImpl> commandButtonList) {
+        if (commandButtonList == null) {
+            Log.w(TAG, "setCustomLayout(): Ignoring null commandButtonList");
             return;
         }
         final MediaController2ImplBase controller;
@@ -289,13 +288,13 @@
             return;
         }
         List<CommandButton> layout = new ArrayList<>();
-        for (int i = 0; i < commandButtonlist.size(); i++) {
-            CommandButton button = ParcelUtils.fromParcelable(commandButtonlist.get(i));
+        for (int i = 0; i < commandButtonList.size(); i++) {
+            CommandButton button = ParcelUtils.fromParcelable(commandButtonList.get(i));
             if (button != null) {
                 layout.add(button);
             }
         }
-        controller.onCustomLayoutChanged(layout);
+        controller.onSetCustomLayout(seq, layout);
     }
 
     @Override
@@ -320,7 +319,7 @@
     }
 
     @Override
-    public void onCustomCommand(ParcelImpl commandParcel, Bundle args, ResultReceiver receiver) {
+    public void onCustomCommand(int seq, ParcelImpl commandParcel, Bundle args) {
         final MediaController2ImplBase controller;
         try {
             controller = getController();
@@ -330,10 +329,10 @@
         }
         SessionCommand2 command = ParcelUtils.fromParcelable(commandParcel);
         if (command == null) {
-            Log.w(TAG, "onCustomCommand(): Ignoring null command");
+            Log.w(TAG, "sendCustomCommand(): Ignoring null command");
             return;
         }
-        controller.onCustomCommand(command, args, receiver);
+        controller.onCustomCommand(seq, command, args);
     }
 
     ////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/media2/src/main/java/androidx/media2/MediaLibraryService2LegacyStub.java b/media2/src/main/java/androidx/media2/MediaLibraryService2LegacyStub.java
index e528e6ec..17fdc10 100644
--- a/media2/src/main/java/androidx/media2/MediaLibraryService2LegacyStub.java
+++ b/media2/src/main/java/androidx/media2/MediaLibraryService2LegacyStub.java
@@ -24,7 +24,6 @@
 import android.os.Bundle;
 import android.os.Process;
 import android.os.RemoteException;
-import android.os.ResultReceiver;
 import android.support.v4.media.MediaBrowserCompat;
 import android.support.v4.media.MediaBrowserCompat.MediaItem;
 import android.support.v4.media.session.MediaSessionCompat;
@@ -334,7 +333,7 @@
         }
 
         @Override
-        final void onCustomLayoutChanged(List<CommandButton> layout) throws RemoteException {
+        final void setCustomLayout(int seq, List<CommandButton> layout) throws RemoteException {
             // No-op. BrowserCompat doesn't understand Controller features.
         }
 
@@ -349,7 +348,7 @@
         }
 
         @Override
-        final void onCustomCommand(SessionCommand2 command, Bundle args, ResultReceiver receiver)
+        final void sendCustomCommand(int seq, SessionCommand2 command, Bundle args)
                 throws RemoteException {
             // No-op. BrowserCompat doesn't understand Controller features.
         }
diff --git a/media2/src/main/java/androidx/media2/MediaSession2.java b/media2/src/main/java/androidx/media2/MediaSession2.java
index 8acfbe8..c6b1ec2 100644
--- a/media2/src/main/java/androidx/media2/MediaSession2.java
+++ b/media2/src/main/java/androidx/media2/MediaSession2.java
@@ -30,7 +30,6 @@
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.RemoteException;
-import android.os.ResultReceiver;
 import android.os.SystemClock;
 import android.support.v4.media.session.MediaSessionCompat;
 import android.support.v4.media.session.PlaybackStateCompat;
@@ -40,9 +39,11 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
+import androidx.concurrent.futures.ResolvableFuture;
 import androidx.core.content.ContextCompat;
 import androidx.core.util.ObjectsCompat;
 import androidx.media.MediaSessionManager.RemoteUserInfo;
+import androidx.media2.MediaController2.ControllerResult;
 import androidx.media2.MediaController2.PlaybackInfo;
 import androidx.media2.MediaSession2.SessionResult.ResultCode;
 import androidx.media2.SessionPlayer2.BuffState;
@@ -128,7 +129,7 @@
  *             {@link SessionPlayer2#PLAYER_STATE_PLAYING}</li>
  *             <li>{@link SessionPlayer2#play()} otherwise</li></ul>
  *             <li>For a double tap, {@link SessionPlayer2#skipToNextPlaylistItem()}</li></ul></td>
- *     </td>
+ *     </tr>
  * </table>
  * @see MediaSessionService2
  */
@@ -235,15 +236,23 @@
      * Sets ordered list of {@link CommandButton} for controllers to build UI with it.
      * <p>
      * It's up to controller's decision how to represent the layout in its own UI.
-     * Here's the same way
-     * (layout[i] means a CommandButton at index i in the given list)
-     * For 5 icons row
-     *      layout[3] layout[1] layout[0] layout[2] layout[4]
-     * For 3 icons row
-     *      layout[1] layout[0] layout[2]
-     * For 5 icons row with overflow icon (can show +5 extra buttons with overflow button)
-     *      expanded row:   layout[5] layout[6] layout[7] layout[8] layout[9]
-     *      main row:       layout[3] layout[1] layout[0] layout[2] layout[4]
+     * Here are some examples.
+     * <p>
+     * Note: <code>layout[i]</code> means a CommandButton at index i in the given list
+     * <table>
+     * <tr><th>Controller UX layout</th><th>Layout example</th></tr>
+     * <tr><td>Row with 3 icons</td>
+     *     <td><code>layout[1]</code> <code>layout[0]</code> <code>layout[2]</code></td></tr>
+     * <tr><td>Row with 5 icons</td>
+     *     <td><code>layout[3]</code> <code>layout[1]</code> <code>layout[0]</code>
+     *         <code>layout[2]</code> <code>layout[4]</code></td></tr>
+     * <tr><td rowspan=2>Row with 5 icons and an overflow icon, and another expandable row with 5
+     *         extra icons</td>
+     *     <td><code>layout[3]</code> <code>layout[1]</code> <code>layout[0]</code>
+     *         <code>layout[2]</code> <code>layout[4]</code></td></tr>
+     * <tr><td><code>layout[3]</code> <code>layout[1]</code> <code>layout[0]</code>
+     *         <code>layout[2]</code> <code>layout[4]</code></td></tr>
+     * </table>
      * <p>
      * This API can be called in the
      * {@link SessionCallback#onConnect(MediaSession2, ControllerInfo)}.
@@ -251,13 +260,18 @@
      * @param controller controller to specify layout.
      * @param layout ordered list of layout.
      */
-    public void setCustomLayout(@NonNull ControllerInfo controller,
-            @NonNull List<CommandButton> layout) {
-        mImpl.setCustomLayout(controller, layout);
+    public @NonNull ListenableFuture<SessionResult> setCustomLayout(
+            @NonNull ControllerInfo controller, @NonNull List<CommandButton> layout) {
+        return mImpl.setCustomLayout(controller, layout);
     }
 
     /**
-     * Set the new allowed command group for the controller
+     * Sets the new allowed command group for the controller.
+     * <p>
+     * This is synchronous call. Changes in the allowed commands take effect immediately regardless
+     * of the controller notified about the change through
+     * {@link MediaController2.ControllerCallback
+     * #onAllowedCommandsChanged(MediaController2, SessionCommandGroup2)}
      *
      * @param controller controller to change allowed commands
      * @param commands new allowed commands
@@ -268,13 +282,17 @@
     }
 
     /**
-     * Send custom command to all connected controllers.
+     * Broadcasts custom command to all connected controllers.
+     * <p>
+     * This is synchronous call and doesn't wait for result from the controller. Use
+     * {@link #sendCustomCommand(ControllerInfo, SessionCommand2, Bundle)} for getting the result.
      *
      * @param command a command
      * @param args optional argument
+     * @see #sendCustomCommand(ControllerInfo, SessionCommand2, Bundle)
      */
-    public void sendCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args) {
-        mImpl.sendCustomCommand(command, args);
+    public void broadcastCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args) {
+        mImpl.broadcastCustomCommand(command, args);
     }
 
     /**
@@ -282,12 +300,12 @@
      *
      * @param command a command
      * @param args optional argument
-     * @param receiver result receiver for the session
+     * @see #broadcastCustomCommand(SessionCommand2, Bundle)
      */
-    public void sendCustomCommand(@NonNull ControllerInfo controller,
-            @NonNull SessionCommand2 command, @Nullable Bundle args,
-            @Nullable ResultReceiver receiver) {
-        mImpl.sendCustomCommand(controller, command, args, receiver);
+    public @NonNull ListenableFuture<SessionResult> sendCustomCommand(
+            @NonNull ControllerInfo controller, @NonNull SessionCommand2 command,
+            @Nullable Bundle args) {
+        return mImpl.sendCustomCommand(controller, command, args);
     }
 
     /**
@@ -1097,18 +1115,19 @@
         }
     }
 
+    // TODO: Drop 'Cb' from the name.
     abstract static class ControllerCb {
         abstract void onPlayerResult(int seq, PlayerResult result) throws RemoteException;
         abstract void onSessionResult(int seq, SessionResult result) throws RemoteException;
 
         // Mostly matched with the methods in MediaController2.ControllerCallback
-        abstract void onCustomLayoutChanged(@NonNull List<CommandButton> layout)
+        abstract void setCustomLayout(int seq, @NonNull List<CommandButton> layout)
                 throws RemoteException;
+        abstract void sendCustomCommand(int seq, @NonNull SessionCommand2 command,
+                @Nullable Bundle args) throws RemoteException;
         abstract void onPlaybackInfoChanged(@NonNull PlaybackInfo info) throws RemoteException;
         abstract void onAllowedCommandsChanged(@NonNull SessionCommandGroup2 commands)
                 throws RemoteException;
-        abstract void onCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args,
-                @Nullable ResultReceiver receiver) throws RemoteException;
         abstract void onPlayerStateChanged(long eventTimeMs, long positionMs, int playerState)
                 throws RemoteException;
         abstract void onPlaybackSpeedChanged(long eventTimeMs, long positionMs, float speed)
@@ -1155,14 +1174,13 @@
         @NonNull List<ControllerInfo> getConnectedControllers();
         boolean isConnected(@NonNull ControllerInfo controller);
 
-        void setCustomLayout(@NonNull ControllerInfo controller,
+        ListenableFuture<SessionResult> setCustomLayout(@NonNull ControllerInfo controller,
                 @NonNull List<CommandButton> layout);
         void setAllowedCommands(@NonNull ControllerInfo controller,
                 @NonNull SessionCommandGroup2 commands);
-        void sendCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args);
-        void sendCustomCommand(@NonNull ControllerInfo controller,
-                @NonNull SessionCommand2 command, @Nullable Bundle args,
-                @Nullable ResultReceiver receiver);
+        void broadcastCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args);
+        ListenableFuture<SessionResult> sendCustomCommand(@NonNull ControllerInfo controller,
+                @NonNull SessionCommand2 command, @Nullable Bundle args);
         void notifyRoutesInfoChanged(@NonNull ControllerInfo controller,
                 @Nullable List<Bundle> routes);
 
@@ -1355,6 +1373,20 @@
                     result.getCompletionTime());
         }
 
+        static @Nullable SessionResult from(@Nullable ControllerResult result) {
+            if (result == null) {
+                return null;
+            }
+            return new SessionResult(result.getResultCode(), result.getCustomCommandResult(),
+                    result.getMediaItem(), result.getCompletionTime());
+        }
+
+        static ListenableFuture<SessionResult> createFutureWithResult(@ResultCode int resultCode) {
+            ResolvableFuture<SessionResult> result = ResolvableFuture.create();
+            result.set(new SessionResult(resultCode));
+            return result;
+        }
+
         /**
          * Gets the result code.
          *
@@ -1382,11 +1414,12 @@
         }
 
         /**
-         * Gets the result of {@link #sendCustomCommand(SessionCommand2, Bundle)}. This is only
-         * valid when it's returned by the {@link #sendCustomCommand(SessionCommand2, Bundle)} and
-         * will be {@code null} otherwise.
+         * Gets the result of {@link #sendCustomCommand(ControllerInfo, SessionCommand2, Bundle)}.
+         * This is only valid when it's returned by the
+         * {@link #sendCustomCommand(ControllerInfo, SessionCommand2, Bundle)} and will be
+         * {@code null} otherwise.
          *
-         * @see #sendCustomCommand(SessionCommand2, Bundle)
+         * @see #sendCustomCommand(ControllerInfo, SessionCommand2, Bundle)
          * @return result of send custom command
          */
         public @Nullable Bundle getCustomCommandResult() {
diff --git a/media2/src/main/java/androidx/media2/MediaSession2ImplBase.java b/media2/src/main/java/androidx/media2/MediaSession2ImplBase.java
index fd6178a..9380f12 100644
--- a/media2/src/main/java/androidx/media2/MediaSession2ImplBase.java
+++ b/media2/src/main/java/androidx/media2/MediaSession2ImplBase.java
@@ -19,7 +19,10 @@
 import static androidx.media2.MediaSession2.ControllerCb;
 import static androidx.media2.MediaSession2.ControllerInfo;
 import static androidx.media2.MediaSession2.SessionCallback;
+import static androidx.media2.MediaSession2.SessionResult.RESULT_CODE_DISCONNECTED;
 import static androidx.media2.MediaSession2.SessionResult.RESULT_CODE_INVALID_STATE;
+import static androidx.media2.MediaSession2.SessionResult.RESULT_CODE_SUCCESS;
+import static androidx.media2.MediaSession2.SessionResult.RESULT_CODE_UNKNOWN_ERROR;
 import static androidx.media2.MediaUtils2.DIRECT_EXECUTOR;
 import static androidx.media2.SessionPlayer2.PLAYER_STATE_IDLE;
 import static androidx.media2.SessionPlayer2.UNKNOWN_TIME;
@@ -38,7 +41,6 @@
 import android.os.IBinder;
 import android.os.Process;
 import android.os.RemoteException;
-import android.os.ResultReceiver;
 import android.os.SystemClock;
 import android.support.v4.media.session.MediaSessionCompat;
 import android.support.v4.media.session.MediaSessionCompat.Token;
@@ -56,6 +58,8 @@
 import androidx.media.VolumeProviderCompat;
 import androidx.media2.MediaController2.PlaybackInfo;
 import androidx.media2.MediaSession2.MediaSession2Impl;
+import androidx.media2.MediaSession2.SessionResult;
+import androidx.media2.SequencedFutureManager.SequencedFuture;
 import androidx.media2.SessionPlayer2.PlayerResult;
 
 import com.google.common.util.concurrent.ListenableFuture;
@@ -329,6 +333,9 @@
 
     @Override
     public boolean isConnected(ControllerInfo controller) {
+        if (controller == null) {
+            return false;
+        }
         if (controller.equals(mSessionLegacyStub.getControllersForAll())) {
             return true;
         }
@@ -337,7 +344,7 @@
     }
 
     @Override
-    public void setCustomLayout(@NonNull ControllerInfo controller,
+    public ListenableFuture<SessionResult> setCustomLayout(@NonNull ControllerInfo controller,
             @NonNull final List<MediaSession2.CommandButton> layout) {
         if (controller == null) {
             throw new IllegalArgumentException("controller shouldn't be null");
@@ -345,10 +352,10 @@
         if (layout == null) {
             throw new IllegalArgumentException("layout shouldn't be null");
         }
-        notifyToController(controller, new NotifyRunnable() {
+        return sendCommand(controller, new ControllerCommand() {
             @Override
-            public void run(ControllerCb callback) throws RemoteException {
-                callback.onCustomLayoutChanged(layout);
+            public void run(int seq, ControllerCb controller) throws RemoteException {
+                controller.setCustomLayout(seq, layout);
             }
         });
     }
@@ -379,33 +386,33 @@
     }
 
     @Override
-    public void sendCustomCommand(@NonNull final SessionCommand2 command,
+    public void broadcastCustomCommand(@NonNull final SessionCommand2 command,
             @Nullable final Bundle args) {
         if (command == null) {
             throw new IllegalArgumentException("command shouldn't be null");
         }
-        notifyToAllControllers(new NotifyRunnable() {
+        broadcastCommand(new ControllerCommand() {
             @Override
-            public void run(ControllerCb callback) throws RemoteException {
-                callback.onCustomCommand(command, args, null);
+            public void run(int seq, ControllerCb controller) throws RemoteException {
+                controller.sendCustomCommand(seq, command, args);
             }
         });
     }
 
     @Override
-    public void sendCustomCommand(@NonNull ControllerInfo controller,
-            @NonNull final SessionCommand2 command, @Nullable final Bundle args,
-            @Nullable final ResultReceiver receiver) {
+    public ListenableFuture<SessionResult> sendCustomCommand(
+            @NonNull ControllerInfo controller, @NonNull final SessionCommand2 command,
+            @Nullable final Bundle args) {
         if (controller == null) {
             throw new IllegalArgumentException("controller shouldn't be null");
         }
         if (command == null) {
             throw new IllegalArgumentException("command shouldn't be null");
         }
-        notifyToController(controller, new NotifyRunnable() {
+        return sendCommand(controller, new ControllerCommand() {
             @Override
-            public void run(ControllerCb callback) throws RemoteException {
-                callback.onCustomCommand(command, args, receiver);
+            public void run(int seq, ControllerCb controller) throws RemoteException {
+                controller.sendCustomCommand(seq, command, args);
             }
         });
     }
@@ -1018,23 +1025,14 @@
 
     void notifyToController(@NonNull final ControllerInfo controller,
             @NonNull NotifyRunnable runnable) {
-        if (controller == null) {
-            return;
-        }
         if (!isConnected(controller)) {
             // Do not send command to an unconnected controller.
             return;
         }
-
         try {
             runnable.run(controller.getControllerCb());
         } catch (DeadObjectException e) {
-            if (DEBUG) {
-                Log.d(TAG, controller.toString() + " is gone", e);
-            }
-            // Note: Only removing from MediaSession2Stub would be fine for now, because other
-            //       (legacy) stubs wouldn't throw DeadObjectException.
-            mSession2Stub.getConnectedControllersManager().removeController(controller);
+            onDeadObjectException(controller, e);
         } catch (RemoteException e) {
             // Currently it's TransactionTooLargeException or DeadSystemException.
             // We'd better to leave log for those cases because
@@ -1055,6 +1053,90 @@
         notifyToController(controller, runnable);
     }
 
+    private ListenableFuture<SessionResult> sendCommand(@NonNull ControllerInfo controller,
+            @NonNull ControllerCommand command) {
+        if (!isConnected(controller)) {
+            return SessionResult.createFutureWithResult(RESULT_CODE_DISCONNECTED);
+        }
+        try {
+            final ListenableFuture<SessionResult> result;
+            final int seq;
+            final SequencedFutureManager manager = mSession2Stub.getConnectedControllersManager()
+                    .getSequencedFutureManager(controller);
+            if (manager != null) {
+                result = manager.createSequencedFuture();
+                seq = ((SequencedFuture<SessionResult>) result).getSequenceNumber();
+            } else {
+                // Can be null in two cases. Use the 0 as sequence number in both cases because
+                //     Case 1) Controller is from the legacy stub
+                //             -> Sequence number isn't needed, so 0 is OK
+                //     Case 2) Controller is removed after the connection check above
+                //             -> Call will fail below or ignored by the controller, so 0 is OK.
+                seq = 0;
+                result = SessionResult.createFutureWithResult(RESULT_CODE_SUCCESS);
+            }
+            command.run(seq, controller.getControllerCb());
+            return result;
+        } catch (DeadObjectException e) {
+            onDeadObjectException(controller, e);
+            return SessionResult.createFutureWithResult(RESULT_CODE_DISCONNECTED);
+        } catch (RemoteException e) {
+            // Currently it's TransactionTooLargeException or DeadSystemException.
+            // We'd better to leave log for those cases because
+            //   - TransactionTooLargeException means that we may need to fix our code.
+            //     (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);
+            return SessionResult.createFutureWithResult(RESULT_CODE_UNKNOWN_ERROR);
+        }
+    }
+
+    private void broadcastCommand(@NonNull ControllerCommand command) {
+        List<ControllerInfo> controllers =
+                mSession2Stub.getConnectedControllersManager().getConnectedControllers();
+        controllers.add(mSessionLegacyStub.getControllersForAll());
+        for (int i = 0; i < controllers.size(); i++) {
+            ControllerInfo controller = controllers.get(i);
+            try {
+                final SequencedFutureManager manager = mSession2Stub
+                        .getConnectedControllersManager().getSequencedFutureManager(controller);
+                final int seq;
+                if (manager != null) {
+                    seq = manager.obtainNextSequenceNumber();
+                    // Can be null in two cases. Use the 0 as sequence number in both cases because
+                    //     Case 1) Controller is from the legacy stub
+                    //             -> Sequence number isn't needed, so 0 is OK
+                    //     Case 2) Controller is removed after the connection check above
+                    //             -> Call will fail below or ignored by the controller, so 0 is OK.
+                } else {
+                    seq = 0;
+                }
+                command.run(seq, controller.getControllerCb());
+            } catch (DeadObjectException e) {
+                onDeadObjectException(controller, e);
+            } catch (RemoteException e) {
+                // Currently it's TransactionTooLargeException or DeadSystemException.
+                // We'd better to leave log for those cases because
+                //   - TransactionTooLargeException means that we may need to fix our code.
+                //     (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);
+            }
+        }
+    }
+
+    /**
+     * Removes controller. Call this when DeadObjectException is happened with binder call.
+     */
+    private void onDeadObjectException(ControllerInfo controller, DeadObjectException e) {
+        if (DEBUG) {
+            Log.d(TAG, controller.toString() + " is gone", e);
+        }
+        // Note: Only removing from MediaSession2Stub and ignoring (legacy) stubs would be fine for
+        //       now. Because calls to the legacy stubs doesn't throw DeadObjectException.
+        mSession2Stub.getConnectedControllersManager().removeController(controller);
+    }
+
     ///////////////////////////////////////////////////
     // Inner classes
     ///////////////////////////////////////////////////
@@ -1068,6 +1150,11 @@
         void run(ControllerCb callback) throws RemoteException;
     }
 
+    @FunctionalInterface
+    interface ControllerCommand {
+        void run(int seq, ControllerCb controller) throws RemoteException;
+    }
+
     private static class SessionPlayerCallback extends SessionPlayer2.PlayerCallback {
         private final WeakReference<MediaSession2ImplBase> mSession;
 
diff --git a/media2/src/main/java/androidx/media2/MediaSession2Stub.java b/media2/src/main/java/androidx/media2/MediaSession2Stub.java
index e80b9c5..2d537f7 100644
--- a/media2/src/main/java/androidx/media2/MediaSession2Stub.java
+++ b/media2/src/main/java/androidx/media2/MediaSession2Stub.java
@@ -31,7 +31,6 @@
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.RemoteException;
-import android.os.ResultReceiver;
 import android.os.SystemClock;
 import android.support.v4.media.session.MediaSessionCompat;
 import android.text.TextUtils;
@@ -141,7 +140,6 @@
         }
     }
 
-
     private void onSessionCommand(@NonNull IMediaController2 caller, int seq,
             final @CommandCode int commandCode,
             final @NonNull Command command) {
@@ -390,6 +388,18 @@
     }
 
     @Override
+    public void onControllerResult(final IMediaController2 caller, int seq,
+            final ParcelImpl controllerResult) {
+        SequencedFutureManager manager = mConnectedControllersManager.getSequencedFutureManager(
+                caller.asBinder());
+        if (manager == null) {
+            return;
+        }
+        MediaController2.ControllerResult result = ParcelUtils.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 {
         onSessionCommand(caller, seq, SessionCommand2.COMMAND_CODE_VOLUME_SET_VOLUME,
@@ -654,15 +664,16 @@
 
     @Override
     public void setPlaylist(final IMediaController2 caller, int seq,
-            final List<ParcelImpl> playlist, final Bundle metadata) {
+            final ParcelImplListSlice listSlice, final Bundle metadata) {
         onSessionCommand(caller, seq, SessionCommand2.COMMAND_CODE_PLAYER_SET_PLAYLIST,
                 new PlayerCommand() {
                     @Override
                     public ListenableFuture<PlayerResult> run(ControllerInfo controller) {
-                        if (playlist == null) {
+                        if (listSlice == null || listSlice.getList() == 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,
@@ -1030,6 +1041,7 @@
     }
 
     final class Controller2Cb extends ControllerCb {
+        // TODO: Drop 'Callback' from the name.
         private final IMediaController2 mIControllerCallback;
 
         Controller2Cb(@NonNull IMediaController2 callback) {
@@ -1055,8 +1067,8 @@
         }
 
         @Override
-        void onCustomLayoutChanged(List<CommandButton> layout) throws RemoteException {
-            mIControllerCallback.onCustomLayoutChanged(
+        void setCustomLayout(int seq, List<CommandButton> layout) throws RemoteException {
+            mIControllerCallback.onSetCustomLayout(seq,
                     MediaUtils2.convertCommandButtonListToParcelImplList(layout));
         }
 
@@ -1072,10 +1084,10 @@
         }
 
         @Override
-        void onCustomCommand(SessionCommand2 command, Bundle args, ResultReceiver receiver)
+        void sendCustomCommand(int seq, SessionCommand2 command, Bundle args)
                 throws RemoteException {
-            mIControllerCallback.onCustomCommand((ParcelImpl) ParcelUtils.toParcelable(command),
-                    args, receiver);
+            mIControllerCallback.onCustomCommand(seq,
+                    (ParcelImpl) ParcelUtils.toParcelable(command), args);
         }
 
         @Override
diff --git a/media2/src/main/java/androidx/media2/MediaSessionLegacyStub.java b/media2/src/main/java/androidx/media2/MediaSessionLegacyStub.java
index 2147a62..e5d35c7 100644
--- a/media2/src/main/java/androidx/media2/MediaSessionLegacyStub.java
+++ b/media2/src/main/java/androidx/media2/MediaSessionLegacyStub.java
@@ -573,7 +573,7 @@
         }
 
         @Override
-        void onCustomLayoutChanged(List<CommandButton> layout) throws RemoteException {
+        void setCustomLayout(int seq, List<CommandButton> layout) throws RemoteException {
             // no-op.
         }
 
@@ -588,7 +588,7 @@
         }
 
         @Override
-        void onCustomCommand(SessionCommand2 command, Bundle args, ResultReceiver receiver)
+        void sendCustomCommand(int seq, SessionCommand2 command, Bundle args)
                 throws RemoteException {
             // no-op
         }
@@ -710,7 +710,7 @@
         }
 
         @Override
-        void onCustomLayoutChanged(List<CommandButton> layout) throws RemoteException {
+        void setCustomLayout(int seq, List<CommandButton> layout) throws RemoteException {
             throw new AssertionError("This shouldn't be called.");
         }
 
@@ -726,7 +726,7 @@
         }
 
         @Override
-        void onCustomCommand(SessionCommand2 command, Bundle args, ResultReceiver receiver)
+        void sendCustomCommand(int seq, SessionCommand2 command, Bundle args)
                 throws RemoteException {
             // no-op
         }
diff --git a/media2/src/main/java/androidx/media2/ParcelImplListSlice.java b/media2/src/main/java/androidx/media2/ParcelImplListSlice.java
new file mode 100644
index 0000000..68d08eb
--- /dev/null
+++ b/media2/src/main/java/androidx/media2/ParcelImplListSlice.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media2;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.util.Log;
+
+import androidx.annotation.RestrictTo;
+import androidx.versionedparcelable.ParcelImpl;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Transfer a large list of {@link ParcelImpl} objects across an IPC. Splits into
+ * multiple transactions if needed.
+ *
+ * Note: Using this class makes synchronous binder calls, and also loses oneway property.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public class ParcelImplListSlice implements Parcelable {
+    private static final String TAG = "ParcelImplListSlice";
+    private static final boolean DEBUG = false;
+
+    // TODO: get this number from somewhere else.
+    private static final int MAX_IPC_SIZE = 4 * 1024;
+
+    final List<ParcelImpl> mList;
+
+    public ParcelImplListSlice(List<ParcelImpl> list) {
+        mList = list;
+    }
+
+    ParcelImplListSlice(Parcel p) {
+        final int itemCount = p.readInt();
+        mList = new ArrayList<>(itemCount);
+        if (DEBUG) {
+            Log.d(TAG, "Retrieving " + itemCount + " items");
+        }
+        if (itemCount <= 0) {
+            return;
+        }
+
+        int i = 0;
+        while (i < itemCount) {
+            if (p.readInt() == 0) {
+                break;
+            }
+
+            final ParcelImpl parcelImpl = p.readParcelable(ParcelImpl.class.getClassLoader());
+            mList.add(parcelImpl);
+
+            if (DEBUG) {
+                Log.d(TAG, "Read inline #" + i + ": " + mList.get(mList.size() - 1));
+            }
+            i++;
+        }
+        if (i >= itemCount) {
+            return;
+        }
+        final IBinder retriever = p.readStrongBinder();
+        while (i < itemCount) {
+            if (DEBUG) {
+                Log.d(TAG, "Reading more @" + i + " of " + itemCount + ": retriever=" + retriever);
+            }
+            Parcel data = Parcel.obtain();
+            Parcel reply = Parcel.obtain();
+            data.writeInt(i);
+            try {
+                retriever.transact(IBinder.FIRST_CALL_TRANSACTION, data, reply, 0);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failure retrieving array; only received " + i + " of " + itemCount, e);
+                return;
+            }
+            while (i < itemCount && reply.readInt() != 0) {
+                final ParcelImpl parcelImpl = reply.readParcelable(
+                        ParcelImpl.class.getClassLoader());
+                mList.add(parcelImpl);
+
+                if (DEBUG) {
+                    Log.d(TAG, "Read extra #" + i + ": " + mList.get(mList.size() - 1));
+                }
+                i++;
+            }
+            reply.recycle();
+            data.recycle();
+        }
+    }
+
+    public List<ParcelImpl> getList() {
+        return mList;
+    }
+
+    /**
+     * Write this to another Parcel. Note that this discards the internal Parcel
+     * and should not be used anymore. This is so we can pass this to a Binder
+     * where we won't have a chance to call recycle on this.
+     */
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        final int itemCount = mList.size();
+        dest.writeInt(itemCount);
+        if (DEBUG) {
+            Log.d(TAG, "Writing " + itemCount + " items");
+        }
+        if (itemCount > 0) {
+            int i = 0;
+            while (i < itemCount && dest.dataSize() < MAX_IPC_SIZE) {
+                dest.writeInt(1);
+
+                final ParcelImpl parcelable = mList.get(i);
+                dest.writeParcelable(parcelable, flags);
+
+                if (DEBUG) {
+                    Log.d(TAG, "Wrote inline #" + i + ": " + mList.get(i));
+                }
+                i++;
+            }
+            if (i < itemCount) {
+                dest.writeInt(0);
+                Binder retriever = new Binder() {
+                    @Override
+                    protected boolean onTransact(int code, Parcel data, Parcel reply, int flags)
+                            throws RemoteException {
+                        if (code != FIRST_CALL_TRANSACTION) {
+                            return super.onTransact(code, data, reply, flags);
+                        }
+                        int i = data.readInt();
+                        if (DEBUG) {
+                            Log.d(TAG, "Writing more @" + i + " of " + itemCount);
+                        }
+                        while (i < itemCount && reply.dataSize() < MAX_IPC_SIZE) {
+                            reply.writeInt(1);
+
+                            final ParcelImpl parcelable = mList.get(i);
+                            reply.writeParcelable(parcelable, flags);
+
+                            if (DEBUG) {
+                                Log.d(TAG, "Wrote extra #" + i + ": " + mList.get(i));
+                            }
+                            i++;
+                        }
+                        if (i < itemCount) {
+                            if (DEBUG) {
+                                Log.d(TAG, "Breaking @" + i + " of " + itemCount);
+                            }
+                            reply.writeInt(0);
+                        }
+                        return true;
+                    }
+                };
+                if (DEBUG) {
+                    Log.d(TAG, "Breaking @" + i + " of " + itemCount + ": retriever=" + retriever);
+                }
+                dest.writeStrongBinder(retriever);
+            }
+        }
+    }
+
+    @Override
+    public int describeContents() {
+        int contents = 0;
+        final List<ParcelImpl> list = getList();
+        for (int i = 0; i < list.size(); i++) {
+            contents |= list.get(i).describeContents();
+        }
+        return contents;
+    }
+
+    public static final Parcelable.Creator<ParcelImplListSlice> CREATOR =
+            new Parcelable.Creator<ParcelImplListSlice>() {
+        @Override
+        public ParcelImplListSlice createFromParcel(Parcel in) {
+            return new ParcelImplListSlice(in);
+        }
+
+        @Override
+        public ParcelImplListSlice[] newArray(int size) {
+            return new ParcelImplListSlice[size];
+        }
+    };
+}
diff --git a/media2/src/main/java/androidx/media2/SequencedFutureManager.java b/media2/src/main/java/androidx/media2/SequencedFutureManager.java
index 171b3af..2b4a60b 100644
--- a/media2/src/main/java/androidx/media2/SequencedFutureManager.java
+++ b/media2/src/main/java/androidx/media2/SequencedFutureManager.java
@@ -34,6 +34,7 @@
  */
 @TargetApi(Build.VERSION_CODES.P)
 class SequencedFutureManager<T> implements AutoCloseable {
+    private static final boolean DEBUG = false;
     private static final String TAG = "SequencedFutureManager";
     private final Object mLock = new Object();
     private final T mResultWhenClosed;
@@ -94,8 +95,12 @@
             if (future != null) {
                 future.set(result);
             } else {
-                Log.w(TAG, "Unexpected sequence number, seq=" + seq,
-                        new IllegalArgumentException());
+                if (DEBUG) {
+                    // Note: May not be an error if the caller doesn't return ListenableFuture
+                    //       e.g. MediaSession2#broadcastCustomCommand
+                    Log.d(TAG, "Unexpected sequence number, seq=" + seq,
+                            new IllegalArgumentException());
+                }
             }
         }
     }
diff --git a/settings.gradle b/settings.gradle
index 7ed5d05..6062745 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -68,7 +68,7 @@
 includeProject(":exifinterface", "exifinterface")
 includeProject(":fragment", "fragment")
 includeProject(":fragment-ktx", "fragment/ktx")
-includeProject(":fragment-test", "fragment/test")
+includeProject(":fragment-testing", "fragment/testing")
 includeProject(":gridlayout", "gridlayout")
 includeProject(":heifwriter", "heifwriter")
 includeProject(":interpolator", "interpolator")
diff --git a/textclassifier/api/current.txt b/textclassifier/api/current.txt
index 11932f6..2a7e78e 100644
--- a/textclassifier/api/current.txt
+++ b/textclassifier/api/current.txt
@@ -110,8 +110,7 @@
   }
 
   public final class TextLinks {
-    method public int apply(android.widget.TextView, androidx.textclassifier.TextLinksParams);
-    method public int apply(android.content.Context, android.text.Spannable, androidx.textclassifier.TextLinksParams);
+    method public int apply(android.text.Spannable, androidx.textclassifier.TextClassifier, androidx.textclassifier.TextLinksParams);
     method public static androidx.textclassifier.TextLinks createFromBundle(android.os.Bundle);
     method public java.util.Collection<androidx.textclassifier.TextLinks.TextLink> getLinks();
     method public android.os.Bundle toBundle();
diff --git a/textclassifier/integration-tests/testapp/src/main/java/androidx/textclassifier/integration/testapp/MainActivity.java b/textclassifier/integration-tests/testapp/src/main/java/androidx/textclassifier/integration/testapp/MainActivity.java
index 580fd9d..569b4cd 100644
--- a/textclassifier/integration-tests/testapp/src/main/java/androidx/textclassifier/integration/testapp/MainActivity.java
+++ b/textclassifier/integration-tests/testapp/src/main/java/androidx/textclassifier/integration/testapp/MainActivity.java
@@ -18,9 +18,9 @@
 
 import android.os.Bundle;
 import android.text.Spannable;
+import android.text.method.LinkMovementMethod;
 import android.view.View;
 import android.widget.AdapterView;
-import android.widget.EditText;
 import android.widget.Spinner;
 import android.widget.TextView;
 
@@ -44,20 +44,15 @@
     private static final Executor sWorkerThreadExecutor = Executors.newSingleThreadExecutor();
     private static final Executor sMainThreadExecutor = new MainThreadExecutor();
 
-    private EditText mInput;
-
+    private TextView mInput;
     private TextView mStatusTextView;
 
-    private TextClassificationManager mTextClassificationManager;
-
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-
-        mTextClassificationManager = TextClassificationManager.of(this);
-
         setContentView(R.layout.activity_main);
         mInput = findViewById(R.id.textView_input);
+        setLinkMovementMethod(mInput);
         mStatusTextView = findViewById(R.id.textView_tc);
         findViewById(R.id.button_generate_links).setOnClickListener(v -> linkifyAsync(mInput));
         updateStatusText();
@@ -70,18 +65,15 @@
             @Override
             public void onItemSelected(AdapterView<?> adapterView, View view, int pos, long l) {
                 if (pos == DEFAULT) {
-                    mTextClassificationManager.setTextClassifier(null);
+                    setTextClassifier(null);
                 } else {
-                    mTextClassificationManager.setTextClassifier(
-                            new SimpleTextClassifier(MainActivity.this));
+                    setTextClassifier(new SimpleTextClassifier(MainActivity.this));
                 }
                 updateStatusText();
             }
 
             @Override
-            public void onNothingSelected(AdapterView<?> adapterView) {
-
-            }
+            public void onNothingSelected(AdapterView<?> adapterView) {}
         });
     }
 
@@ -107,12 +99,23 @@
         sWorkerThreadExecutor.execute(() -> {
             TextLinks.Request request = new TextLinks.Request.Builder(textView.getText()).build();
             TextLinks textLinks = getTextClassifier().generateLinks(request);
-            sMainThreadExecutor.execute(
-                    () -> textLinks.apply(textView, TextLinksParams.DEFAULT_PARAMS));
+            sMainThreadExecutor.execute(() ->
+                    textLinks.apply(
+                            (Spannable) textView.getText(),
+                            getTextClassifier(),
+                            TextLinksParams.DEFAULT_PARAMS));
         });
     }
 
     private TextClassifier getTextClassifier() {
-        return mTextClassificationManager.getTextClassifier();
+        return TextClassificationManager.of(this).getTextClassifier();
+    }
+
+    private void setTextClassifier(TextClassifier textClassifier) {
+        TextClassificationManager.of(this).setTextClassifier(textClassifier);
+    }
+
+    private void setLinkMovementMethod(TextView textView) {
+        textView.setMovementMethod(LinkMovementMethod.getInstance());
     }
 }
diff --git a/textclassifier/integration-tests/testapp/src/main/java/androidx/textclassifier/integration/testapp/SimpleTextClassifier.java b/textclassifier/integration-tests/testapp/src/main/java/androidx/textclassifier/integration/testapp/SimpleTextClassifier.java
index bcc76e0..e1ca0e4a 100644
--- a/textclassifier/integration-tests/testapp/src/main/java/androidx/textclassifier/integration/testapp/SimpleTextClassifier.java
+++ b/textclassifier/integration-tests/testapp/src/main/java/androidx/textclassifier/integration/testapp/SimpleTextClassifier.java
@@ -67,9 +67,8 @@
 
     @Override
     public TextLinks generateLinks(TextLinks.Request request) {
-        TextLinks.Builder builder = new TextLinks.Builder(request.getText().toString());
-        CharSequence text = request.getText();
-
+        String text = request.getText().toString();
+        TextLinks.Builder builder = new TextLinks.Builder(text);
         final Spannable spannable = new SpannableString(text);
         if (LinkifyCompat.addLinks(spannable, Pattern.compile("android", Pattern.CASE_INSENSITIVE),
                 null, null, (matcher, s) -> "https://www.android.com")) {
diff --git a/textclassifier/src/androidTest/java/androidx/textclassifier/TextLinksTest.java b/textclassifier/src/androidTest/java/androidx/textclassifier/TextLinksTest.java
index 343b948..764389c 100644
--- a/textclassifier/src/androidTest/java/androidx/textclassifier/TextLinksTest.java
+++ b/textclassifier/src/androidTest/java/androidx/textclassifier/TextLinksTest.java
@@ -28,9 +28,7 @@
 import android.content.Context;
 import android.text.Spannable;
 import android.text.SpannableString;
-import android.text.method.LinkMovementMethod;
 import android.text.style.URLSpan;
-import android.widget.TextView;
 
 import androidx.collection.ArrayMap;
 import androidx.core.os.LocaleListCompat;
@@ -173,7 +171,8 @@
         TextLinks textLinks = new TextLinks.Builder(text).build();
 
         Context context = InstrumentationRegistry.getContext();
-        int status = textLinks.apply(context, text, TextLinksParams.DEFAULT_PARAMS);
+        TextClassifier textClassifier = TextClassificationManager.of(context).getTextClassifier();
+        int status = textLinks.apply(text, textClassifier, TextLinksParams.DEFAULT_PARAMS);
         assertThat(status).isEqualTo(TextLinks.STATUS_NO_LINKS_FOUND);
 
         final TextLinks.TextLinkSpan[] spans =
@@ -189,29 +188,13 @@
                 .build();
 
         Context context = InstrumentationRegistry.getContext();
-        int status = textLinks.apply(context, text, TextLinksParams.DEFAULT_PARAMS);
+        TextClassifier textClassifier = TextClassificationManager.of(context).getTextClassifier();
+        int status = textLinks.apply(text, textClassifier, TextLinksParams.DEFAULT_PARAMS);
         assertThat(status).isEqualTo(TextLinks.STATUS_LINKS_APPLIED);
 
         assertAppliedSpannable(text);
     }
 
-    @Test
-    public void testApply_textview() {
-        SpannableString text = new SpannableString(FULL_TEXT);
-        TextLinks textLinks = new TextLinks.Builder(text)
-                .addLink(START, END, Collections.singletonMap(TextClassifier.TYPE_PHONE, 1.0f))
-                .build();
-
-        final TextView textView = new TextView(InstrumentationRegistry.getTargetContext());
-        textView.setText(text);
-
-        int status = textLinks.apply(textView, TextLinksParams.DEFAULT_PARAMS);
-        assertThat(status).isEqualTo(TextLinks.STATUS_LINKS_APPLIED);
-        assertThat(textView.getMovementMethod()).isInstanceOf(LinkMovementMethod.class);
-
-        assertAppliedSpannable((Spannable) textView.getText());
-    }
-
     private void assertAppliedSpannable(Spannable spannable) {
         TextLinks.TextLinkSpan[] spans =
                 spannable.getSpans(0, spannable.length(), TextLinks.TextLinkSpan.class);
diff --git a/textclassifier/src/main/java/androidx/textclassifier/TextLinks.java b/textclassifier/src/main/java/androidx/textclassifier/TextLinks.java
index bbc97ab..3e3dbd9 100644
--- a/textclassifier/src/main/java/androidx/textclassifier/TextLinks.java
+++ b/textclassifier/src/main/java/androidx/textclassifier/TextLinks.java
@@ -20,13 +20,10 @@
 import static androidx.textclassifier.ConvertUtils.unwrapLocalListCompat;
 
 import android.app.PendingIntent;
-import android.content.Context;
 import android.os.Build;
 import android.os.Bundle;
 import android.text.Spannable;
-import android.text.SpannableString;
 import android.text.Spanned;
-import android.text.method.LinkMovementMethod;
 import android.text.method.MovementMethod;
 import android.text.style.ClickableSpan;
 import android.text.style.URLSpan;
@@ -40,7 +37,6 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
-import androidx.annotation.UiThread;
 import androidx.annotation.VisibleForTesting;
 import androidx.core.app.RemoteActionCompat;
 import androidx.core.os.LocaleListCompat;
@@ -164,64 +160,30 @@
     }
 
     /**
-     * Similar to {@link #apply(Context, Spannable, TextLinksParams)}, except the links are applied
-     * to a TextView directly. This also adds a LinkMovementMethod to the TextView if necessary.
-     *
-     * @see #apply(Context, Spannable, TextLinksParams)
-     */
-    @UiThread
-    @Status
-    public int apply(@NonNull TextView textView, TextLinksParams textLinksParams) {
-        Preconditions.checkNotNull(textView);
-
-        addLinkMovementMethod(textView);
-
-        SpannableString spannableString = SpannableString.valueOf(textView.getText());
-        int status = apply(textView.getContext(), spannableString, textLinksParams);
-        if (status == TextLinks.STATUS_LINKS_APPLIED) {
-            textView.setText(spannableString);
-        }
-        return status;
-    }
-
-    /**
      * Annotates the given text with the generated links.
      *
-     * <p> A text classifier returned by {@link TextClassificationManager#getTextClassifier()} is
-     * used.
-     *
      * <p><strong>NOTE: </strong>It may be necessary to set a LinkMovementMethod on the TextView
      * widget to properly handle links. See {@link TextView#setMovementMethod(MovementMethod)}
      *
-     * @param context context
      * @param text the text to apply the links to. Must match the original text
-     * @param textLinksParams the param that specifies how the links should be applied.
+     * @param textClassifier the TextClassifier to use to classify a clicked link. Should usually
+     *                       be the one used to generate the links
+     * @param textLinksParams the param that specifies how the links should be applied
      *
      * @return the status code which indicates the operation is success or not.
      */
     @Status
     public int apply(
-            @NonNull Context context,
             @NonNull Spannable text,
+            @NonNull TextClassifier textClassifier,
             @NonNull TextLinksParams textLinksParams) {
-        Preconditions.checkNotNull(context);
         Preconditions.checkNotNull(text);
+        Preconditions.checkNotNull(textClassifier);
         Preconditions.checkNotNull(textLinksParams);
 
-        TextClassifier textClassifier = TextClassificationManager.of(context).getTextClassifier();
-
         return textLinksParams.apply(text, this, textClassifier);
     }
 
-    private void addLinkMovementMethod(@NonNull TextView textView) {
-        MovementMethod method = textView.getMovementMethod();
-        if (!(method instanceof LinkMovementMethod)) {
-            if (textView.getLinksClickable()) {
-                textView.setMovementMethod(LinkMovementMethod.getInstance());
-            }
-        }
-    }
-
     /**
      * A link, identifying a substring of text and possible entity types for it.
      */
diff --git a/textclassifier/src/main/java/androidx/textclassifier/TextLinksParams.java b/textclassifier/src/main/java/androidx/textclassifier/TextLinksParams.java
index 5e3ad67..d2bd619 100644
--- a/textclassifier/src/main/java/androidx/textclassifier/TextLinksParams.java
+++ b/textclassifier/src/main/java/androidx/textclassifier/TextLinksParams.java
@@ -16,7 +16,6 @@
 
 package androidx.textclassifier;
 
-import android.content.Context;
 import android.text.Spannable;
 import android.text.style.ClickableSpan;
 
@@ -31,7 +30,7 @@
 
 /**
  * Used to specify how to apply links when using
- * {@link TextLinks#apply(Context, Spannable, TextLinksParams)} APIs.
+ * {@link TextLinks#apply(Spannable, TextClassifier, TextLinksParams)} APIs.
  */
 public final class TextLinksParams {
 
@@ -49,7 +48,7 @@
     /**
      * Default configuration of applying a TextLinks to a spannable or a TextView.
      *
-     * @see TextLinks#apply(Context, Spannable, TextLinksParams)
+     * @see TextLinks#apply(Spannable, TextClassifier, TextLinksParams)
      */
     public static final TextLinksParams DEFAULT_PARAMS = new TextLinksParams.Builder().build();
 
diff --git a/work/workmanager/api/current.txt b/work/workmanager/api/current.txt
index 826e4ab..0a7d318 100644
--- a/work/workmanager/api/current.txt
+++ b/work/workmanager/api/current.txt
@@ -135,8 +135,8 @@
     method public final java.util.List<android.net.Uri> getTriggeredContentUris();
     method public final boolean isCancelled();
     method public final boolean isStopped();
-    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Payload> onStartWork();
     method public void onStopped(boolean);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Payload> startWork();
   }
 
   public static final class ListenableWorker.Payload {
@@ -261,7 +261,6 @@
   }
 
   public final class WorkStatus {
-    ctor public WorkStatus(java.util.UUID, androidx.work.State, androidx.work.Data, java.util.List<java.lang.String>);
     method public java.util.UUID getId();
     method public androidx.work.Data getOutputData();
     method public androidx.work.State getState();
@@ -272,8 +271,8 @@
     ctor public Worker(android.content.Context, androidx.work.WorkerParameters);
     method public abstract androidx.work.ListenableWorker.Result doWork();
     method public final androidx.work.Data getOutputData();
-    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Payload> onStartWork();
     method public final void setOutputData(androidx.work.Data);
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Payload> startWork();
   }
 
   public abstract class WorkerFactory {
diff --git a/work/workmanager/src/main/java/androidx/work/ListenableWorker.java b/work/workmanager/src/main/java/androidx/work/ListenableWorker.java
index f979aaf..af3286403 100644
--- a/work/workmanager/src/main/java/androidx/work/ListenableWorker.java
+++ b/work/workmanager/src/main/java/androidx/work/ListenableWorker.java
@@ -36,10 +36,10 @@
 
 /**
  * The basic object that performs work.  Worker classes are instantiated at runtime by the
- * {@link WorkerFactory} specified in the {@link Configuration}.  The {@link #onStartWork()} method
+ * {@link WorkerFactory} specified in the {@link Configuration}.  The {@link #startWork()} method
  * is called on the background thread.  In case the work is preempted and later restarted for any
  * reason, a new instance of {@link ListenableWorker} is created. This means that
- * {@code onStartWork} is called exactly once per {@link ListenableWorker} instance.
+ * {@code startWork} is called exactly once per {@link ListenableWorker} instance.
  */
 public abstract class ListenableWorker {
 
@@ -182,7 +182,7 @@
      *         cancel this Future, WorkManager will treat this unit of work as failed.
      */
     @MainThread
-    public abstract @NonNull ListenableFuture<Payload> onStartWork();
+    public abstract @NonNull ListenableFuture<Payload> startWork();
 
     /**
      * Returns {@code true} if this Worker has been told to stop.  This could be because of an
@@ -274,7 +274,7 @@
 
 
     /**
-     * The payload of an {@link #onStartWork()} computation that contains both the result and the
+     * The payload of an {@link #startWork()} computation that contains both the result and the
      * output data.
      */
     public static final class Payload {
@@ -285,7 +285,7 @@
         /**
          * Constructs a Payload with the given {@link Result} and an empty output.
          *
-         * @param result The result of the {@link #onStartWork()} computation
+         * @param result The result of the {@link #startWork()} computation
          */
         public Payload(@NonNull Result result) {
             this(result, Data.EMPTY);
@@ -294,8 +294,8 @@
         /**
          * Constructs a Payload with the given {@link Result} and output.
          *
-         * @param result The result of the {@link #onStartWork()} computation
-         * @param output The output {@link Data} of the {@link #onStartWork()} computation
+         * @param result The result of the {@link #startWork()} computation
+         * @param output The output {@link Data} of the {@link #startWork()} computation
          */
         public Payload(@NonNull Result result, @NonNull Data output) {
             mResult = result;
diff --git a/work/workmanager/src/main/java/androidx/work/WorkStatus.java b/work/workmanager/src/main/java/androidx/work/WorkStatus.java
index 57e0c52..66870e3 100644
--- a/work/workmanager/src/main/java/androidx/work/WorkStatus.java
+++ b/work/workmanager/src/main/java/androidx/work/WorkStatus.java
@@ -17,6 +17,7 @@
 package androidx.work;
 
 import android.support.annotation.NonNull;
+import android.support.annotation.RestrictTo;
 
 import java.util.HashSet;
 import java.util.List;
@@ -36,6 +37,10 @@
     private @NonNull Data mOutputData;
     private @NonNull Set<String> mTags;
 
+    /**
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     public WorkStatus(
             @NonNull UUID id,
             @NonNull State state,
diff --git a/work/workmanager/src/main/java/androidx/work/Worker.java b/work/workmanager/src/main/java/androidx/work/Worker.java
index 011c51c..a4d683d 100644
--- a/work/workmanager/src/main/java/androidx/work/Worker.java
+++ b/work/workmanager/src/main/java/androidx/work/Worker.java
@@ -49,7 +49,7 @@
     public abstract @NonNull Result doWork();
 
     @Override
-    public final @NonNull ListenableFuture<Payload> onStartWork() {
+    public final @NonNull ListenableFuture<Payload> startWork() {
         mFuture = SettableFuture.create();
         getBackgroundExecutor().execute(new Runnable() {
             @Override
@@ -70,7 +70,7 @@
      * {@link OverwritingInputMerger}, unless otherwise specified using the
      * {@link OneTimeWorkRequest.Builder#setInputMerger(Class)} method.
      * <p>
-     * This method is invoked after {@code onStartWork} and returns
+     * This method is invoked after {@code startWork} and returns
      * {@link ListenableWorker.Result#SUCCESS} or a
      * {@link ListenableWorker.Result#FAILURE}.
      * <p>
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 51521d7..5193a00 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/WorkerWrapper.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/WorkerWrapper.java
@@ -216,13 +216,13 @@
             }
 
             final SettableFuture<ListenableWorker.Payload> future = SettableFuture.create();
-            // Call mWorker.onStartWork() on the main thread.
+            // Call mWorker.startWork() on the main thread.
             mWorkTaskExecutor.getMainThreadExecutor()
                     .execute(new Runnable() {
                         @Override
                         public void run() {
                             try {
-                                mInnerFuture = mWorker.onStartWork();
+                                mInnerFuture = mWorker.startWork();
                                 future.setFuture(mInnerFuture);
                             } catch (Throwable e) {
                                 future.setException(e);
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 a3e4d7e..522937c 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
@@ -81,7 +81,7 @@
 
     @NonNull
     @Override
-    public ListenableFuture<Payload> onStartWork() {
+    public ListenableFuture<Payload> startWork() {
         getBackgroundExecutor().execute(new Runnable() {
             @Override
             public void run() {
@@ -132,7 +132,7 @@
             // changes in constraints can cause the worker to throw RuntimeExceptions, and
             // that should cause a retry.
             try {
-                final ListenableFuture<Payload> innerFuture = mDelegate.onStartWork();
+                final ListenableFuture<Payload> innerFuture = mDelegate.startWork();
                 innerFuture.addListener(new Runnable() {
                     @Override
                     public void run() {
@@ -147,7 +147,7 @@
                 }, getBackgroundExecutor());
             } catch (Throwable exception) {
                 Logger.debug(TAG, String.format(
-                        "Delegated worker %s threw exception in onStartWork.", className),
+                        "Delegated worker %s threw exception in startWork.", className),
                         exception);
                 synchronized (mLock) {
                     if (mAreConstraintsUnmet) {