Implement set/getPlaybackSpeed

This CL implements set/getPlaybackSpeed in MediaSession2/Controller2,
and also adds tests.

Bug: 77241129
Test: ./gradlew media:check media:connectedCheck
Change-Id: I4e689aa24afc4abb1015911bd176ed679fe2fd45
diff --git a/media/src/androidTest/java/androidx/media/MediaController2Test.java b/media/src/androidTest/java/androidx/media/MediaController2Test.java
index 3cd7a2a..9e54276 100644
--- a/media/src/androidTest/java/androidx/media/MediaController2Test.java
+++ b/media/src/androidTest/java/androidx/media/MediaController2Test.java
@@ -16,8 +16,7 @@
 
 package androidx.media;
 
-import static junit.framework.Assert.assertEquals;
-
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
@@ -222,7 +221,7 @@
         long time2 = System.currentTimeMillis();
         assertEquals(state, controller.getPlayerState());
         assertEquals(bufferedPosition, controller.getBufferedPosition());
-        assertEquals(speed, controller.getPlaybackSpeed());
+        assertEquals(speed, controller.getPlaybackSpeed(), 0.0f);
         long positionLowerBound = (long) (position + speed * (System.currentTimeMillis() - time2));
         long currentPosition = controller.getCurrentPosition();
         long positionUpperBound = (long) (position + speed * (System.currentTimeMillis() - time1));
@@ -348,6 +347,15 @@
         }
     }
 
