Merge "MediaSession2: Implement setAllowedCommands()" into pi-dev
diff --git a/packages/MediaComponents/src/com/android/media/IMediaSession2Callback.aidl b/packages/MediaComponents/src/com/android/media/IMediaSession2Callback.aidl
index 9ee4928..3d812f8 100644
--- a/packages/MediaComponents/src/com/android/media/IMediaSession2Callback.aidl
+++ b/packages/MediaComponents/src/com/android/media/IMediaSession2Callback.aidl
@@ -41,6 +41,7 @@
     void onDisconnected();
 
     void onCustomLayoutChanged(in List<Bundle> commandButtonlist);
+    void onAllowedCommandsChanged(in Bundle commands);
 
     void onCustomCommand(in Bundle command, in Bundle args, in ResultReceiver receiver);
 
diff --git a/packages/MediaComponents/src/com/android/media/MediaController2Impl.java b/packages/MediaComponents/src/com/android/media/MediaController2Impl.java
index c76cd6c..dd23148 100644
--- a/packages/MediaComponents/src/com/android/media/MediaController2Impl.java
+++ b/packages/MediaComponents/src/com/android/media/MediaController2Impl.java
@@ -776,6 +776,12 @@
         });
     }
 
+    void onAllowedCommandsChanged(final CommandGroup commands) {
+        mCallbackExecutor.execute(() -> {
+            mCallback.onAllowedCommandsChanged(mInstance, commands);
+        });
+    }
+
     void onCustomLayoutChanged(final List<CommandButton> layout) {
         mCallbackExecutor.execute(() -> {
             mCallback.onCustomLayoutChanged(mInstance, layout);
diff --git a/packages/MediaComponents/src/com/android/media/MediaSession2CallbackStub.java b/packages/MediaComponents/src/com/android/media/MediaSession2CallbackStub.java
index b8e651e..451368f 100644
--- a/packages/MediaComponents/src/com/android/media/MediaSession2CallbackStub.java
+++ b/packages/MediaComponents/src/com/android/media/MediaSession2CallbackStub.java
@@ -215,6 +215,27 @@
     }
 
     @Override
+    public void onAllowedCommandsChanged(Bundle commandsBundle) {
+        final MediaController2Impl controller;
+        try {
+            controller = getController();
+        } catch (IllegalStateException e) {
+            Log.w(TAG, "Don't fail silently here. Highly likely a bug");
+            return;
+        }
+        if (controller == null) {
+            // TODO(jaewan): Revisit here. Could be a bug
+            return;
+        }
+        CommandGroup commands = CommandGroup.fromBundle(controller.getContext(), commandsBundle);
+        if (commands == null) {
+            Log.w(TAG, "onAllowedCommandsChanged(): Ignoring null commands");
+            return;
+        }
+        controller.onAllowedCommandsChanged(commands);
+    }
+
+    @Override
     public void onCustomCommand(Bundle commandBundle, Bundle args, ResultReceiver receiver) {
         final MediaController2Impl controller;
         try {
diff --git a/packages/MediaComponents/src/com/android/media/MediaSession2Impl.java b/packages/MediaComponents/src/com/android/media/MediaSession2Impl.java
index 2a28291..1f24d4e 100644
--- a/packages/MediaComponents/src/com/android/media/MediaSession2Impl.java
+++ b/packages/MediaComponents/src/com/android/media/MediaSession2Impl.java
@@ -429,7 +429,13 @@
 
     @Override
     public void setAllowedCommands_impl(ControllerInfo controller, CommandGroup commands) {
-        // TODO(jaewan): Implement
+        if (controller == null) {
+            throw new IllegalArgumentException("controller shouldn't be null");
+        }
+        if (commands == null) {
+            throw new IllegalArgumentException("commands shouldn't be null");
+        }
+        mSessionStub.setAllowedCommands(controller, commands);
     }
 
     @Override
diff --git a/packages/MediaComponents/src/com/android/media/MediaSession2Stub.java b/packages/MediaComponents/src/com/android/media/MediaSession2Stub.java
index ae4b17f..d4164f6 100644
--- a/packages/MediaComponents/src/com/android/media/MediaSession2Stub.java
+++ b/packages/MediaComponents/src/com/android/media/MediaSession2Stub.java
@@ -958,6 +958,22 @@
         }
     }
 
+    public void setAllowedCommands(ControllerInfo controller, CommandGroup commands) {
+        synchronized (mLock) {
+            mAllowedCommandGroupMap.put(controller, commands);
+        }
+        final IMediaSession2Callback controllerBinder = getControllerBinderIfAble(controller);
+        if (controllerBinder == null) {
+            return;
+        }
+        try {
+            controllerBinder.onAllowedCommandsChanged(commands.toBundle());
+        } catch (RemoteException e) {
+            Log.w(TAG, "Controller is gone", e);
+            // TODO(jaewan): What to do when the controller is gone?
+        }
+    }
+
     public void sendCustomCommand(ControllerInfo controller, Command command, Bundle args,
             ResultReceiver receiver) {
         if (receiver != null && controller == null) {
diff --git a/packages/MediaComponents/test/src/android/media/MediaBrowser2Test.java b/packages/MediaComponents/test/src/android/media/MediaBrowser2Test.java
index 07f7711..e58bd02 100644
--- a/packages/MediaComponents/test/src/android/media/MediaBrowser2Test.java
+++ b/packages/MediaComponents/test/src/android/media/MediaBrowser2Test.java
@@ -499,13 +499,17 @@
             mCallbackProxy.onCustomCommand(command, args, receiver);
         }
 
-
         @Override
         public void onCustomLayoutChanged(MediaController2 controller, List<CommandButton> layout) {
             mCallbackProxy.onCustomLayoutChanged(layout);
         }
 
         @Override
+        public void onAllowedCommandsChanged(MediaController2 controller, CommandGroup commands) {
+            mCallbackProxy.onAllowedCommandsChanged(commands);
+        }
+
+        @Override
         public void onGetLibraryRootDone(MediaBrowser2 browser, Bundle rootHints,
                 String rootMediaId, Bundle rootExtra) {
             super.onGetLibraryRootDone(browser, rootHints, rootMediaId, rootExtra);
diff --git a/packages/MediaComponents/test/src/android/media/MediaSession2Test.java b/packages/MediaComponents/test/src/android/media/MediaSession2Test.java
index 2c7de5c..afb191f 100644
--- a/packages/MediaComponents/test/src/android/media/MediaSession2Test.java
+++ b/packages/MediaComponents/test/src/android/media/MediaSession2Test.java
@@ -42,7 +42,6 @@
 import android.support.annotation.NonNull;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
-import android.text.TextUtils;
 
 import org.junit.After;
 import org.junit.Before;
@@ -426,6 +425,38 @@
     }
 
     @Test
+    public void testSetAllowedCommands() throws InterruptedException {
+        final CommandGroup commands = new CommandGroup(mContext);
+        commands.addCommand(new Command(mContext, MediaSession2.COMMAND_CODE_PLAYBACK_PLAY));
+        commands.addCommand(new Command(mContext, MediaSession2.COMMAND_CODE_PLAYBACK_PAUSE));
+        commands.addCommand(new Command(mContext, MediaSession2.COMMAND_CODE_PLAYBACK_STOP));
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        final TestControllerCallbackInterface callback = new TestControllerCallbackInterface() {
+            @Override
+            public void onAllowedCommandsChanged(CommandGroup commandsOut) {
+                assertNotNull(commandsOut);
+                List<Command> expected = commands.getCommands();
+                List<Command> actual = commandsOut.getCommands();
+
+                assertNotNull(actual);
+                assertEquals(expected.size(), actual.size());
+                for (int i = 0; i < expected.size(); i++) {
+                    assertEquals(expected.get(i), actual.get(i));
+                }
+                latch.countDown();
+            }
+        };
+
+        final MediaController2 controller = createController(mSession.getToken(), true, callback);
+        ControllerInfo controllerInfo = getTestControllerInfo();
+        assertNotNull(controllerInfo);
+
+        mSession.setAllowedCommands(controllerInfo, commands);
+        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
     public void testSendCustomAction() throws InterruptedException {
         final Command testCommand =
                 new Command(mContext, MediaSession2.COMMAND_CODE_PLAYBACK_PREPARE);
@@ -457,13 +488,12 @@
     private ControllerInfo getTestControllerInfo() {
         List<ControllerInfo> controllers = mSession.getConnectedControllers();
         assertNotNull(controllers);
-        final String packageName = mContext.getPackageName();
         for (int i = 0; i < controllers.size(); i++) {
-            if (TextUtils.equals(packageName, controllers.get(i).getPackageName())) {
+            if (Process.myUid() == controllers.get(i).getUid()) {
                 return controllers.get(i);
             }
         }
-        fail("Fails to get custom command");
+        fail("Failed to get test controller info");
         return null;
     }
 
diff --git a/packages/MediaComponents/test/src/android/media/MediaSession2TestBase.java b/packages/MediaComponents/test/src/android/media/MediaSession2TestBase.java
index b32400f..83706c0 100644
--- a/packages/MediaComponents/test/src/android/media/MediaSession2TestBase.java
+++ b/packages/MediaComponents/test/src/android/media/MediaSession2TestBase.java
@@ -69,6 +69,7 @@
         default void onPlaybackInfoChanged(MediaController2.PlaybackInfo info) {}
         default void onPlaybackStateChanged(PlaybackState2 state) {}
         default void onCustomLayoutChanged(List<CommandButton> layout) {}
+        default void onAllowedCommandsChanged(CommandGroup commands) {}
         default void onCustomCommand(Command command, Bundle args, ResultReceiver receiver) {}
     }
 
@@ -247,6 +248,11 @@
         public void onCustomLayoutChanged(MediaController2 controller, List<CommandButton> layout) {
             mCallbackProxy.onCustomLayoutChanged(layout);
         }
+
+        @Override
+        public void onAllowedCommandsChanged(MediaController2 controller, CommandGroup commands) {
+            mCallbackProxy.onAllowedCommandsChanged(commands);
+        }
     }
 
     public class TestMediaController extends MediaController2 implements TestControllerInterface {
diff --git a/packages/MediaComponents/test/src/android/media/MediaSession2_PermissionTest.java b/packages/MediaComponents/test/src/android/media/MediaSession2_PermissionTest.java
index ae818e2..ca36513 100644
--- a/packages/MediaComponents/test/src/android/media/MediaSession2_PermissionTest.java
+++ b/packages/MediaComponents/test/src/android/media/MediaSession2_PermissionTest.java
@@ -16,13 +16,33 @@
 
 package android.media;
 
-import static android.media.MediaSession2.*;
+import static android.media.MediaSession2.COMMAND_CODE_PLAYBACK_FAST_FORWARD;
+import static android.media.MediaSession2.COMMAND_CODE_PLAYBACK_PAUSE;
+import static android.media.MediaSession2.COMMAND_CODE_PLAYBACK_PLAY;
+import static android.media.MediaSession2.COMMAND_CODE_PLAYBACK_REWIND;
+import static android.media.MediaSession2.COMMAND_CODE_PLAYBACK_SEEK_TO;
+import static android.media.MediaSession2.COMMAND_CODE_PLAYBACK_SET_PLAYLIST_PARAMS;
+import static android.media.MediaSession2.COMMAND_CODE_PLAYBACK_SET_VOLUME;
+import static android.media.MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM;
+import static android.media.MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_PREV_ITEM;
+import static android.media.MediaSession2.COMMAND_CODE_PLAYBACK_STOP;
+import static android.media.MediaSession2.COMMAND_CODE_PLAY_FROM_MEDIA_ID;
+import static android.media.MediaSession2.COMMAND_CODE_PLAY_FROM_SEARCH;
+import static android.media.MediaSession2.COMMAND_CODE_PLAY_FROM_URI;
+import static android.media.MediaSession2.COMMAND_CODE_PREPARE_FROM_MEDIA_ID;
+import static android.media.MediaSession2.COMMAND_CODE_PREPARE_FROM_SEARCH;
+import static android.media.MediaSession2.COMMAND_CODE_PREPARE_FROM_URI;
+import static android.media.MediaSession2.ControllerInfo;
+import static android.media.MediaSession2.PlaylistParams;
 
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.after;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.verify;
@@ -41,6 +61,8 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.List;
+
 /**
  * Tests whether {@link MediaSession2} receives commands that hasn't allowed.
  */
@@ -262,7 +284,8 @@
         verify(mCallback, timeout(TIMEOUT_MS).atLeastOnce()).onCommandRequest(
                 matchesSession(), matchesCaller(), matches(COMMAND_CODE_PLAYBACK_SET_VOLUME));
 
-        createSessionWithAllowedActions(createCommandGroupWithout(COMMAND_CODE_PLAYBACK_SET_VOLUME));
+        createSessionWithAllowedActions(
+                createCommandGroupWithout(COMMAND_CODE_PLAYBACK_SET_VOLUME));
         createController(mSession.getToken()).setVolumeTo(0, 0);
         verify(mCallback, after(WAIT_TIME_MS).never()).onCommandRequest(any(), any(), any());
     }
@@ -359,4 +382,43 @@
         verify(mCallback, after(WAIT_TIME_MS).never()).onPrepareFromSearch(
                 any(), any(), any(), any());
     }
+
+    @Test
+    public void testChangingPermissionWithSetAllowedCommands() throws InterruptedException {
+        final String query = "testChangingPermissionWithSetAllowedCommands";
+        createSessionWithAllowedActions(
+                createCommandGroupWith(COMMAND_CODE_PREPARE_FROM_SEARCH));
+
+        TestControllerCallbackInterface controllerCallback =
+                mock(TestControllerCallbackInterface.class);
+        MediaController2 controller =
+                createController(mSession.getToken(), true, controllerCallback);
+
+        controller.prepareFromSearch(query, null);
+        verify(mCallback, timeout(TIMEOUT_MS).atLeastOnce()).onPrepareFromSearch(
+                matchesSession(), matchesCaller(), eq(query), isNull());
+        clearInvocations(mCallback);
+
+        // Change allowed commands.
+        mSession.setAllowedCommands(getTestControllerInfo(),
+                createCommandGroupWithout(COMMAND_CODE_PREPARE_FROM_SEARCH));
+        verify(controllerCallback, timeout(TIMEOUT_MS).atLeastOnce())
+                .onAllowedCommandsChanged(any());
+
+        controller.prepareFromSearch(query, null);
+        verify(mCallback, after(WAIT_TIME_MS).never()).onPrepareFromSearch(
+                any(), any(), any(), any());
+    }
+
+    private ControllerInfo getTestControllerInfo() {
+        List<ControllerInfo> controllers = mSession.getConnectedControllers();
+        assertNotNull(controllers);
+        for (int i = 0; i < controllers.size(); i++) {
+            if (Process.myUid() == controllers.get(i).getUid()) {
+                return controllers.get(i);
+            }
+        }
+        fail("Failed to get test controller info");
+        return null;
+    }
 }