MediaSession2: Introduce MediaSession2.Command / CommandGroup

Test: Run all tests once
Change-Id: I67d2b09a68bc47a3c9b09be146e8fca6584e5755
diff --git a/packages/MediaComponents/src/com/android/media/MediaController2Impl.java b/packages/MediaComponents/src/com/android/media/MediaController2Impl.java
index d7364da..1a27056 100644
--- a/packages/MediaComponents/src/com/android/media/MediaController2Impl.java
+++ b/packages/MediaComponents/src/com/android/media/MediaController2Impl.java
@@ -22,6 +22,9 @@
 import android.content.ServiceConnection;
 import android.media.IMediaSession2;
 import android.media.IMediaSession2Callback;
+import android.media.MediaSession2;
+import android.media.MediaSession2.Command;
+import android.media.MediaSession2.CommandGroup;
 import android.media.MediaController2;
 import android.media.MediaController2.ControllerCallback;
 import android.media.MediaPlayerBase;
@@ -29,6 +32,7 @@
 import android.media.SessionToken;
 import android.media.session.PlaybackState;
 import android.media.update.MediaController2Provider;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -197,66 +201,38 @@
 
     @Override
     public void play_impl() {
-        final IMediaSession2 binder = mSessionBinder;
-        if (binder != null) {
-            try {
-                binder.play(mSessionCallbackStub);
-            } catch (RemoteException e) {
-                Log.w(TAG, "Cannot connect to the service or the session is gone", e);
-            }
-        } else {
-            Log.w(TAG, "Session isn't active", new IllegalStateException());
-        }
+        sendCommand(MediaSession2.COMMAND_CODE_PLAYBACK_START);
     }
 
     @Override
     public void pause_impl() {
-        final IMediaSession2 binder = mSessionBinder;
-        if (binder != null) {
-            try {
-                binder.pause(mSessionCallbackStub);
-            } catch (RemoteException e) {
-                Log.w(TAG, "Cannot connect to the service or the session is gone", e);
-            }
-        } else {
-            Log.w(TAG, "Session isn't active", new IllegalStateException());
-        }
+        sendCommand(MediaSession2.COMMAND_CODE_PLAYBACK_PAUSE);
     }
 
     @Override
     public void stop_impl() {
-        final IMediaSession2 binder = mSessionBinder;
-        if (binder != null) {
-            try {
-                binder.stop(mSessionCallbackStub);
-            } catch (RemoteException e) {
-                Log.w(TAG, "Cannot connect to the service or the session is gone", e);
-            }
-        } else {
-            Log.w(TAG, "Session isn't active", new IllegalStateException());
-        }
+        sendCommand(MediaSession2.COMMAND_CODE_PLAYBACK_STOP);
     }
 
     @Override
     public void skipToPrevious_impl() {
-        final IMediaSession2 binder = mSessionBinder;
-        if (binder != null) {
-            try {
-                binder.skipToPrevious(mSessionCallbackStub);
-            } catch (RemoteException e) {
-                Log.w(TAG, "Cannot connect to the service or the session is gone", e);
-            }
-        } else {
-            Log.w(TAG, "Session isn't active", new IllegalStateException());
-        }
+        sendCommand(MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_PREV_ITEM);
     }
 
     @Override
     public void skipToNext_impl() {
+        sendCommand(MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM);
+    }
+
+    private void sendCommand(int code) {
+        // TODO(jaewan): optimization) Cache Command objects?
+        Command command = new Command(code);
+        // TODO(jaewan): Check if the command is in the allowed group.
+
         final IMediaSession2 binder = mSessionBinder;
         if (binder != null) {
             try {
-                binder.skipToNext(mSessionCallbackStub);
+                binder.sendCommand(mSessionCallbackStub, command.toBundle(), null);
             } catch (RemoteException e) {
                 Log.w(TAG, "Cannot connect to the service or the session is gone", e);
             }
@@ -356,13 +332,14 @@
 
     // Called when the result for connecting to the session was delivered.
     // Should be used without a lock to prevent potential deadlock.
-    private void onConnectionChangedNotLocked(IMediaSession2 sessionBinder, long commands) {
+    private void onConnectionChangedNotLocked(IMediaSession2 sessionBinder,
+            CommandGroup commandGroup) {
         if (DEBUG) {
             Log.d(TAG, "onConnectionChangedNotLocked sessionBinder=" + sessionBinder);
         }
         boolean release = false;
         try {
-            if (sessionBinder == null) {
+            if (sessionBinder == null || commandGroup == null) {
                 // Connection rejected.
                 release = true;
                 return;
@@ -391,7 +368,7 @@
             }
             // TODO(jaewan): Keep commands to prevents illegal API calls.
             mCallbackExecutor.execute(() -> {
-                mCallback.onConnected(commands);
+                mCallback.onConnected(commandGroup);
             });
             if (registerCallbackForPlaybackNeeded) {
                 registerCallbackForPlaybackNotLocked();
@@ -431,7 +408,7 @@
         }
 
         @Override
-        public void onConnectionChanged(IMediaSession2 sessionBinder, long commands)
+        public void onConnectionChanged(IMediaSession2 sessionBinder, Bundle commandGroup)
                 throws RuntimeException {
             final MediaController2Impl controller;
             try {
@@ -440,7 +417,8 @@
                 Log.w(TAG, "Don't fail silently here. Highly likely a bug");
                 return;
             }
-            controller.onConnectionChangedNotLocked(sessionBinder, commands);
+            controller.onConnectionChangedNotLocked(
+                    sessionBinder, CommandGroup.fromBundle(commandGroup));
         }
     }
 
diff --git a/packages/MediaComponents/src/com/android/media/MediaSession2Stub.java b/packages/MediaComponents/src/com/android/media/MediaSession2Stub.java
index 20e493e..bae02e5 100644
--- a/packages/MediaComponents/src/com/android/media/MediaSession2Stub.java
+++ b/packages/MediaComponents/src/com/android/media/MediaSession2Stub.java
@@ -22,10 +22,12 @@
 import android.media.IMediaSession2;
 import android.media.IMediaSession2Callback;
 import android.media.MediaSession2;
-import android.media.MediaSession2.CommandFlags;
+import android.media.MediaSession2.Command;
+import android.media.MediaSession2.CommandGroup;
 import android.media.MediaSession2.ControllerInfo;
 import android.media.session.PlaybackState;
 import android.os.Binder;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
@@ -73,7 +75,7 @@
                     ((ControllerInfoImpl) list.get(i).getProvider()).getControllerBinder();
             try {
                 // Should be used without a lock hold to prevent potential deadlock.
-                callbackBinder.onConnectionChanged(null, 0);
+                callbackBinder.onConnectionChanged(null, null);
             } catch (RemoteException e) {
                 // Controller is gone. Should be fine because we're destroying.
             }
@@ -110,32 +112,8 @@
     }
 
     @Override
-    public void play(IMediaSession2Callback caller) throws RemoteException {
-        onCommand(caller, MediaSession2.COMMAND_FLAG_PLAYBACK_START);
-    }
-
-    @Override
-    public void pause(IMediaSession2Callback caller) throws RemoteException {
-        onCommand(caller, MediaSession2.COMMAND_FLAG_PLAYBACK_PAUSE);
-    }
-
-    @Override
-    public void stop(IMediaSession2Callback caller) throws RemoteException {
-        onCommand(caller, MediaSession2.COMMAND_FLAG_PLAYBACK_STOP);
-    }
-
-    @Override
-    public void skipToPrevious(IMediaSession2Callback caller) throws RemoteException {
-        onCommand(caller, MediaSession2.COMMAND_FLAG_PLAYBACK_SKIP_PREV_ITEM);
-    }
-
-    @Override
-    public void skipToNext(IMediaSession2Callback caller) throws RemoteException {
-        onCommand(caller, MediaSession2.COMMAND_FLAG_PLAYBACK_SKIP_NEXT_ITEM);
-    }
-
-    private void onCommand(IMediaSession2Callback caller, @CommandFlags long command)
-            throws IllegalArgumentException {
+    public void sendCommand(IMediaSession2Callback caller, Bundle command, Bundle args)
+            throws RuntimeException {
         ControllerInfo controller = getController(caller);
         if (controller == null) {
             if (DEBUG) {
@@ -143,7 +121,7 @@
             }
             return;
         }
-        mCommandHandler.postCommand(controller, command);
+        mCommandHandler.postCommand(controller, Command.fromBundle(command), args);
     }
 
     @Deprecated
@@ -247,13 +225,12 @@
             switch (msg.what) {
                 case MSG_CONNECT:
                     ControllerInfo request = (ControllerInfo) msg.obj;
-                    long allowedCommands = session.getCallback().onConnect(request);
+                    CommandGroup allowedCommands = session.getCallback().onConnect(request);
                     // Don't reject connection for the request from trusted app.
                     // Otherwise server will fail to retrieve session's information to dispatch
                     // media keys to.
-                    boolean accept = (allowedCommands != 0) || request.isTrusted();
-                    ControllerInfoImpl impl =
-                            (ControllerInfoImpl) request.getProvider();
+                    boolean accept = allowedCommands != null || request.isTrusted();
+                    ControllerInfoImpl impl = (ControllerInfoImpl) request.getProvider();
                     if (accept) {
                         synchronized (mLock) {
                             mControllers.put(impl.getId(), request);
@@ -266,15 +243,16 @@
                     try {
                         impl.getControllerBinder().onConnectionChanged(
                                 accept ? MediaSession2Stub.this : null,
-                                allowedCommands);
+                                allowedCommands == null ? null : allowedCommands.toBundle());
                     } catch (RemoteException e) {
                         // Controller may be died prematurely.
                     }
                     break;
                 case MSG_COMMAND:
                     CommandParam param = (CommandParam) msg.obj;
-                    long command = param.command;
-                    boolean accepted = session.getCallback().onCommand(param.controller, command);
+                    Command command = param.command;
+                    boolean accepted = session.getCallback().onCommandRequest(
+                            param.controller, command);
                     if (!accepted) {
                         // Don't run rejected command.
                         if (DEBUG) {
@@ -284,19 +262,24 @@
                         return;
                     }
 
-                    // Switch cannot be used because command is long, but switch only supports
-                    // int.
-                    // TODO(jaewan): Replace this with the switch
-                    if (command == MediaSession2.COMMAND_FLAG_PLAYBACK_START) {
-                        session.getInstance().play();
-                    } else if (command == MediaSession2.COMMAND_FLAG_PLAYBACK_PAUSE) {
-                        session.getInstance().pause();
-                    } else if (command == MediaSession2.COMMAND_FLAG_PLAYBACK_STOP) {
-                        session.getInstance().stop();
-                    } else if (command == MediaSession2.COMMAND_FLAG_PLAYBACK_SKIP_PREV_ITEM) {
-                        session.getInstance().skipToPrevious();
-                    } else if (command == MediaSession2.COMMAND_FLAG_PLAYBACK_SKIP_NEXT_ITEM) {
-                        session.getInstance().skipToNext();
+                    switch (param.command.getCommandCode()) {
+                        case MediaSession2.COMMAND_CODE_PLAYBACK_START:
+                            session.getInstance().play();
+                            break;
+                        case MediaSession2.COMMAND_CODE_PLAYBACK_PAUSE:
+                            session.getInstance().pause();
+                            break;
+                        case MediaSession2.COMMAND_CODE_PLAYBACK_STOP:
+                            session.getInstance().stop();
+                            break;
+                        case MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_PREV_ITEM:
+                            session.getInstance().skipToPrevious();
+                            break;
+                        case MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM:
+                            session.getInstance().skipToNext();
+                            break;
+                        default:
+                            // TODO(jaewan): Handle custom command.
                     }
                     break;
             }
@@ -306,19 +289,21 @@
             obtainMessage(MSG_CONNECT, request).sendToTarget();
         }
 
-        public void postCommand(ControllerInfo controller, @CommandFlags long command) {
-            CommandParam param = new CommandParam(controller, command);
+        public void postCommand(ControllerInfo controller, Command command, Bundle args) {
+            CommandParam param = new CommandParam(controller, command, args);
             obtainMessage(MSG_COMMAND, param).sendToTarget();
         }
     }
 
     private static class CommandParam {
         public final ControllerInfo controller;
-        public final @CommandFlags long command;
+        public final Command command;
+        public final Bundle args;
 
-        private CommandParam(ControllerInfo controller, long command) {
+        private CommandParam(ControllerInfo controller, Command command, Bundle args) {
             this.controller = controller;
             this.command = command;
+            this.args = args;
         }
     }
 }
diff --git a/packages/MediaComponents/test/runtest.sh b/packages/MediaComponents/test/runtest.sh
index bda48b0..5c0ef51 100644
--- a/packages/MediaComponents/test/runtest.sh
+++ b/packages/MediaComponents/test/runtest.sh
@@ -124,7 +124,7 @@
       if [[ "${OPTION_MIN}" != "true" ]]; then
         build_targets="${build_targets} droid"
       fi
-      m ${build_targets} -j || (echo "Build failed. stop" ; break)
+      m ${build_targets} -j || break
 
       ${adb} root
       ${adb} remount
diff --git a/packages/MediaComponents/test/src/android/media/MediaController2Test.java b/packages/MediaComponents/test/src/android/media/MediaController2Test.java
index 161a463..38d34cc 100644
--- a/packages/MediaComponents/test/src/android/media/MediaController2Test.java
+++ b/packages/MediaComponents/test/src/android/media/MediaController2Test.java
@@ -205,8 +205,8 @@
     public void testControllerCallback_sessionRejects() throws InterruptedException {
         final MediaSession2.SessionCallback sessionCallback = new SessionCallback() {
             @Override
-            public long onConnect(ControllerInfo controller) {
-                return 0;
+            public MediaSession2.CommandGroup onConnect(ControllerInfo controller) {
+                return null;
             }
         };
         sHandler.postAndSync(() -> {
diff --git a/packages/MediaComponents/test/src/android/media/MediaSession2Test.java b/packages/MediaComponents/test/src/android/media/MediaSession2Test.java
index a77fd63..f7224ea 100644
--- a/packages/MediaComponents/test/src/android/media/MediaSession2Test.java
+++ b/packages/MediaComponents/test/src/android/media/MediaSession2Test.java
@@ -209,15 +209,15 @@
         assertFalse(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
         assertFalse(mPlayer.mPauseCalled);
         assertEquals(1, callback.commands.size());
-        assertEquals(MediaSession2.COMMAND_FLAG_PLAYBACK_PAUSE,
-                (long) callback.commands.get(0));
+        assertEquals(MediaSession2.COMMAND_CODE_PLAYBACK_PAUSE,
+                (long) callback.commands.get(0).getCommandCode());
         controller.skipToNext();
         assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
         assertTrue(mPlayer.mSkipToNextCalled);
         assertFalse(mPlayer.mPauseCalled);
         assertEquals(2, callback.commands.size());
-        assertEquals(MediaSession2.COMMAND_FLAG_PLAYBACK_SKIP_NEXT_ITEM,
-                (long) callback.commands.get(1));
+        assertEquals(MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM,
+                (long) callback.commands.get(1).getCommandCode());
     }
 
     @Test
@@ -236,28 +236,28 @@
 
     public class MockOnConnectCallback extends SessionCallback {
         @Override
-        public long onConnect(ControllerInfo controllerInfo) {
+        public MediaSession2.CommandGroup onConnect(ControllerInfo controllerInfo) {
             if (Process.myUid() != controllerInfo.getUid()) {
-                return 0;
+                return null;
             }
             assertEquals(mContext.getPackageName(), controllerInfo.getPackageName());
             assertEquals(Process.myUid(), controllerInfo.getUid());
             assertFalse(controllerInfo.isTrusted());
             // Reject all
-            return 0;
+            return null;
         }
     }
 
     public class MockOnCommandCallback extends SessionCallback {
-        public final ArrayList<Long> commands = new ArrayList<>();
+        public final ArrayList<MediaSession2.Command> commands = new ArrayList<>();
 
         @Override
-        public boolean onCommand(ControllerInfo controllerInfo, long command) {
+        public boolean onCommandRequest(ControllerInfo controllerInfo, MediaSession2.Command command) {
             assertEquals(mContext.getPackageName(), controllerInfo.getPackageName());
             assertEquals(Process.myUid(), controllerInfo.getUid());
             assertFalse(controllerInfo.isTrusted());
             commands.add(command);
-            if (command == MediaSession2.COMMAND_FLAG_PLAYBACK_PAUSE) {
+            if (command.getCommandCode() == MediaSession2.COMMAND_CODE_PLAYBACK_PAUSE) {
                 return false;
             }
             return true;
diff --git a/packages/MediaComponents/test/src/android/media/MediaSession2TestBase.java b/packages/MediaComponents/test/src/android/media/MediaSession2TestBase.java
index 80b8b79..ab842c4 100644
--- a/packages/MediaComponents/test/src/android/media/MediaSession2TestBase.java
+++ b/packages/MediaComponents/test/src/android/media/MediaSession2TestBase.java
@@ -20,6 +20,7 @@
 import static junit.framework.Assert.assertTrue;
 
 import android.content.Context;
+import android.media.MediaSession2.CommandGroup;
 import android.os.HandlerThread;
 import android.support.annotation.CallSuper;
 import android.support.annotation.NonNull;
@@ -106,7 +107,7 @@
 
         @CallSuper
         @Override
-        public void onConnected(long commands) {
+        public void onConnected(CommandGroup commands) {
             super.onConnected(commands);
             connectLatch.countDown();
         }
diff --git a/packages/MediaComponents/test/src/android/media/MediaSessionManager_MediaSession2.java b/packages/MediaComponents/test/src/android/media/MediaSessionManager_MediaSession2.java
index 552f0a6..0ee37b1 100644
--- a/packages/MediaComponents/test/src/android/media/MediaSessionManager_MediaSession2.java
+++ b/packages/MediaComponents/test/src/android/media/MediaSessionManager_MediaSession2.java
@@ -115,9 +115,9 @@
             mSession = new MediaSession2.Builder(mContext, new MockPlayer(0)).setId(TAG)
                     .setSessionCallback(new SessionCallback() {
                         @Override
-                        public long onConnect(ControllerInfo controller) {
+                        public MediaSession2.CommandGroup onConnect(ControllerInfo controller) {
                             // Reject all connection request.
-                            return 0;
+                            return null;
                         }
                     }).build();
         });
diff --git a/packages/MediaComponents/test/src/android/media/MockMediaSessionService2.java b/packages/MediaComponents/test/src/android/media/MockMediaSessionService2.java
index 0fa2f52..e4a7485 100644
--- a/packages/MediaComponents/test/src/android/media/MockMediaSessionService2.java
+++ b/packages/MediaComponents/test/src/android/media/MockMediaSessionService2.java
@@ -59,7 +59,7 @@
 
     private class MySessionCallback extends SessionCallback {
         @Override
-        public long onConnect(ControllerInfo controller) {
+        public MediaSession2.CommandGroup onConnect(ControllerInfo controller) {
             if (Process.myUid() != controller.getUid()) {
                 // It's system app wants to listen changes. Ignore.
                 return super.onConnect(controller);