MediaSession2/Controller2: Add playlist support

This CL implements following APIs:
 - MediaSession2.get/setPlaylist
 - MediaController2.getPlaylist
 - MediaController2.ControllerCallback.onPlaylistChanged

Bug: 72537268
Test: Passed MediaSession2Test
Change-Id: I206bb1018cde38d7db296df0912d02272fe1c6c7
diff --git a/packages/MediaComponents/src/com/android/media/IMediaSession2Callback.aidl b/packages/MediaComponents/src/com/android/media/IMediaSession2Callback.aidl
index 34aaa87..aabbc69 100644
--- a/packages/MediaComponents/src/com/android/media/IMediaSession2Callback.aidl
+++ b/packages/MediaComponents/src/com/android/media/IMediaSession2Callback.aidl
@@ -29,6 +29,7 @@
  */
 oneway interface IMediaSession2Callback {
     void onPlaybackStateChanged(in Bundle state);
+    void onPlaylistChanged(in List<Bundle> playlist);
     void onPlaylistParamsChanged(in Bundle params);
 
     /**
diff --git a/packages/MediaComponents/src/com/android/media/MediaController2Impl.java b/packages/MediaComponents/src/com/android/media/MediaController2Impl.java
index 57624e2..30e32ec 100644
--- a/packages/MediaComponents/src/com/android/media/MediaController2Impl.java
+++ b/packages/MediaComponents/src/com/android/media/MediaController2Impl.java
@@ -70,6 +70,8 @@
     @GuardedBy("mLock")
     private PlaybackState2 mPlaybackState;
     @GuardedBy("mLock")
+    private List<MediaItem2> mPlaylist;
+    @GuardedBy("mLock")
     private PlaylistParams mPlaylistParams;
 
     // Assignment should be used with the lock hold, but should be used without a lock to prevent
@@ -346,8 +348,9 @@
 
     @Override
     public List<MediaItem2> getPlaylist_impl() {
-        // TODO(jaewan): Implement
-        return null;
+        synchronized (mLock) {
+            return mPlaylist;
+        }
     }
 
     @Override
@@ -441,6 +444,26 @@
         });
     }
 
+    private void pushPlaylistChanges(final List<Bundle> list) {
+        final List<MediaItem2> playlist = new ArrayList<>();
+        for (int i = 0; i < list.size(); i++) {
+            MediaItem2 item = MediaItem2.fromBundle(mContext, list.get(i));
+            if (item != null) {
+                playlist.add(item);
+            }
+        }
+
+        synchronized (mLock) {
+            mPlaylist = playlist;
+            mCallbackExecutor.execute(() -> {
+                if (!mInstance.isConnected()) {
+                    return;
+                }
+                mCallback.onPlaylistChanged(playlist);
+            });
+        }
+    }
+
     // 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,
@@ -546,6 +569,21 @@
         }
 
         @Override
+        public void onPlaylistChanged(List<Bundle> playlist) throws RuntimeException {
+            final MediaController2Impl controller;
+            try {
+                controller = getController();
+            } catch (IllegalStateException e) {
+                Log.w(TAG, "Don't fail silently here. Highly likely a bug");
+                return;
+            }
+            if (playlist == null) {
+                return;
+            }
+            controller.pushPlaylistChanges(playlist);
+        }
+
+        @Override
         public void onPlaylistParamsChanged(Bundle params) throws RuntimeException {
             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 a6ba767..7c36739 100644
--- a/packages/MediaComponents/src/com/android/media/MediaSession2Impl.java
+++ b/packages/MediaComponents/src/com/android/media/MediaSession2Impl.java
@@ -77,11 +77,14 @@
     private MediaPlayerInterface mPlayer;
     @GuardedBy("mLock")
     private MyPlaybackListener mListener;
+    @GuardedBy("mLock")
     private PlaylistParams mPlaylistParams;
+    @GuardedBy("mLock")
+    private List<MediaItem2> mPlaylist;
 
     /**
      * Can be only called by the {@link Builder#build()}.
-     * 
+     *
      * @param instance
      * @param context
      * @param player
@@ -293,7 +296,11 @@
         if (params == null) {
             throw new IllegalArgumentException("PlaylistParams should not be null!");
         }
-        mPlaylistParams = params;
+        ensureCallingThread();
+        ensurePlayer();
+        synchronized (mLock) {
+            mPlaylistParams = params;
+        }
         mPlayer.setPlaylistParams(params);
         mSessionStub.notifyPlaylistParamsChanged(params);
     }
@@ -334,14 +341,24 @@
     }
 
     @Override
-    public void setPlaylist_impl(List<MediaItem2> playlist, PlaylistParams param) {
-        // TODO(jaewan): Implement
+    public void setPlaylist_impl(List<MediaItem2> playlist) {
+        if (playlist == null) {
+            throw new IllegalArgumentException("Playlist should not be null!");
+        }
+        ensureCallingThread();
+        ensurePlayer();
+        synchronized (mLock) {
+            mPlaylist = playlist;
+        }
+        mPlayer.setPlaylist(playlist);
+        mSessionStub.notifyPlaylistChanged(playlist);
     }
 
     @Override
     public List<MediaItem2> getPlaylist_impl() {
-        // TODO(jaewan): Implement this
-        return null;
+        synchronized (mLock) {
+            return mPlaylist;
+        }
     }
 
     @Override
diff --git a/packages/MediaComponents/src/com/android/media/MediaSession2Stub.java b/packages/MediaComponents/src/com/android/media/MediaSession2Stub.java
index 4451b3c..4fc69b9 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.media.MediaItem2;
 import android.media.MediaLibraryService2.BrowserRoot;
 import android.media.MediaLibraryService2.MediaLibrarySessionCallback;
 import android.media.MediaSession2;
@@ -345,6 +346,32 @@
         }
     }
 
+    public void notifyPlaylistChanged(List<MediaItem2> playlist) {
+        if (playlist == null) {
+            return;
+        }
+        final List<Bundle> bundleList = new ArrayList<>();
+        for (int i = 0; i < playlist.size(); i++) {
+            if (playlist.get(i) != null) {
+                Bundle bundle = playlist.get(i).toBundle();
+                if (bundle != null) {
+                    bundleList.add(bundle);
+                }
+            }
+        }
+        final List<ControllerInfo> list = getControllers();
+        for (int i = 0; i < list.size(); i++) {
+            IMediaSession2Callback callbackBinder =
+                    ControllerInfoImpl.from(list.get(i)).getControllerBinder();
+            try {
+                callbackBinder.onPlaylistChanged(bundleList);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Controller is gone", e);
+                // TODO(jaewan): What to do when the controller is gone?
+            }
+        }
+    }
+
     public void notifyPlaylistParamsChanged(MediaSession2.PlaylistParams params) {
         final List<ControllerInfo> list = getControllers();
         for (int i = 0; i < list.size(); i++) {
diff --git a/packages/MediaComponents/test/src/android/media/MediaSession2Test.java b/packages/MediaComponents/test/src/android/media/MediaSession2Test.java
index ed4fa89..6b10ccc 100644
--- a/packages/MediaComponents/test/src/android/media/MediaSession2Test.java
+++ b/packages/MediaComponents/test/src/android/media/MediaSession2Test.java
@@ -145,6 +145,30 @@
     }
 
     @Test
+    public void testSetPlaylist() throws Exception {
+        final List<MediaItem2> playlist = new ArrayList<>();
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        final TestControllerCallbackInterface callback = new TestControllerCallbackInterface() {
+            @Override
+            public void onPlaylistChanged(List<MediaItem2> givenList) {
+                assertMediaItemListEquals(playlist, givenList);
+                latch.countDown();
+            }
+        };
+
+        final MediaController2 controller = createController(mSession.getToken(), true, callback);
+        mSession.setPlaylist(playlist);
+
+        assertTrue(mPlayer.mSetPlaylistCalled);
+        assertMediaItemListEquals(playlist, mPlayer.mPlaylist);
+        assertMediaItemListEquals(playlist, mSession.getPlaylist());
+
+        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        assertMediaItemListEquals(playlist, controller.getPlaylist());
+    }
+
+    @Test
     public void testSetPlaylistParams() throws Exception {
         final PlaylistParams params = new PlaylistParams(
                 PlaylistParams.REPEAT_MODE_ALL,
@@ -339,4 +363,28 @@
             return true;
         }
     }
+
+    private static void assertMediaItemListEquals(List<MediaItem2> a, List<MediaItem2> b) {
+        if (a == null || b == null) {
+            assertEquals(a, b);
+        }
+        assertEquals(a.size(), b.size());
+
+        for (int i = 0; i < a.size(); i++) {
+            MediaItem2 aItem = a.get(i);
+            MediaItem2 bItem = b.get(i);
+
+            if (aItem == null || bItem == null) {
+                assertEquals(aItem, bItem);
+                continue;
+            }
+
+            assertEquals(aItem.getMediaId(), bItem.getMediaId());
+            assertEquals(aItem.getFlags(), bItem.getFlags());
+            TestUtils.equals(aItem.getMetadata().getBundle(), bItem.getMetadata().getBundle());
+
+            // Note: Here it does not check whether DataSourceDesc are equal,
+            // since there DataSourceDec is not comparable.
+        }
+    }
 }
diff --git a/packages/MediaComponents/test/src/android/media/MediaSession2TestBase.java b/packages/MediaComponents/test/src/android/media/MediaSession2TestBase.java
index 982346b..8e1c782 100644
--- a/packages/MediaComponents/test/src/android/media/MediaSession2TestBase.java
+++ b/packages/MediaComponents/test/src/android/media/MediaSession2TestBase.java
@@ -61,6 +61,7 @@
 
     interface TestControllerCallbackInterface {
         // Add methods in ControllerCallback/BrowserCallback that you want to test.
+        default void onPlaylistChanged(List<MediaItem2> playlist) {}
         default void onPlaylistParamsChanged(MediaSession2.PlaylistParams params) {}
 
         // Currently empty. Add methods in ControllerCallback/BrowserCallback that you want to test.
@@ -217,6 +218,13 @@
         }
 
         @Override
+        public void onPlaylistChanged(List<MediaItem2> params) {
+            if (mCallbackProxy != null) {
+                mCallbackProxy.onPlaylistChanged(params);
+            }
+        }
+
+        @Override
         public void onPlaylistParamsChanged(MediaSession2.PlaylistParams params) {
             if (mCallbackProxy != null) {
                 mCallbackProxy.onPlaylistParamsChanged(params);
diff --git a/packages/MediaComponents/test/src/android/media/MockPlayer.java b/packages/MediaComponents/test/src/android/media/MockPlayer.java
index a0f56ab..1faf0f4 100644
--- a/packages/MediaComponents/test/src/android/media/MockPlayer.java
+++ b/packages/MediaComponents/test/src/android/media/MockPlayer.java
@@ -16,7 +16,6 @@
 
 package android.media;
 
-import android.media.MediaPlayerInterface;
 import android.media.MediaSession2.PlaylistParams;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
@@ -44,10 +43,13 @@
     public long mSeekPosition;
     public boolean mSetCurrentPlaylistItemCalled;
     public int mItemIndex;
+    public boolean mSetPlaylistCalled;
     public boolean mSetPlaylistParamsCalled;
 
     public List<PlaybackListenerHolder> mListeners = new ArrayList<>();
+    public List<MediaItem2> mPlaylist;
     public PlaylistParams mPlaylistParams;
+
     private PlaybackState2 mLastPlaybackState;
     private AudioAttributes mAudioAttributes;
 
@@ -194,11 +196,13 @@
     }
 
     @Override
-    public void setPlaylist(List<MediaItem2> item, PlaylistParams param) {
+    public void setPlaylist(List<MediaItem2> playlist) {
+        mSetPlaylistCalled = true;
+        mPlaylist = playlist;
     }
 
     @Override
     public List<MediaItem2> getPlaylist() {
-        return null;
+        return mPlaylist;
     }
 }