+    @Test
+    public void testSetPlaybackSpeed() throws Exception {
+        prepareLooper();
+        final float speed = 1.5f;
+        mController.setPlaybackSpeed(speed);
+        assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertEquals(speed, mPlayer.mPlaybackSpeed, 0.0f);
+    }
+
     /**
      * Test whether {@link MediaSession2#setPlaylist(List, MediaMetadata2)} is notified
      * through the
diff --git a/media/src/androidTest/java/androidx/media/MediaSession2Test.java b/media/src/androidTest/java/androidx/media/MediaSession2Test.java
index d738c9f..e2cfedf 100644
--- a/media/src/androidTest/java/androidx/media/MediaSession2Test.java
+++ b/media/src/androidTest/java/androidx/media/MediaSession2Test.java
@@ -248,6 +248,47 @@
         }
     }
 
+    /**
+     * This also tests {@link ControllerCallback#onPlaybackSpeedChanged(MediaController2, float)}
+     * and {@link MediaController2#getPlaybackSpeed()}.
+     */
+    @Test
+    public void testPlaybackSpeedChanged() throws Exception {
+        prepareLooper();
+        final float speed = 1.5f;
+        mPlayer.setPlaybackSpeed(speed);
+
+        final CountDownLatch latchForSessionCallback = new CountDownLatch(1);
+        try (MediaSession2 session = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setId("testPlaybackSpeedChanged")
+                .setSessionCallback(sHandlerExecutor, new SessionCallback() {
+                    @Override
+                    public void onPlaybackSpeedChanged(MediaSession2 session,
+                            MediaPlayerBase player, float speedOut) {
+                        assertEquals(speed, speedOut, 0.0f);
+                        latchForSessionCallback.countDown();
+                    }
+                }).build()) {
+
+            final CountDownLatch latchForControllerCallback = new CountDownLatch(1);
+            final MediaController2 controller =
+                    createController(mSession.getToken(), true, new ControllerCallback() {
+                        @Override
+                        public void onPlaybackSpeedChanged(MediaController2 controller,
+                                float speedOut) {
+                            assertEquals(speed, speedOut, 0.0f);
+                            latchForControllerCallback.countDown();
+                        }
+                    });
+
+            mPlayer.notifyPlaybackSpeedChanged(speed);
+            assertTrue(latchForSessionCallback.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+            assertTrue(latchForControllerCallback.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+            assertEquals(speed, controller.getPlaybackSpeed(), 0.0f);
+        }
+    }
+
     @Test
     public void testUpdatePlayer() throws Exception {
         prepareLooper();
@@ -381,6 +422,23 @@
     }
 
     @Test
+    public void testSetPlaybackSpeed() throws Exception {
+        prepareLooper();
+        final float speed = 1.5f;
+        mSession.setPlaybackSpeed(speed);
+        assertTrue(mPlayer.mSetPlaybackSpeedCalled);
+        assertEquals(speed, mPlayer.mPlaybackSpeed, 0.0f);
+    }
+
+    @Test
+    public void testGetPlaybackSpeed() throws Exception {
+        prepareLooper();
+        final float speed = 1.5f;
+        mPlayer.setPlaybackSpeed(speed);
+        assertEquals(speed, mSession.getPlaybackSpeed(), 0.0f);
+    }
+
+    @Test
     public void testSkipToPreviousItem() {
         prepareLooper();
         mSession.skipToPreviousItem();
diff --git a/media/src/androidTest/java/androidx/media/MockPlayer.java b/media/src/androidTest/java/androidx/media/MockPlayer.java
index 5c0dd5f..ce940d8 100644
--- a/media/src/androidTest/java/androidx/media/MockPlayer.java
+++ b/media/src/androidTest/java/androidx/media/MockPlayer.java
@@ -35,6 +35,7 @@
     public boolean mResetCalled;
     public boolean mPrepareCalled;
     public boolean mSeekToCalled;
+    public boolean mSetPlaybackSpeedCalled;
     public long mSeekPosition;
     public long mCurrentPosition;
     public long mBufferedPosition;
@@ -194,6 +195,19 @@
         }
     }
 
+    public void notifyPlaybackSpeedChanged(final float speed) {
+        for (int i = 0; i < mCallbacks.size(); i++) {
+            final PlayerEventCallback callback = mCallbacks.keyAt(i);
+            final Executor executor = mCallbacks.valueAt(i);
+            executor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    callback.onPlaybackSpeedChanged(MockPlayer.this, speed);
+                }
+            });
+        }
+    }
+
     public void notifyError(int what) {
         for (int i = 0; i < mCallbacks.size(); i++) {
             final PlayerEventCallback callback = mCallbacks.keyAt(i);
@@ -241,7 +255,11 @@
 
     @Override
     public void setPlaybackSpeed(float speed) {
+        mSetPlaybackSpeedCalled = true;
         mPlaybackSpeed = speed;
+        if (mCountDownLatch != null) {
+            mCountDownLatch.countDown();
+        }
     }
 
     @Override
diff --git a/media/src/main/java/androidx/media/MediaConstants2.java b/media/src/main/java/androidx/media/MediaConstants2.java
index 5de31c0..2a168eb 100644
--- a/media/src/main/java/androidx/media/MediaConstants2.java
+++ b/media/src/main/java/androidx/media/MediaConstants2.java
@@ -29,6 +29,8 @@
             "androidx.media.session.event.ON_ROUTES_INFO_CHANGED";
     static final String SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED =
             "androidx.media.session.event.ON_PLAYBACK_INFO_CHANGED";
+    static final String SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED =
+            "androidx.media.session.event.ON_PLAYBACK_SPEED_CHANGED";
     static final String SESSION_EVENT_ON_REPEAT_MODE_CHANGED =
             "androidx.media.session.event.ON_REPEAT_MODE_CHANGED";
     static final String SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED =
@@ -59,6 +61,7 @@
     static final String ARGUMENT_ALLOWED_COMMANDS = "androidx.media.argument.ALLOWED_COMMANDS";
     static final String ARGUMENT_SEEK_POSITION = "androidx.media.argument.SEEK_POSITION";
     static final String ARGUMENT_PLAYER_STATE = "androidx.media.argument.PLAYER_STATE";
+    static final String ARGUMENT_PLAYBACK_SPEED = "androidx.media.argument.PLAYBACK_SPEED";
     static final String ARGUMENT_ERROR_CODE = "androidx.media.argument.ERROR_CODE";
     static final String ARGUMENT_REPEAT_MODE = "androidx.media.argument.REPEAT_MODE";
     static final String ARGUMENT_SHUFFLE_MODE = "androidx.media.argument.SHUFFLE_MODE";
diff --git a/media/src/main/java/androidx/media/MediaController2.java b/media/src/main/java/androidx/media/MediaController2.java
index 40c23ce..6ca8267 100644
--- a/media/src/main/java/androidx/media/MediaController2.java
+++ b/media/src/main/java/androidx/media/MediaController2.java
@@ -32,6 +32,7 @@
 import static androidx.media.MediaConstants2.ARGUMENT_PACKAGE_NAME;
 import static androidx.media.MediaConstants2.ARGUMENT_PID;
 import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_INFO;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_SPEED;
 import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_STATE_COMPAT;
 import static androidx.media.MediaConstants2.ARGUMENT_PLAYER_STATE;
 import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST;
@@ -58,6 +59,7 @@
 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED;
 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ERROR;
 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED;
 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYER_STATE_CHANGED;
 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_CHANGED;
 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED;
@@ -73,6 +75,7 @@
 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PREPARE;
 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_RESET;
 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_SEEK_TO;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_SET_SPEED;
 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_ADD_ITEM;
 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REMOVE_ITEM;
 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REPLACE_ITEM;
@@ -617,6 +620,20 @@
                         mPlaybackInfo = info;
                     }
                     mCallback.onPlaybackInfoChanged(MediaController2.this, info);
+                    break;
+                }
+                case SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED: {
+                    PlaybackStateCompat state =
+                            extras.getParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT);
+                    if (state == null) {
+                        return;
+                    }
+                    synchronized (mLock) {
+                        mPlaybackStateCompat = state;
+                    }
+                    mCallback.onPlaybackSpeedChanged(
+                            MediaController2.this, state.getPlaybackSpeed());
+                    break;
                 }
             }
         }
