Multi-listener support in MediaPlaybackModel

- Updating MediaPlaybackModel to support multiple listeners.
- Allowing MediaBrowser extras to be passed in optionally.

Bug: 34352155
Test: Manually
Change-Id: I309c9319cb68338a2716f6377a98ae5e22166a3e
diff --git a/src/com/android/car/media/MediaPlaybackFragment.java b/src/com/android/car/media/MediaPlaybackFragment.java
index 25b1e65..04bd846 100644
--- a/src/com/android/car/media/MediaPlaybackFragment.java
+++ b/src/com/android/car/media/MediaPlaybackFragment.java
@@ -52,6 +52,7 @@
 import android.widget.ProgressBar;
 import android.widget.SeekBar;
 import android.widget.TextView;
+
 import com.android.car.apps.common.BitmapDownloader;
 import com.android.car.apps.common.BitmapWorkerOptions;
 import com.android.car.apps.common.util.Assert;
@@ -143,7 +144,9 @@
         mActivity = (MediaActivity) getHost();
         mShowTitleDelayMs =
                 mActivity.getResources().getInteger(R.integer.new_album_art_fade_in_offset);
-        mMediaPlaybackModel = new MediaPlaybackModel(mActivity.getContext(), this);
+        mMediaPlaybackModel =
+                new MediaPlaybackModel(mActivity.getContext(), null /* browserExtras */);
+        mMediaPlaybackModel.addListener(this);
         mTelephonyManager = (TelephonyManager) mActivity.getContext()
                 .getSystemService(Context.TELEPHONY_SERVICE);
     }
diff --git a/src/com/android/car/media/MediaPlaybackModel.java b/src/com/android/car/media/MediaPlaybackModel.java
index ab45e48..6f234e2 100644
--- a/src/com/android/car/media/MediaPlaybackModel.java
+++ b/src/com/android/car/media/MediaPlaybackModel.java
@@ -31,10 +31,13 @@
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.util.Log;
+
 import com.android.car.apps.common.util.Assert;
 
 import java.util.ArrayList;
+import java.util.LinkedList;
 import java.util.List;
+import java.util.function.Consumer;
 
 /**
  * A model for controlling media playback. This model will take care of all Media Manager, Browser,
@@ -45,8 +48,9 @@
 public class MediaPlaybackModel {
     private static final String TAG = "GH.MediaPlaybackModel";
 
-    private final MediaPlaybackModel.Listener mListener;
     private final Context mContext;
+    private final Bundle mBrowserExtras;
+    private final List<MediaPlaybackModel.Listener> mListeners = new LinkedList<>();
 
     private Handler mHandler;
     private MediaController mController;
@@ -61,21 +65,38 @@
      * done in the main thread.
      */
     public interface Listener {
+        /** Indicates active media app has changed. A new mediaBrowser is now connecting to the new
+          * app and mediaController has been released, pending connection to new service.
+          */
         void onMediaAppChanged(@Nullable ComponentName currentName,
                                @Nullable ComponentName newName);
         void onMediaAppStatusMessageChanged(@Nullable String message);
+
+        /**
+         * Indicates the mediaBrowser is not connected and mediaController is available.
+         */
         void onMediaConnected();
+        /**
+         * Indicates mediaBrowser connection is temporarily suspended.
+         * */
         void onMediaConnectionSuspended();
+        /**
+         * Indicates that the MediaBrowser connected failed. The mediaBrowser and controller have
+         * now been released.
+         */
         void onMediaConnectionFailed(CharSequence failedMediaClientName);
         void onPlaybackStateChanged(@Nullable PlaybackState state);
         void onMetadataChanged(@Nullable MediaMetadata metadata);
         void onQueueChanged(List<MediaSession.QueueItem> queue);
+        /**
+         * Indicates that the MediaSession was destroyed. The mediaController has been released.
+         */
         void onSessionDestroyed(CharSequence destroyedMediaClientName);
     }
 
-    public MediaPlaybackModel(Context context, MediaPlaybackModel.Listener listener) {
+    public MediaPlaybackModel(Context context, Bundle browserExtras) {
         mContext = context;
-        mListener = listener;
+        mBrowserExtras = browserExtras;
         mHandler = new Handler(Looper.getMainLooper());
     }
 
@@ -111,6 +132,25 @@
     }
 
     @MainThread
