Make MediaSession2.Command updatable

Bug: 72619281
Test: build & runtest-MediaComponents
Change-Id: I917caaa09dfdc5dd981a555277a2a266dac8f5a0
diff --git a/packages/MediaComponents/src/com/android/media/MediaController2Impl.java b/packages/MediaComponents/src/com/android/media/MediaController2Impl.java
index 30e32ec..4144342 100644
--- a/packages/MediaComponents/src/com/android/media/MediaController2Impl.java
+++ b/packages/MediaComponents/src/com/android/media/MediaController2Impl.java
@@ -53,10 +53,9 @@
     private static final boolean DEBUG = true; // TODO(jaewan): Change
 
     private final MediaController2 mInstance;
-
+    private final Context mContext;
     private final Object mLock = new Object();
 
-    private final Context mContext;
     private final MediaSession2CallbackStub mSessionCallbackStub;
     private final SessionToken2 mToken;
     private final ControllerCallback mCallback;
@@ -209,6 +208,10 @@
         return mCallbackExecutor;
     }
 
+    Context getContext() {
+      return mContext;
+    }
+
     @Override
     public SessionToken2 getSessionToken_impl() {
         return mToken;
@@ -606,7 +609,7 @@
                 return;
             }
             controller.onConnectionChangedNotLocked(
-                    sessionBinder, CommandGroup.fromBundle(commandGroup));
+                    sessionBinder, CommandGroup.fromBundle(controller.getContext(), commandGroup));
         }
 
         @Override
@@ -645,7 +648,8 @@
             }
             List<CommandButton> layout = new ArrayList<>();
             for (int i = 0; i < commandButtonlist.size(); i++) {
-                CommandButton button = CommandButton.fromBundle(commandButtonlist.get(i));
+                CommandButton button = CommandButton.fromBundle(
+                        browser.getContext(), commandButtonlist.get(i));
                 if (button != null) {
                     layout.add(button);
                 }
@@ -662,7 +666,7 @@
                 Log.w(TAG, "Don't fail silently here. Highly likely a bug");
                 return;
             }
-            Command command = Command.fromBundle(commandBundle);
+            Command command = Command.fromBundle(controller.getContext(), commandBundle);
             if (command == null) {
                 return;
             }
diff --git a/packages/MediaComponents/src/com/android/media/MediaSession2Impl.java b/packages/MediaComponents/src/com/android/media/MediaSession2Impl.java
index 7c36739..5bb608d 100644
--- a/packages/MediaComponents/src/com/android/media/MediaSession2Impl.java
+++ b/packages/MediaComponents/src/com/android/media/MediaSession2Impl.java
@@ -20,6 +20,8 @@
 import static android.media.SessionToken2.TYPE_SESSION;
 import static android.media.SessionToken2.TYPE_SESSION_SERVICE;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.Manifest.permission;
 import android.app.PendingIntent;
 import android.content.Context;
@@ -520,6 +522,96 @@
         }
     }
 