@@ -1171,7 +1188,15 @@
      * Set the playback speed.
      */
     public void setPlaybackSpeed(float speed) {
-        // TODO(jaewan): implement this (b/74093080)
+        synchronized (mLock) {
+            if (!mConnected) {
+                Log.w(TAG, "Session isn't active", new IllegalStateException());
+                return;
+            }
+            Bundle args = new Bundle();
+            args.putFloat(ARGUMENT_PLAYBACK_SPEED, speed);
+            sendCommand(COMMAND_CODE_PLAYBACK_SET_SPEED, args);
+        }
     }
 
     /**
diff --git a/media/src/main/java/androidx/media/MediaSession2ImplBase.java b/media/src/main/java/androidx/media/MediaSession2ImplBase.java
index ccba2fc..b7c32c2 100644
--- a/media/src/main/java/androidx/media/MediaSession2ImplBase.java
+++ b/media/src/main/java/androidx/media/MediaSession2ImplBase.java
@@ -1022,6 +1022,21 @@
             });
         }
 
+        @Override
+        public void onPlaybackSpeedChanged(final MediaPlayerBase mpb, final float speed) {
+            final MediaSession2ImplBase session = getSession();
+            if (session == null) {
+                return;
+            }
+            session.getCallbackExecutor().execute(new Runnable() {
+                @Override
+                public void run() {
+                    session.getCallback().onPlaybackSpeedChanged(session.getInstance(), mpb, speed);
+                    session.getSession2Stub().notifyPlaybackSpeedChanged(speed);
+                }
+            });
+        }
+
         private MediaSession2ImplBase getSession() {
             final MediaSession2ImplBase session = mSession.get();
             if (session == null && DEBUG) {
diff --git a/media/src/main/java/androidx/media/MediaSession2StubImplBase.java b/media/src/main/java/androidx/media/MediaSession2StubImplBase.java
index 1e3d3f3..9978e4c 100644
--- a/media/src/main/java/androidx/media/MediaSession2StubImplBase.java
+++ b/media/src/main/java/androidx/media/MediaSession2StubImplBase.java
@@ -29,6 +29,7 @@
 import static androidx.media.MediaConstants2.ARGUMENT_PACKAGE_NAME;
 import static androidx.media.MediaConstants2.ARGUMENT_PID;
 import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_INFO;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_SPEED;
 import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_STATE_COMPAT;
 import static androidx.media.MediaConstants2.ARGUMENT_PLAYER_STATE;
 import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST;
@@ -55,6 +56,7 @@
 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED;
 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ERROR;
 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED;
 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYER_STATE_CHANGED;
 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_CHANGED;
 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED;
@@ -68,6 +70,7 @@
 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PREPARE;
 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_RESET;
 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_SEEK_TO;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_SET_SPEED;
 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_ADD_ITEM;
 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REMOVE_ITEM;
 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REPLACE_ITEM;
@@ -415,6 +418,12 @@
                                 Bundle route = extras.getBundle(ARGUMENT_ROUTE_BUNDLE);
                                 mSession.getCallback().onSelectRoute(
                                         mSession.getInstance(), controller, route);
+                                break;
+                            }
+                            case COMMAND_CODE_PLAYBACK_SET_SPEED: {
+                                float speed = extras.getFloat(ARGUMENT_PLAYBACK_SPEED);
+                                mSession.setPlaybackSpeed(speed);
+                                break;
                             }
                         }
                     }
@@ -541,6 +550,19 @@
         });
     }
 
+    void notifyPlaybackSpeedChanged(final float speed) {
+        notifyAll(new Session2Runnable() {
+            @Override
+            public void run(ControllerInfo controller) throws RemoteException {
+                Bundle bundle = new Bundle();
+                bundle.putParcelable(
+                        ARGUMENT_PLAYBACK_STATE_COMPAT, mSession.getPlaybackStateCompat());
+                controller.getControllerBinder().onEvent(
+                        SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED, bundle);
+            }
+        });
+    }
+
     void notifyError(final int errorCode, final Bundle extras) {
         notifyAll(new Session2Runnable() {
             @Override
diff --git a/media/src/main/java/androidx/media/SessionCommand2.java b/media/src/main/java/androidx/media/SessionCommand2.java
index a07799b..aca8234 100644
--- a/media/src/main/java/androidx/media/SessionCommand2.java
+++ b/media/src/main/java/androidx/media/SessionCommand2.java
@@ -315,6 +315,15 @@
      */
     public static final int COMMAND_CODE_LIBRARY_UNSUBSCRIBE = 35;
 
+    /**
+     * Command code for {@link MediaController2#setPlaybackSpeed(float)}}.
+     * <p>
+     * Command would be sent directly to the player if the session doesn't reject the request
+     * through the {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo,
+     * SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_PLAYBACK_SET_SPEED = 39;
+
     private static final String KEY_COMMAND_CODE =
             "android.media.media_session2.command.command_code";
     private static final String KEY_COMMAND_CUSTOM_COMMAND =