+    public void addListener(MediaPlaybackModel.Listener listener) {
+        Assert.isMainThread();
+        mListeners.add(listener);
+    }
+
+    @MainThread
+    public void removeListener(MediaPlaybackModel.Listener listener) {
+        Assert.isMainThread();
+        mListeners.remove(listener);
+    }
+
+    @MainThread
+    private void notifyListeners(Consumer<Listener> callback) {
+        Assert.isMainThread();
+        // Invokes callback.accept(listener) for each listener.
+        mListeners.forEach(callback);
+    }
+
+    @MainThread
     public Resources getPackageResources() {
         Assert.isMainThread();
         return mPackageResources;
@@ -207,46 +247,40 @@
     private final MediaManager.Listener mMediaManagerListener = new MediaManager.Listener() {
         @Override
         public void onMediaAppChanged(final ComponentName name) {
-            mHandler.post(new Runnable() {
-                @Override
-                public void run() {
-                    if (mBrowser != null) {
-                        mBrowser.disconnect();
-                    }
-                    mBrowser = new MediaBrowser(mContext, name, mConnectionCallback, null);
-                    try {
-                        mPackageResources = mContext.getPackageManager().getResourcesForApplication(
-                                name.getPackageName());
-                    } catch (PackageManager.NameNotFoundException e) {
-                        Log.e(TAG, "Unable to get resources for " + name.getPackageName());
-                    }
-
-                    if (mController != null) {
-                        mController.unregisterCallback(mMediaControllerCallback);
-                        mController = null;
-                    }
-                    mBrowser.connect();
-
-                    // reset the colors and views if we switch to another app.
-                    mAccentColor = MediaManager.getInstance(mContext)
-                            .getMediaClientAccentColor();
-                    mPrimaryColorDark = MediaManager.getInstance(mContext)
-                            .getMediaClientPrimaryColorDark();
-
-                    final ComponentName currentName = mCurrentComponentName;
-                    mListener.onMediaAppChanged(currentName, name);
-                    mCurrentComponentName = name;
+            mHandler.post(() -> {
+                if (mBrowser != null) {
+                    mBrowser.disconnect();
                 }
+                mBrowser = new MediaBrowser(mContext, name, mConnectionCallback, mBrowserExtras);
+                try {
+                    mPackageResources = mContext.getPackageManager().getResourcesForApplication(
+                            name.getPackageName());
+                } catch (PackageManager.NameNotFoundException e) {
+                    Log.e(TAG, "Unable to get resources for " + name.getPackageName());
+                }
+
+                if (mController != null) {
+                    mController.unregisterCallback(mMediaControllerCallback);
+                    mController = null;
+                }
+                mBrowser.connect();
+
+                // reset the colors and views if we switch to another app.
+                mAccentColor = MediaManager.getInstance(mContext)
+                        .getMediaClientAccentColor();
+                mPrimaryColorDark = MediaManager.getInstance(mContext)
+                        .getMediaClientPrimaryColorDark();
+
+                final ComponentName currentName = mCurrentComponentName;
+                notifyListeners((listener) -> listener.onMediaAppChanged(currentName, name));
+                mCurrentComponentName = name;
             });
         }
 
         @Override
         public void onStatusMessageChanged(final String message) {
-            mHandler.post(new Runnable() {
-                @Override
-                public void run() {
-                    mListener.onMediaAppStatusMessageChanged(message);
-                }
+            mHandler.post(() -> {
+                notifyListeners((listener) -> listener.onMediaAppStatusMessageChanged(message));
             });
         }
     };
@@ -255,52 +289,44 @@
             new MediaBrowser.ConnectionCallback() {
                 @Override
                 public void onConnected() {
-                    mHandler.post(new Runnable() {
-                        @Override
-                        public void run() {
-                            // Existing mController has already been disconnected before we call
-                            // MediaBrowser.connect()
-                            // getSessionToken returns a non null token
-                            MediaSession.Token token = mBrowser.getSessionToken();
-                            if (mController != null) {
-                                mController.unregisterCallback(mMediaControllerCallback);
-                            }
-                            mController = new MediaController(mContext, token);
-                            mController.registerCallback(mMediaControllerCallback);
-                            mListener.onMediaConnected();
+                    mHandler.post(()->{
+                        // Existing mController has already been disconnected before we call
+                        // MediaBrowser.connect()
+                        // getSessionToken returns a non null token
+                        MediaSession.Token token = mBrowser.getSessionToken();
+                        if (mController != null) {
+                            mController.unregisterCallback(mMediaControllerCallback);
                         }
+                        mController = new MediaController(mContext, token);
+                        mController.registerCallback(mMediaControllerCallback);
+                        notifyListeners(Listener::onMediaConnected);
                     });
                 }
 
                 @Override
                 public void onConnectionSuspended() {
-                    mHandler.post(new Runnable() {
-                        @Override
-                        public void run() {
-                            if (Log.isLoggable(TAG, Log.VERBOSE)) {
-                                Log.v(TAG, "Media browser service connection suspended."
-                                        + " Waiting to be reconnected....");
-                            }
-                            mListener.onMediaConnectionSuspended();
+                    mHandler.post(() -> {
+                        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                            Log.v(TAG, "Media browser service connection suspended."
+                                    + " Waiting to be reconnected....");
                         }
+                        notifyListeners(Listener::onMediaConnectionSuspended);
                     });
                 }
 
                 @Override
                 public void onConnectionFailed() {
-                    mHandler.post(new Runnable() {
-                        @Override
-                        public void run() {
-                            Log.e(TAG, "Media browser service connection FAILED!");
-                            // disconnect anyway to make sure we get into a sanity state
-                            mBrowser.disconnect();
-                            mBrowser = null;
-                            mCurrentComponentName = null;
+                    mHandler.post(() -> {
+                        Log.e(TAG, "Media browser service connection FAILED!");
+                        // disconnect anyway to make sure we get into a sanity state
+                        mBrowser.disconnect();
+                        mBrowser = null;
+                        mCurrentComponentName = null;
 
-                            CharSequence failedClientName = MediaManager.getInstance(mContext)
-                                    .getMediaClientName();
-                            mListener.onMediaConnectionFailed(failedClientName);
-                        }
+                        CharSequence failedClientName = MediaManager.getInstance(mContext)
+                                .getMediaClientName();
+                        notifyListeners(
+                                (listener) -> listener.onMediaConnectionFailed(failedClientName));
                     });
                 }
             };
@@ -309,56 +335,43 @@
             new MediaController.Callback() {
                 @Override
                 public void onPlaybackStateChanged(final PlaybackState state) {
-                    mHandler.post(new Runnable() {
-                        @Override
-                        public void run() {
-                            mListener.onPlaybackStateChanged(state);
-                        }
+                    mHandler.post(() -> {
+                        notifyListeners((listener) -> listener.onPlaybackStateChanged(state));
                     });
                 }
 
                 @Override
                 public void onMetadataChanged(final MediaMetadata metadata) {
-                    mHandler.post(new Runnable() {
-                        @Override
-                        public void run() {
-                            mListener.onMetadataChanged(metadata);
-                        }
+                    mHandler.post(() -> {
+                        notifyListeners((listener) -> listener.onMetadataChanged(metadata));
                     });
                 }
 
                 @Override
                 public void onQueueChanged(final List<MediaSession.QueueItem> queue) {
-                    mHandler.post(new Runnable() {
-                        @Override
-                        public void run() {
-                            List<MediaSession.QueueItem> currentQueue = queue;
-                            if (currentQueue == null) {
-                                currentQueue = new ArrayList<>();
-                            }
-                            mListener.onQueueChanged(currentQueue);
-                        }
+                    mHandler.post(() -> {
+                        final List<MediaSession.QueueItem> currentQueue =
+                                queue != null ? queue : new ArrayList<>();
+                        notifyListeners((listener) -> listener.onQueueChanged(currentQueue));
                     });
                 }
 
                 @Override
                 public void onSessionDestroyed() {
-                    mHandler.post(new Runnable() {
-                        @Override
-                        public void run() {
-                            if (Log.isLoggable(TAG, Log.VERBOSE)) {
-                                Log.v(TAG, "onSessionDestroyed()");
-                            }
-                            mCurrentComponentName = null;
-                            if (mController != null) {
-                                mController.unregisterCallback(mMediaControllerCallback);
-                                mController = null;
-                            }
-
-                            CharSequence destroyedMediaClientName = MediaManager.getInstance(
-                                    mContext).getMediaClientName();
-                            mListener.onSessionDestroyed(destroyedMediaClientName);
+                    mHandler.post(() -> {
+                        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                            Log.v(TAG, "onSessionDestroyed()");
                         }
+                        mCurrentComponentName = null;
+                        if (mController != null) {
+                            mController.unregisterCallback(mMediaControllerCallback);
+                            mController = null;
+                        }
+
+                        CharSequence destroyedClientName = MediaManager.getInstance(
+                                mContext).getMediaClientName();
+                        notifyListeners(
+                                (listener) -> listener.onSessionDestroyed(destroyedClientName));
                     });
                 }
             };