+    public static final class CommandImpl implements CommandProvider {
+        private static final String KEY_COMMAND_CODE
+                = "android.media.media_session2.command.command_code";
+        private static final String KEY_COMMAND_CUSTOM_COMMAND
+                = "android.media.media_session2.command.custom_command";
+        private static final String KEY_COMMAND_EXTRA
+                = "android.media.media_session2.command.extra";
+
+        private final Command mInstance;
+        private final int mCommandCode;
+        // Nonnull if it's custom command
+        private final String mCustomCommand;
+        private final Bundle mExtra;
+
+        public CommandImpl(Command instance, int commandCode) {
+            mInstance = instance;
+            mCommandCode = commandCode;
+            mCustomCommand = null;
+            mExtra = null;
+        }
+
+        public CommandImpl(Command instance, @NonNull String action, @Nullable Bundle extra) {
+            if (action == null) {
+                throw new IllegalArgumentException("action shouldn't be null");
+            }
+            mInstance = instance;
+            mCommandCode = MediaSession2.COMMAND_CODE_CUSTOM;
+            mCustomCommand = action;
+            mExtra = extra;
+        }
+
+        public int getCommandCode_impl() {
+            return mCommandCode;
+        }
+
+        public @Nullable String getCustomCommand_impl() {
+            return mCustomCommand;
+        }
+
+        public @Nullable Bundle getExtra_impl() {
+            return mExtra;
+        }
+
+        /**
+         * @ 7return a new Bundle instance from the Command
+         */
+        public Bundle toBundle_impl() {
+            Bundle bundle = new Bundle();
+            bundle.putInt(KEY_COMMAND_CODE, mCommandCode);
+            bundle.putString(KEY_COMMAND_CUSTOM_COMMAND, mCustomCommand);
+            bundle.putBundle(KEY_COMMAND_EXTRA, mExtra);
+            return bundle;
+        }
+
+        /**
+         * @return a new Command instance from the Bundle
+         */
+        public static Command fromBundle_impl(Context context, Bundle command) {
+            int code = command.getInt(KEY_COMMAND_CODE);
+            if (code != MediaSession2.COMMAND_CODE_CUSTOM) {
+                return new Command(context, code);
+            } else {
+                String customCommand = command.getString(KEY_COMMAND_CUSTOM_COMMAND);
+                if (customCommand == null) {
+                    return null;
+                }
+                return new Command(context, customCommand, command.getBundle(KEY_COMMAND_EXTRA));
+            }
+        }
+
+        @Override
+        public boolean equals_impl(Object obj) {
+            if (!(obj instanceof CommandImpl)) {
+                return false;
+            }
+            CommandImpl other = (CommandImpl) obj;
+            // TODO(jaewan): Should we also compare contents in bundle?
+            //               It may not be possible if the bundle contains private class.
+            return mCommandCode == other.mCommandCode
+                    && TextUtils.equals(mCustomCommand, other.mCustomCommand);
+        }
+
+        @Override
+        public int hashCode_impl() {
+            final int prime = 31;
+            return ((mCustomCommand != null)
+                    ? mCustomCommand.hashCode() : 0) * prime + mCommandCode;
+        }
+    }
+
     public static class ControllerInfoImpl implements ControllerInfoProvider {
         private final ControllerInfo mInstance;
         private final int mUid;
diff --git a/packages/MediaComponents/src/com/android/media/MediaSession2Stub.java b/packages/MediaComponents/src/com/android/media/MediaSession2Stub.java
index 4fc69b9..4bb5f47 100644
--- a/packages/MediaComponents/src/com/android/media/MediaSession2Stub.java
+++ b/packages/MediaComponents/src/com/android/media/MediaSession2Stub.java
@@ -16,6 +16,7 @@
 
 package com.android.media;
 
+import android.content.Context;
 import android.media.MediaItem2;
 import android.media.MediaLibraryService2.BrowserRoot;
 import android.media.MediaLibraryService2.MediaLibrarySessionCallback;
@@ -92,7 +93,8 @@
     public void connect(String callingPackage, final IMediaSession2Callback callback)
             throws RuntimeException {
         final MediaSession2Impl sessionImpl = getSession();
-        final ControllerInfo request = new ControllerInfo(sessionImpl.getContext(),
+        final Context context = sessionImpl.getContext();
+        final ControllerInfo request = new ControllerInfo(context,
                 Binder.getCallingUid(), Binder.getCallingPid(), callingPackage, callback);
         sessionImpl.getCallbackExecutor().execute(() -> {
             final MediaSession2Impl session = mSession.get();
@@ -111,7 +113,7 @@
                 }
                 if (allowedCommands == null) {
                     // For trusted apps, send non-null allowed commands to keep connection.
-                    allowedCommands = new CommandGroup();
+                    allowedCommands = new CommandGroup(context);
                 }
             }
             if (DEBUG) {
@@ -178,7 +180,7 @@
                 return;
             }
             // TODO(jaewan): Sanity check.
-            Command command = new Command(commandCode);
+            Command command = new Command(session.getContext(), commandCode);
             boolean accepted = session.getCallback().onCommandRequest(controller, command);
             if (!accepted) {
                 // Don't run rejected command.
@@ -248,7 +250,7 @@
             if (session == null) {
                 return;
             }
-            final Command command = Command.fromBundle(commandBundle);
+            final Command command = Command.fromBundle(session.getContext(), commandBundle);
             session.getCallback().onCustomCommand(controller, command, args, receiver);
         });
     }
diff --git a/packages/MediaComponents/src/com/android/media/SessionToken2Impl.java b/packages/MediaComponents/src/com/android/media/SessionToken2Impl.java
index c91a89c..b2b7959 100644
--- a/packages/MediaComponents/src/com/android/media/SessionToken2Impl.java
+++ b/packages/MediaComponents/src/com/android/media/SessionToken2Impl.java
@@ -152,7 +152,7 @@
         return mSessionBinder;
     }
 
-    public static SessionToken2 fromBundle(Context context, Bundle bundle) {
+    public static SessionToken2 fromBundle_impl(Context context, Bundle bundle) {
         if (bundle == null) {
             return null;
         }
diff --git a/packages/MediaComponents/src/com/android/media/update/ApiFactory.java b/packages/MediaComponents/src/com/android/media/update/ApiFactory.java
index b9d7612..0fc1ac1 100644
--- a/packages/MediaComponents/src/com/android/media/update/ApiFactory.java
+++ b/packages/MediaComponents/src/com/android/media/update/ApiFactory.java
@@ -32,6 +32,7 @@
 import android.media.MediaMetadata2;
 import android.media.MediaPlayerInterface;
 import android.media.MediaSession2;
+import android.media.MediaSession2.Command;
 import android.media.MediaSession2.ControllerInfo;
 import android.media.MediaSession2.SessionCallback;
 import android.media.MediaSessionService2;
@@ -110,6 +111,20 @@
     }
 
     @Override
+    public MediaSession2Provider.CommandProvider createMediaSession2Command(Command instance,
+            int commandCode, String action, Bundle extra) {
+        if (action == null && extra == null) {
+            return new MediaSession2Impl.CommandImpl(instance, commandCode);
+        }
+        return new MediaSession2Impl.CommandImpl(instance, action, extra);
+    }
+
+    @Override
+    public Command fromBundle_MediaSession2Command(Context context, Bundle command) {
+        return MediaSession2Impl.CommandImpl.fromBundle_impl(context, command);
+    }
+
+    @Override
     public MediaSessionService2Provider createMediaSessionService2(
             MediaSessionService2 instance) {
         return new MediaSessionService2Impl(instance);
@@ -151,7 +166,7 @@
 
     @Override
     public SessionToken2 SessionToken2_fromBundle(Context context, Bundle bundle) {
-        return SessionToken2Impl.fromBundle(context, bundle);
+        return SessionToken2Impl.fromBundle_impl(context, bundle);
     }
 
     @Override
diff --git a/packages/MediaComponents/test/src/android/media/MediaController2Test.java b/packages/MediaComponents/test/src/android/media/MediaController2Test.java
index 9c5aa21..27dbaf8 100644
--- a/packages/MediaComponents/test/src/android/media/MediaController2Test.java
+++ b/packages/MediaComponents/test/src/android/media/MediaController2Test.java
@@ -68,7 +68,7 @@
         // Create this test specific MediaSession2 to use our own Handler.
         mPlayer = new MockPlayer(1);
         mSession = new MediaSession2.Builder(mContext, mPlayer)
-                .setSessionCallback(sHandlerExecutor, new SessionCallback())
+                .setSessionCallback(sHandlerExecutor, new SessionCallback(mContext))
                 .setId(TAG).build();
         mController = createController(mSession.getToken());
         TestServiceRegistry.getInstance().setHandler(sHandler);
@@ -256,12 +256,13 @@
     @Test
     public void testSendCustomCommand() throws InterruptedException {
         // TODO(jaewan): Need to revisit with the permission.
-        final Command testCommand = new Command(MediaSession2.COMMAND_CODE_PLAYBACK_PREPARE);
+        final Command testCommand =
+                new Command(mContext, MediaSession2.COMMAND_CODE_PLAYBACK_PREPARE);
         final Bundle testArgs = new Bundle();
         testArgs.putString("args", "testSendCustomAction");
 
         final CountDownLatch latch = new CountDownLatch(1);
-        final SessionCallback callback = new SessionCallback() {
+        final SessionCallback callback = new SessionCallback(mContext) {
             @Override
             public void onCustomCommand(ControllerInfo controller, Command customCommand,
                     Bundle args, ResultReceiver cb) {
@@ -291,7 +292,7 @@
 
     @Test
     public void testControllerCallback_sessionRejects() throws InterruptedException {
-        final MediaSession2.SessionCallback sessionCallback = new SessionCallback() {
+        final MediaSession2.SessionCallback sessionCallback = new SessionCallback(mContext) {
             @Override
             public MediaSession2.CommandGroup onConnect(ControllerInfo controller) {
                 return null;
@@ -357,7 +358,7 @@
             final MockPlayer player = new MockPlayer(0);
             sessionHandler.postAndSync(() -> {
                 mSession = new MediaSession2.Builder(mContext, mPlayer)
-                        .setSessionCallback(sHandlerExecutor, new SessionCallback())
+                        .setSessionCallback(sHandlerExecutor, new SessionCallback(mContext))
                         .setId("testDeadlock").build();
             });
             final MediaController2 controller = createController(mSession.getToken());
@@ -545,7 +546,7 @@
             // Recreated session has different session stub, so previously created controller
             // shouldn't be available.
             mSession = new MediaSession2.Builder(mContext, mPlayer)
-                    .setSessionCallback(sHandlerExecutor, new SessionCallback())
+                    .setSessionCallback(sHandlerExecutor, new SessionCallback(mContext))
                     .setId(id).build();
         });
         testNoInteraction();
diff --git a/packages/MediaComponents/test/src/android/media/MediaSession2Test.java b/packages/MediaComponents/test/src/android/media/MediaSession2Test.java
index 6b10ccc..c5bcfff 100644
--- a/packages/MediaComponents/test/src/android/media/MediaSession2Test.java
+++ b/packages/MediaComponents/test/src/android/media/MediaSession2Test.java
@@ -66,7 +66,7 @@
         super.setUp();
         mPlayer = new MockPlayer(0);
         mSession = new MediaSession2.Builder(mContext, mPlayer)
-                .setSessionCallback(sHandlerExecutor, new SessionCallback()).build();
+                .setSessionCallback(sHandlerExecutor, new SessionCallback(mContext)).build();
     }
 
     @After
@@ -295,7 +295,8 @@
 
     @Test
     public void testSendCustomAction() throws InterruptedException {
-        final Command testCommand = new Command(MediaSession2.COMMAND_CODE_PLAYBACK_PREPARE);
+        final Command testCommand =
+                new Command(mContext, MediaSession2.COMMAND_CODE_PLAYBACK_PREPARE);
         final Bundle testArgs = new Bundle();
         testArgs.putString("args", "testSendCustomAction");
 
@@ -335,6 +336,10 @@
     }
 
     public class MockOnConnectCallback extends SessionCallback {
+        public MockOnConnectCallback() {
+            super(mContext);
+        }
+
         @Override
         public MediaSession2.CommandGroup onConnect(ControllerInfo controllerInfo) {
             if (Process.myUid() != controllerInfo.getUid()) {
@@ -351,6 +356,10 @@
     public class MockOnCommandCallback extends SessionCallback {
         public final ArrayList<MediaSession2.Command> commands = new ArrayList<>();
 
+        public MockOnCommandCallback() {
+            super(mContext);
+        }
+
         @Override
         public boolean onCommandRequest(ControllerInfo controllerInfo, MediaSession2.Command command) {
             assertEquals(mContext.getPackageName(), controllerInfo.getPackageName());
diff --git a/packages/MediaComponents/test/src/android/media/MediaSessionManager_MediaSession2.java b/packages/MediaComponents/test/src/android/media/MediaSessionManager_MediaSession2.java
index d0106fa..96ae8b7 100644
--- a/packages/MediaComponents/test/src/android/media/MediaSessionManager_MediaSession2.java
+++ b/packages/MediaComponents/test/src/android/media/MediaSessionManager_MediaSession2.java
@@ -60,7 +60,9 @@
         // per test thread differs across the {@link MediaSession2} with the same TAG.
         final MockPlayer player = new MockPlayer(1);
         mSession = new MediaSession2.Builder(mContext, player)
-                .setSessionCallback(sHandlerExecutor, new SessionCallback()).setId(TAG).build();
+                .setSessionCallback(sHandlerExecutor, new SessionCallback(mContext))
+                .setId(TAG)
+                .build();
         ensureChangeInSession();
     }
 
@@ -109,7 +111,7 @@
         sHandler.postAndSync(() -> {
             mSession.close();
             mSession = new MediaSession2.Builder(mContext, new MockPlayer(0)).setId(TAG)
-                    .setSessionCallback(sHandlerExecutor, new SessionCallback() {
+                    .setSessionCallback(sHandlerExecutor, new SessionCallback(mContext) {
                         @Override
                         public MediaSession2.CommandGroup onConnect(ControllerInfo controller) {
                             // Reject all connection request.
diff --git a/packages/MediaComponents/test/src/android/media/MockMediaLibraryService2.java b/packages/MediaComponents/test/src/android/media/MockMediaLibraryService2.java
index c1187c2..6e1501a 100644
--- a/packages/MediaComponents/test/src/android/media/MockMediaLibraryService2.java
+++ b/packages/MediaComponents/test/src/android/media/MockMediaLibraryService2.java
@@ -81,6 +81,10 @@
     }
 
     private class TestLibrarySessionCallback extends MediaLibrarySessionCallback {
+        public TestLibrarySessionCallback() {
+            super(MockMediaLibraryService2.this);
+        }
+
         @Override
         public CommandGroup onConnect(ControllerInfo controller) {
             if (Process.myUid() != controller.getUid()) {
diff --git a/packages/MediaComponents/test/src/android/media/MockMediaSessionService2.java b/packages/MediaComponents/test/src/android/media/MockMediaSessionService2.java
index 5c5c7d2..d85875e 100644
--- a/packages/MediaComponents/test/src/android/media/MockMediaSessionService2.java
+++ b/packages/MediaComponents/test/src/android/media/MockMediaSessionService2.java
@@ -91,6 +91,10 @@
     }
 
     private class MySessionCallback extends SessionCallback {
+        public MySessionCallback() {
+            super(MockMediaSessionService2.this);
+        }
+
         @Override
         public MediaSession2.CommandGroup onConnect(ControllerInfo controller) {
             if (Process.myUid() != controller.getUid()) {