Moving Media app off CarActivity

- MediaActivity now extends CarDrawerActivity from car-stream-ui-lib and
  no longer relies on legacy CarActivity.
- Re-implemented Drawer logic using CarDrawerAdapter. Added
  media-specific subclass: MediaDrawerAdapter. It relies on
  MediaBrowserItemsFetcher (and subclasses) for actual fetching of
  browse items or queue items.
- Removed now dead classes: MediaProxyActivity, MediaCarMenuCallbacks
  and MediaMenuBitmapDownloader.
- Drawer layout is still a bit broken because CarDrawerAdapter is not
  flexible enough for Media needs. Filed follow-on bug b/36573125 to
  address.

Bug: 34352155
Test: Played music in from LocalMediaPlay and BT Media player. Local
      version works for the most part (except for b/36571620). BT version browse
      works, but playback is buggy. Need to investigate BT side of things.

Change-Id: Ic9ee87fcdeaecb2c71a9d354316c1342ae90de28
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 7900c7f..2f2baa3 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -30,8 +30,8 @@
             android:name="android.car.application"
             android:resource="@xml/automotive_app_desc" />
 
-        <activity android:name=".MediaProxyActivity"
-            android:theme="@android:style/Theme.NoTitleBar"
+        <activity android:name=".MediaActivity"
+            android:theme="@style/CarDrawerActivityTheme"
             android:label="CarMediaApp"
             android:resizeableActivity="true"
             android:launchMode="singleTop">
diff --git a/src/com/android/car/media/MediaActivity.java b/src/com/android/car/media/MediaActivity.java
index ab282dc..3ff1d93 100644
--- a/src/com/android/car/media/MediaActivity.java
+++ b/src/com/android/car/media/MediaActivity.java
@@ -16,21 +16,23 @@
 package com.android.car.media;
 
 import android.content.ComponentName;
-import android.content.Context;
 import android.content.Intent;
 import android.graphics.Bitmap;
 import android.os.Bundle;
 import android.provider.MediaStore;
-import android.support.car.Car;
-import android.support.car.app.menu.CarDrawerActivity;
+import android.support.v4.app.Fragment;
 import android.util.Log;
 import android.util.Pair;
 import android.util.TypedValue;
 import android.view.View;
 
+import com.android.car.app.CarDrawerActivity;
+import com.android.car.app.CarDrawerAdapter;
+import com.android.car.media.drawer.MediaDrawerController;
+
 /**
  * This activity controls the UI of media. It also updates the connection status for the media app
- * by broadcast. Drawer menu is controlled by {@link MediaCarMenuCallbacks}.
+ * by broadcast. Drawer menu is controlled by {@link MediaDrawerController}.
  */
 public class MediaActivity extends CarDrawerActivity {
     private static final String ACTION_MEDIA_APP_STATE_CHANGE
@@ -59,21 +61,17 @@
      */
     private boolean mContentFragmentChangeQueued;
 
+    private MediaDrawerController mDrawerController;
     private View mScrimView;
     private CrossfadeImageView mAlbumArtView;
     private MediaPlaybackFragment mMediaPlaybackFragment;
-    private MediaCarMenuCallbacks mMediaCarMenuCallbacks;
-
-    public MediaActivity(Proxy proxy, Context context, Car car) {
-        super(proxy, context, car);
-    }
 
     @Override
     protected void onStart() {
         super.onStart();
         Intent i = new Intent(ACTION_MEDIA_APP_STATE_CHANGE);
         i.putExtra(EXTRA_MEDIA_APP_FOREGROUND, true);
-        getContext().sendBroadcast(i);
+        sendBroadcast(i);
 
         mIsStarted = true;
 
@@ -91,22 +89,21 @@
         super.onStop();
         Intent i = new Intent(ACTION_MEDIA_APP_STATE_CHANGE);
         i.putExtra(EXTRA_MEDIA_APP_FOREGROUND, false);
-        getContext().sendBroadcast(i);
+        sendBroadcast(i);
 
         mIsStarted = false;
     }
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
+        mDrawerController = new MediaDrawerController(this);
         super.onCreate(savedInstanceState);
-        setLightMode();
-        mMediaCarMenuCallbacks = new MediaCarMenuCallbacks(this);
-        setCarMenuCallbacks(mMediaCarMenuCallbacks);
-        setContentView(R.layout.media_activity);
+
+        setMainContent(R.layout.media_activity);
         mScrimView = findViewById(R.id.scrim);
         mAlbumArtView = (CrossfadeImageView) findViewById(R.id.album_art);
-        setBackgroundColor(getContext().getColor(R.color.music_default_artwork));
-        MediaManager.getInstance(getContext()).addListener(mListener);
+        setBackgroundColor(getColor(R.color.music_default_artwork));
+        MediaManager.getInstance(this).addListener(mListener);
     }
 
     @Override
@@ -114,10 +111,15 @@
         super.onDestroy();
         // Send the broadcast to let the current connected media app know it is disconnected now.
         sendMediaConnectionStatusBroadcast(
-                MediaManager.getInstance(getContext()).getCurrentComponent(),
+                MediaManager.getInstance(this).getCurrentComponent(),
                 MediaConstants.MEDIA_DISCONNECTED);
-        mMediaCarMenuCallbacks.cleanup();
-        MediaManager.getInstance(getContext()).removeListener(mListener);
+        mDrawerController.cleanup();
+        MediaManager.getInstance(this).removeListener(mListener);
+    }
+
+    @Override
+    protected CarDrawerAdapter getRootAdapter() {
+        return mDrawerController.getRootAdapter();
     }
 
     @Override
@@ -149,9 +151,7 @@
         }
 
         setIntent(intent);
-        if (isDrawerShowing()) {
-            closeDrawer();
-        }
+        closeDrawer();
     }
 
     @Override
@@ -230,19 +230,19 @@
                     intent.getStringExtra(MediaManager.KEY_MEDIA_PACKAGE),
                     intent.getStringExtra(MediaManager.KEY_MEDIA_CLASS)
             );
-            MediaManager.getInstance(getContext()).setMediaClientComponent(component);
+            MediaManager.getInstance(this).setMediaClientComponent(component);
         } else {
             if (Log.isLoggable(TAG, Log.DEBUG)) {
                 Log.d(TAG, "Launching most recent / default component.");
             }
 
             // Set it to the default GPM component.
-            MediaManager.getInstance(getContext()).connectToMostRecentMediaComponent(
-                    new CarClientServiceAdapter(getContext().getPackageManager()));
+            MediaManager.getInstance(this).connectToMostRecentMediaComponent(
+                    new CarClientServiceAdapter(getPackageManager()));
         }
 
         if (isSearchIntent(intent)) {
-            MediaManager.getInstance(getContext()).processSearchIntent(intent);
+            MediaManager.getInstance(this).processSearchIntent(intent);
             setIntent(null);
         }
     }
@@ -253,7 +253,7 @@
     }
 
     private void sendMediaConnectionStatusBroadcast(
-            ComponentName componentName, @Car.ConnectionType String connectionStatus) {
+            ComponentName componentName, String connectionStatus) {
         // It will be no current component if no media app is chosen before.
         if (componentName == null) {
             return;
@@ -262,10 +262,10 @@
         Intent intent = new Intent(MediaConstants.ACTION_MEDIA_STATUS);
         intent.setPackage(componentName.getPackageName());
         intent.putExtra(MediaConstants.MEDIA_CONNECTION_STATUS, connectionStatus);
-        getContext().sendBroadcast(intent);
+        sendBroadcast(intent);
     }
 
-    public void attachContentFragment() {
+    void attachContentFragment() {
         if (mMediaPlaybackFragment == null) {
             mMediaPlaybackFragment = new MediaPlaybackFragment();
         }
@@ -298,4 +298,15 @@
         @Override
         public void onStatusMessageChanged(String msg) {}
     };
-}
+
+    private void setContentFragment(Fragment fragment) {
+        getSupportFragmentManager().beginTransaction()
+                .replace(getContentContainerId(), fragment)
+                .commit();
+    }
+
+
+    void showQueueInDrawer() {
+        mDrawerController.showQueueInDrawer();
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/car/media/MediaCarMenuCallbacks.java b/src/com/android/car/media/MediaCarMenuCallbacks.java
deleted file mode 100644
index 26415ab..0000000
--- a/src/com/android/car/media/MediaCarMenuCallbacks.java
+++ /dev/null
@@ -1,618 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.media;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.PorterDuff;
-import android.graphics.drawable.Drawable;
-import android.media.browse.MediaBrowser;
-import android.media.session.MediaController;
-import android.media.session.MediaSession;
-import android.media.session.PlaybackState;
-import android.os.Bundle;
-import android.os.Handler;
-import android.support.car.app.menu.CarMenu;
-import android.support.car.app.menu.CarMenuCallbacks;
-import android.support.car.app.menu.RootMenu;
-import android.support.car.app.menu.compat.CarMenuConstantsComapt;
-import android.text.TextUtils;
-import android.util.Log;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Manages all data needed for media drawer menu.
- */
-public class MediaCarMenuCallbacks extends CarMenuCallbacks {
-
-    public static final String QUEUE_ROOT = "QUEUE_ROOT";
-
-    private static final String TAG = "GH.MediaMenuCallbacks";
-    // MEDIA_APP_ROOT is used for onGetRoot() of MediaMenuCallbacks, which is called so early that
-    // MediaBrowser hasn't got the root already. So we return this default root first and store the
-    // real one in mRootId.
-    private static final String MEDIA_APP_ROOT = "MEDIA_APP_ROOT";
-    private static final String EXTRA_ICON_SIZE =
-            "com.google.android.gms.car.media.BrowserIconSize";
-    private static final String QUEUE_ITEM_PREFIX = "queue_item_prefix_";
-    private static final String MEDIA_QUEUE_EMPTY_PLACEHOLDER = "media_queue_emtpy_placeholder";
-
-    private final MediaActivity mActivity;
-    private final Context mContext;
-    private final Handler mHandler;
-    private MediaBrowser mBrowser;
-    private MediaController mController;
-    private CarMenu mMenuResult;
-    private String mMediaId;
-    private String mRootId;
-    // The media id we want to subscribe but media browser is not connected at that time.
-    private String mPendingMediaId;
-    private long mActiveQueueItemId;
-    private boolean mLoadQueueMenuPending;
-    // Whether we add "Queue" as the last item in the main menu.
-    private boolean mIsQueueInMenu;
-    private List<MediaBrowser.MediaItem> mItems;
-    private LoadQueueBitmapRunnable mLoadQueueBitmapRunnable;
-    private LoadMenuBitmapRunnable mLoadMenuBitmapRunnable;
-    // The parent ID is set whenever there's a onChildrenLoaded request.
-    private UpdateMenuRunnable mUpdateMenuRunnable = new UpdateMenuRunnable();
-
-    public MediaCarMenuCallbacks(MediaActivity activity) {
-        mActivity = activity;
-        mContext = activity.getContext();
-        mHandler = new Handler();
-        MediaManager.getInstance(mContext).addListener(mListener);
-    }
-
-    public void cleanup() {
-        MediaManager.getInstance(mContext).removeListener(mListener);
-        mHandler.removeCallbacksAndMessages(null);
-        if (mBrowser != null) {
-            if (mMediaId != null) {
-                mBrowser.unsubscribe(mMediaId);
-                mMediaId = null;
-            }
-            mBrowser.disconnect();
-            mBrowser = null;
-        }
-        if (mController != null) {
-            mController.unregisterCallback(mControllerCallback);
-            mController = null;
-        }
-    }
-
-    @Override
-    public RootMenu onGetRoot(Bundle hints) {
-        // Return the default fake root due to the real one maybe not ready at this time.
-        return new RootMenu(MEDIA_APP_ROOT);
-    }
-
-    @Override
-    public void onLoadChildren(String parentId, CarMenu result) {
-        Log.d(TAG, "onLoadChildren " + parentId);
-        resetCarMenu(result);
-        if (QUEUE_ROOT.equals(parentId)) {
-            // If mBrowser is not connected now, we will load the menu later when it is connected.
-            if (mBrowser == null || !mBrowser.isConnected()) {
-                if (Log.isLoggable(TAG, Log.DEBUG)) {
-                    Log.d(TAG, "MediaBrowser is not connected while loading menu.");
-                }
-                mLoadQueueMenuPending = true;
-                return;
-            }
-
-            // Unsubscribe the old id first, or else it will affect to subscribe the new one.
-            if (!TextUtils.isEmpty(mMediaId) && !QUEUE_ROOT.equals(mMediaId)) {
-                mBrowser.unsubscribe(mMediaId);
-            }
-            mMediaId = parentId;
-
-            loadQueueMenu();
-        } else {
-            // If mBrowser is not connected now, we will load the menu later when it is connected.
-            if (mBrowser == null || !mBrowser.isConnected()) {
-                if (Log.isLoggable(TAG, Log.DEBUG)) {
-                    Log.d(TAG, "MediaBrowser is not connected while loading menu.");
-                }
-                mPendingMediaId = parentId;
-                return;
-            }
-
-            // Unsubscribe the old id first, or else it will affect to subscribe the new one.
-            if (!TextUtils.isEmpty(mMediaId) && !QUEUE_ROOT.equals(mMediaId)) {
-                mBrowser.unsubscribe(mMediaId);
-            }
-            // Replace the fake root id with the real one, then we can use it to subscribe.
-            if (parentId.equals(MEDIA_APP_ROOT)) {
-                mMediaId = mRootId;
-            } else {
-                mMediaId = parentId;
-            }
-            mBrowser.subscribe(mMediaId, mSubscriptionCallback);
-        }
-    }
-
-    @Override
-    public void onItemClicked(String id) {
-        // We treat queue item specially because its id is different from the normal one.
-        if (id.startsWith(QUEUE_ITEM_PREFIX)) {
-            String index = id.substring(QUEUE_ITEM_PREFIX.length());
-            mController.getTransportControls().skipToQueueItem(Long.valueOf(index));
-            mActivity.closeDrawer();
-        } else {
-            if (mItems == null) {
-                if (Log.isLoggable(TAG, Log.DEBUG)) {
-                    Log.d(TAG, "Media menu is empty.");
-                }
-                return;
-            }
-
-            for (MediaBrowser.MediaItem item : mItems) {
-                if (item.getMediaId().equals(id)) {
-                    if (item.isPlayable()) {
-                        if (mController != null) {
-                            mController.getTransportControls().pause();
-                            mController.getTransportControls().playFromMediaId(item.getMediaId(),
-                                    item.getDescription().getExtras());
-                        } else {
-                            Log.e(TAG, "MediaSession is destroyed.");
-                        }
-                        mActivity.closeDrawer();
-                    }
-                    break;
-                }
-            }
-        }
-    }
-
-    private void resetCarMenu(CarMenu result) {
-        // Stop loading previous menu due to we are under the new one now.
-        if (mMenuResult != null) {
-            if (mUpdateMenuRunnable != null) {
-                mHandler.removeCallbacks(mUpdateMenuRunnable);
-                // Spot fix. This runnable is being used in the subscription callbacks and is
-                // causing a crash. The lifecycle here is a little messed up and needs to be
-                // straightened out but for now just set it to a new object instead of setting
-                // it to null.
-                mUpdateMenuRunnable = new UpdateMenuRunnable();
-            }
-            if (mLoadMenuBitmapRunnable != null) {
-                mHandler.removeCallbacks(mLoadMenuBitmapRunnable);
-                mLoadMenuBitmapRunnable = null;
-            }
-            if (mLoadQueueBitmapRunnable != null) {
-                mHandler.removeCallbacks(mLoadQueueBitmapRunnable);
-                mLoadQueueBitmapRunnable = null;
-            }
-        }
-        mMenuResult = result;
-        mMenuResult.detach();
-    }
-
-    private CarMenu.Item emptyQueueMenu() {
-        CarMenu.Builder builder = new CarMenu.Builder(MEDIA_QUEUE_EMPTY_PLACEHOLDER);
-
-        final int iconColor = mContext.getResources().getColor(R.color.car_tint);
-        Drawable drawable = mContext.getResources().getDrawable(R.drawable.ic_list_view_disable);
-        drawable.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN);
-        builder.setIconFromSnapshot(drawable);
-        builder.setIsEmptyPlaceHolder(true);
-        return builder.build();
-    }
-
-    private void loadQueueMenu() {
-        if (mMenuResult == null) {
-            Log.w(TAG, "CarMenu is null while loading queue menu.");
-            return;
-        }
-
-        List<CarMenu.Item> menuItems = new ArrayList<>();
-        if (mController == null) {
-            Log.w(TAG, "MediaController is null while loading queue menu.");
-
-            // Add a icon for empty menu.
-            sendEmptyMenu();
-        } else {
-            List<MediaSession.QueueItem> queue = mController.getQueue();
-            mActiveQueueItemId = getActiveQueueItemId();
-            boolean hasImages = false;
-            for (MediaSession.QueueItem item : queue) {
-                if ((item.getDescription().getIconUri() != null)
-                        || (item.getDescription().getIconBitmap() != null)) {
-                    hasImages = true;
-                    break;
-                }
-            }
-            boolean activeQueueItemFound = false;
-            for (final MediaSession.QueueItem item : queue) {
-                // Only queue items following the active item are displayed in the menu.
-                if (item.getQueueId() == mActiveQueueItemId) {
-                    activeQueueItemFound = true;
-                }
-
-                if (activeQueueItemFound) {
-                    CarMenu.Builder builder =
-                            new CarMenu.Builder(QUEUE_ITEM_PREFIX + item.getQueueId());
-                    builder.setTitle(item.getDescription().getTitle().toString())
-                            .setText(item.getDescription().getSubtitle().toString());
-                    // Place empty bitmap as place holder first, we will load the bitamp later.
-                    if (hasImages) {
-                        builder.setIcon(null);
-                    }
-                    if (item.getQueueId() == mActiveQueueItemId) {
-                        int primaryColor =
-                                MediaManager.getInstance(mContext).getMediaClientPrimaryColor();
-                        Drawable drawable =
-                                mContext.getResources().getDrawable(R.drawable.ic_music_active);
-                        drawable.setColorFilter(primaryColor, PorterDuff.Mode.SRC_IN);
-                        builder.setRightIconFromSnapshot(drawable);
-                    }
-                    menuItems.add(builder.build());
-                }
-            }
-
-            // If we have not found any items then set the menu to empty placeholder item.
-            if (menuItems.size() == 0) {
-                sendEmptyMenu();
-            } else {
-                mMenuResult.sendResult(menuItems);
-                mMenuResult = null;
-            }
-
-            if (hasImages) {
-                if (mLoadQueueBitmapRunnable != null) {
-                    mHandler.removeCallbacks(mLoadQueueBitmapRunnable);
-                }
-                mLoadQueueBitmapRunnable = new LoadQueueBitmapRunnable(queue, QUEUE_ROOT);
-                mHandler.post(mLoadQueueBitmapRunnable);
-            }
-        }
-    }
-
-    private void sendEmptyMenu() {
-        if (mMenuResult != null) {
-            List<CarMenu.Item> menuItems = new ArrayList<CarMenu.Item>();
-            menuItems.add(emptyQueueMenu());
-            mMenuResult.sendResult(menuItems);
-            mMenuResult = null;
-        }
-    }
-
-    private boolean enableQueueItem(List<MediaSession.QueueItem> items) {
-        if (items == null || mController == null) {
-            return false;
-        }
-
-        if (mIsQueueInMenu) {
-            // We already have a queue item; do nothing
-            return false;
-        }
-        if (TextUtils.isEmpty(mController.getQueueTitle())) {
-            // No queue title to show; do nothing
-            return false;
-        }
-        return true;
-    }
-
-    private long getActiveQueueItemId() {
-        if (mController == null) {
-            return MediaSession.QueueItem.UNKNOWN_ID;
-        }
-
-        PlaybackState playbackState = mController.getPlaybackState();
-        if (playbackState != null) {
-            return playbackState.getActiveQueueItemId();
-        } else {
-            return MediaSession.QueueItem.UNKNOWN_ID;
-        }
-    }
-
-    private final MediaManager.Listener mListener = new MediaManager.Listener() {
-
-        @Override
-        public void onMediaAppChanged(ComponentName componentName) {
-            mRootId = null;
-            if (mBrowser != null) {
-                // Unsubscribe the old id first, or else it will affect to subscribe the new one.
-                if (!TextUtils.isEmpty(mMediaId) && !QUEUE_ROOT.equals(mMediaId)) {
-                    mBrowser.unsubscribe(mMediaId);
-                    mMediaId = null;
-                }
-                mBrowser.disconnect();
-                mBrowser = null;
-            }
-            Resources resources = mContext.getResources();
-            Bundle extras = new Bundle();
-            if (resources != null) {
-                extras.putInt(EXTRA_ICON_SIZE,
-                        resources.getDimensionPixelSize(R.dimen.car_list_item_icon_size));
-            }
-            mBrowser = new MediaBrowser(mContext, componentName, mConnectionCallbacks, extras);
-            if (mController != null) {
-                mController.unregisterCallback(mControllerCallback);
-                mController = null;
-            }
-            mBrowser.connect();
-            // Only store MediaManager instance to a local variable when it is short lived.
-            MediaManager mediaManager = MediaManager.getInstance(mContext);
-            mActivity.setTitle(mediaManager.getMediaClientName().toString());
-            mActivity.setScrimColor(mediaManager.getMediaClientPrimaryColorDark());
-            mActivity.attachContentFragment();
-        }
-
-        @Override
-        public void onStatusMessageChanged(String msg) {}
-    };
-
-    private final MediaBrowser.ConnectionCallback mConnectionCallbacks =
-            new MediaBrowser.ConnectionCallback() {
-
-        @Override
-        public void onConnected() {
-            // Get the real root and will replace it with the default fake one which is set
-            // in onGetRoot().
-            mRootId = mBrowser.getRoot();
-            if (mPendingMediaId != null) {
-                mMediaId = mPendingMediaId.equals(MEDIA_APP_ROOT) ? mRootId : mPendingMediaId;
-                mPendingMediaId = null;
-            } else {
-                mMediaId = mRootId;
-            }
-            MediaSession.Token token = mBrowser.getSessionToken();
-            if (token != null) {
-                mController = new MediaController(mContext, token);
-                mController.registerCallback(mControllerCallback);
-            } else {
-                // We will still be able to browse media content, but not able to play them.
-                Log.e(TAG, "Media session token is null for "
-                        + MediaManager.getInstance(mContext).getMediaClientName());
-            }
-            if (mLoadQueueMenuPending) {
-                mLoadQueueMenuPending = false;
-                loadQueueMenu();
-            } else {
-                mBrowser.subscribe(mMediaId, mSubscriptionCallback);
-            }
-        }
-
-        @Override
-        public void onConnectionSuspended() {
-            Log.w(TAG, "Media browser service connection suspended. Waiting to be"
-                    + " reconnected....");
-        }
-
-        @Override
-        public void onConnectionFailed() {
-            Log.e(TAG, "Media browser service connection FAILED!");
-            sendEmptyMenu();
-            // disconnect anyway to make sure we get into a sanity state
-            mBrowser.disconnect();
-            mBrowser = null;
-        }
-    };
-
-    private final MediaController.Callback mControllerCallback = new MediaController.Callback() {
-
-        @Override
-        public void onSessionDestroyed() {
-            Log.e(TAG, "Media session is destroyed");
-            sendEmptyMenu();
-            if (mController != null) {
-                mController.unregisterCallback(mControllerCallback);
-            }
-            mController = null;
-        }
-
-        @Override
-        public void onPlaybackStateChanged(PlaybackState state) {
-            long activeQueueItemId = getActiveQueueItemId();
-            if (mActiveQueueItemId != activeQueueItemId) {
-                if (mMediaId == QUEUE_ROOT) {
-                    // After this call, the whole queue menu will be refreshed.
-                    notifyChildrenChanged(QUEUE_ROOT);
-                }
-                mActiveQueueItemId = activeQueueItemId;
-            }
-        }
-
-        @Override
-        public void onQueueChanged(List<MediaSession.QueueItem> queue) {
-            if (mMediaId == mRootId && enableQueueItem(queue)) {
-                notifyChildrenChanged(MEDIA_APP_ROOT);
-            }
-        }
-    };
-
-    private final MediaBrowser.SubscriptionCallback mSubscriptionCallback =
-            new MediaBrowser.SubscriptionCallback() {
-
-        @Override
-        public void onChildrenLoaded(String parentId, List<MediaBrowser.MediaItem> children) {
-            Log.d(TAG, "onChildrenLoaded" + parentId);
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "Loaded " + children.size() + " children.");
-                for (MediaBrowser.MediaItem item : children) {
-                    Log.d(TAG, "\t" + item.getDescription().getTitle());
-                }
-            }
-
-            mIsQueueInMenu = false;
-            if (mController == null) {
-                if (Log.isLoggable(TAG, Log.DEBUG)) {
-                    Log.d(TAG, "MediaController is null in SubscriptionCallback.");
-                }
-                sendEmptyMenu();
-                // the session has been destroyed or we have moved to another facet.
-                return;
-            }
-
-            mItems = new ArrayList<>(children);
-            mHandler.removeCallbacks(mUpdateMenuRunnable);
-            mUpdateMenuRunnable.setParentId(parentId);
-            mHandler.post(mUpdateMenuRunnable);
-        }
-
-        @Override
-        public void onError(String mediaId) {
-            Log.e(TAG, "onError getting items for " + mediaId);
-            sendEmptyMenu();
-        }
-    };
-
-    private class UpdateMenuRunnable implements Runnable {
-        private String mParentId;
-
-        void setParentId(String parentId) {
-            mParentId = parentId;
-        }
-
-        @Override
-        public void run() {
-            if (mMenuResult == null) {
-                Log.e(TAG, "CarMenu is null while update menu, notify change instead.");
-                notifyChildrenChanged(mParentId);
-                return;
-            }
-
-            if (mItems == null) {
-                throw new IllegalArgumentException(
-                        "You must supply CarMenu with a list of MediaItems.");
-            }
-
-            boolean hasImages = false;
-            for (MediaBrowser.MediaItem item : mItems) {
-                if ((item.getDescription().getIconUri() != null)
-                        || (item.getDescription().getIconBitmap() != null)) {
-                    hasImages = true;
-                    break;
-                }
-            }
-            List<CarMenu.Item> menuItems = new ArrayList<>();
-            for (MediaBrowser.MediaItem item : mItems) {
-                menuItems.add(convertMediaItemToMenuItem(item, hasImages));
-            }
-            // If it is under root menu and play queue is not empty, add "Queue" item to the menu.
-            if (mMediaId.equals(mRootId) && mController != null) {
-                List<MediaSession.QueueItem> queue = mController.getQueue();
-                if (queue != null && queue.size() > 0
-                        && !TextUtils.isEmpty(mController.getQueueTitle())) {
-                    String queueTitle = mController.getQueueTitle().toString();
-                    menuItems.add(new CarMenu.Builder(QUEUE_ROOT).setTitle(queueTitle)
-                            .setFlags(CarMenuConstantsComapt.MenuItemConstants.FLAG_BROWSABLE)
-                            .build());
-                    mIsQueueInMenu = true;
-                }
-            }
-            if (menuItems.size() == 0) {
-                sendEmptyMenu();
-            } else {
-                mMenuResult.sendResult(menuItems);
-                mMenuResult = null;
-            }
-
-            if (hasImages) {
-                if (mLoadMenuBitmapRunnable != null) {
-                    mHandler.removeCallbacks(mLoadMenuBitmapRunnable);
-                }
-                // Due to we return fake root id in onGetRoot(), when we call notifyChildChanged()
-                // we still need to use the fake root id instead of the real one.
-                if (mMediaId.equals(mRootId)) {
-                    mLoadMenuBitmapRunnable = new LoadMenuBitmapRunnable(mItems, MEDIA_APP_ROOT);
-                } else {
-                    mLoadMenuBitmapRunnable = new LoadMenuBitmapRunnable(mItems, mMediaId);
-                }
-                mHandler.post(mLoadMenuBitmapRunnable);
-            }
-        }
-
-        /**
-         * Returns CarMenu.Item which is used in rendering menu.
-         *
-         * @param item MediaItem which has all info to render menu.
-         * @param hasImages Whether the menu item has image or not.
-         * @return menu item.
-         */
-        private CarMenu.Item convertMediaItemToMenuItem(MediaBrowser.MediaItem item,
-                boolean hasImages) {
-            CarMenu.Builder builder = new CarMenu.Builder(item.getMediaId());
-            CharSequence title = item.getDescription().getTitle();
-            if (title != null) {
-                builder.setTitle(title.toString());
-            }
-            CharSequence subTitle = item.getDescription().getSubtitle();
-            if (subTitle != null) {
-                builder.setText(subTitle.toString());
-            }
-            if (item.isBrowsable()) {
-                builder.setFlags(CarMenuConstantsComapt.MenuItemConstants.FLAG_BROWSABLE);
-            }
-            // Place empty bitmap as place holder first, we will load the bitamp later.
-            if (hasImages) {
-                builder.setIcon(null);
-            }
-            return builder.build();
-        }
-    }
-
-    private class LoadQueueBitmapRunnable implements Runnable {
-        private final List<MediaSession.QueueItem> mQueue;
-        private final String mParentId;
-
-        public LoadQueueBitmapRunnable(List<MediaSession.QueueItem> queue, String parentId) {
-            mQueue = queue;
-            mParentId = parentId;
-        }
-
-        @Override
-        public void run() {
-            boolean activeQueueItemFound = false;
-            for (MediaSession.QueueItem item : mQueue) {
-                if (item.getQueueId() == mActiveQueueItemId) {
-                    activeQueueItemFound = true;
-                }
-
-                if (activeQueueItemFound) {
-                    MediaMenuBitmapDownloader downloader = new MediaMenuBitmapDownloader(mContext,
-                            MediaCarMenuCallbacks.this, mParentId,
-                            QUEUE_ITEM_PREFIX + item.getQueueId(), mHandler);
-                    downloader.setMenuBitmap(item.getDescription());
-                }
-            }
-        }
-    }
-
-    private class LoadMenuBitmapRunnable implements Runnable {
-        private List<MediaBrowser.MediaItem> mItemList;
-        private String mParentId;
-
-        public LoadMenuBitmapRunnable(List<MediaBrowser.MediaItem> itemList, String parentId) {
-            mItemList = itemList;
-            mParentId = parentId;
-        }
-
-        @Override
-        public void run() {
-            for (MediaBrowser.MediaItem item : mItemList) {
-                MediaMenuBitmapDownloader downloader = new MediaMenuBitmapDownloader(mContext,
-                        MediaCarMenuCallbacks.this, mParentId, item.getMediaId(), mHandler);
-                downloader.setMenuBitmap(item.getDescription());
-            }
-        }
-    }
-}
diff --git a/src/com/android/car/media/MediaMenuBitmapDownloader.java b/src/com/android/car/media/MediaMenuBitmapDownloader.java
deleted file mode 100644
index ea8e370..0000000
--- a/src/com/android/car/media/MediaMenuBitmapDownloader.java
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.media;
-
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.media.MediaDescription;
-import android.net.Uri;
-import android.os.Handler;
-import android.support.car.app.menu.CarMenu;
-import android.util.Log;
-import com.android.car.apps.common.BitmapDownloader;
-import com.android.car.apps.common.BitmapWorkerOptions;
-import com.android.car.apps.common.UriUtils;
-
-import java.lang.ref.WeakReference;
-
-/**
- * Download the icon for car menu item. Once it is done, it will update the icon by calling
- * CarMenuCallbacks.notifyChildChanged(), which is needed to be called after CarMenu.sendResult().
- */
-public class MediaMenuBitmapDownloader {
-    private static final String TAG = "GH.MBDownloader";
-    private static final int MAX_ALBUM_ART_DOWNLOAD_RETRIES = 10;
-    private static final long ALBUM_ART_DOWNLOAD_RETRY_INTERVAL_MS = 1000;
-
-    private final WeakReference<Context> mContext;
-    private final MediaCarMenuCallbacks mCallback;
-    private final String mParentId;
-    private final String mChildId;
-    private final Handler mHandler;
-    private BitmapDownloadRunnable mBitmapDownloadRunnable;
-
-    public MediaMenuBitmapDownloader(Context context, MediaCarMenuCallbacks callback,
-            String parentId, String childId, Handler handler) {
-        mContext = new WeakReference<>(context);
-        mCallback = callback;
-        mParentId = parentId;
-        mChildId = childId;
-        mHandler = handler;
-    }
-
-    public void setMenuBitmap(MediaDescription description) {
-        if (description == null) {
-            Log.w(TAG, "null media descriptor");
-            return;
-        }
-
-        if (mBitmapDownloadRunnable != null) {
-            mHandler.removeCallbacks(mBitmapDownloadRunnable);
-            mBitmapDownloadRunnable.cancelDownload();
-            mBitmapDownloadRunnable = null;
-        }
-
-        Bitmap bitmap = description.getIconBitmap();
-        Uri iconUri = description.getIconUri();
-        if (bitmap == null && iconUri == null) {
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "no bitmap or icon uri found");
-            }
-        } else if (bitmap != null) {
-            updateIcon(bitmap);
-        } else {
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "downloading bitmap " + iconUri);
-            }
-            mBitmapDownloadRunnable = new BitmapDownloadRunnable(iconUri);
-            mHandler.post(mBitmapDownloadRunnable);
-        }
-    }
-
-    private void updateIcon(Bitmap bitmap) {
-        mCallback.notifyChildChanged(mParentId,
-                new CarMenu.Builder(mChildId).setIcon(bitmap).build());
-    }
-
-    private class BitmapDownloadRunnable implements Runnable {
-        private Uri mIconUri;
-        private int mRetries;
-        private BitmapDownloader.BitmapCallback mBitmapCallback;
-
-        public BitmapDownloadRunnable(Uri icon_uri) {
-            mIconUri = icon_uri;
-            mRetries = 0;
-        }
-
-        public void cancelDownload() {
-            if (mBitmapCallback != null) {
-                Context context = mContext.get();
-                if (context == null) {
-                    return;
-                }
-
-                BitmapDownloader.getInstance(context).cancelDownload(mBitmapCallback);
-            }
-        }
-
-        @Override
-        public void run() {
-            mBitmapCallback = new BitmapDownloader.BitmapCallback() {
-                @Override
-                public void onBitmapRetrieved(Bitmap bitmap) {
-                    if (bitmap == null) {
-                        if (++mRetries <= MAX_ALBUM_ART_DOWNLOAD_RETRIES) {
-                            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                                Log.d(TAG, "retrying after failing to download bitmap "
-                                        + mIconUri.toString());
-                            }
-                            mHandler.postDelayed(BitmapDownloadRunnable.this,
-                                    ALBUM_ART_DOWNLOAD_RETRY_INTERVAL_MS);
-                        }
-                    } else {
-                        if (Log.isLoggable(TAG, Log.DEBUG)) {
-                            Log.d(TAG, "downloaded bitmap " + mIconUri.toString() + " retries:"
-                                    + mRetries);
-                        }
-                        updateIcon(bitmap);
-                    }
-                }
-            };
-
-            Context context = mContext.get();
-            if (context == null) {
-                return;
-            }
-
-            int bitmapSize =
-                    context.getResources().getDimensionPixelSize(R.dimen.car_list_item_icon_size);
-            BitmapDownloader.getInstance(context)
-                    .getBitmap(
-                            new BitmapWorkerOptions.Builder(context).resource(mIconUri)
-                                    .height(bitmapSize)
-                                    .width(bitmapSize)
-                                    // We don't want to cache android resources as they are needed
-                                    // to be refreshed after configuration changes.
-                                    .cacheFlag(UriUtils.isAndroidResourceUri(mIconUri)
-                                            ? (BitmapWorkerOptions.CACHE_FLAG_DISK_DISABLED
-                                            | BitmapWorkerOptions.CACHE_FLAG_MEM_DISABLED)
-                                            : 0)
-                                    .build(),
-                            mBitmapCallback);
-        }
-    }
-}
diff --git a/src/com/android/car/media/MediaPlaybackFragment.java b/src/com/android/car/media/MediaPlaybackFragment.java
index 04bd846..8bd44f1 100644
--- a/src/com/android/car/media/MediaPlaybackFragment.java
+++ b/src/com/android/car/media/MediaPlaybackFragment.java
@@ -144,11 +144,10 @@
         mActivity = (MediaActivity) getHost();
         mShowTitleDelayMs =
                 mActivity.getResources().getInteger(R.integer.new_album_art_fade_in_offset);
-        mMediaPlaybackModel =
-                new MediaPlaybackModel(mActivity.getContext(), null /* browserExtras */);
+        mMediaPlaybackModel = new MediaPlaybackModel(mActivity, null /* browserExtras */);
         mMediaPlaybackModel.addListener(this);
-        mTelephonyManager = (TelephonyManager) mActivity.getContext()
-                .getSystemService(Context.TELEPHONY_SERVICE);
+        mTelephonyManager =
+                (TelephonyManager) mActivity.getSystemService(Context.TELEPHONY_SERVICE);
     }
 
     @Override
@@ -156,7 +155,6 @@
         super.onDestroy();
         mCurrentView = null;
         mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
-        mMediaPlaybackModel.onDestroy();
         mMediaPlaybackModel = null;
         mActivity = null;
         // Calling this with null will clear queue of callbacks and message.
@@ -237,7 +235,7 @@
     @Override
     public void onPause() {
         super.onPause();
-        mMediaPlaybackModel.onPause();
+        mMediaPlaybackModel.stop();
         mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
     }
 
@@ -252,7 +250,7 @@
     @Override
     public void onResume() {
         super.onResume();
-        mMediaPlaybackModel.onResume();
+        mMediaPlaybackModel.start();
         // Note: at registration, TelephonyManager will invoke the callback with the current state.
         mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
     }
@@ -271,8 +269,7 @@
         int overflowViewColor = mMediaPlaybackModel.getPrimaryColorDark();
         mOverflowView.getBackground().setColorFilter(overflowViewColor, PorterDuff.Mode.SRC_IN);
         // Tint the overflow actions light or dark depending on contrast.
-        int overflowTintColor = ColorChecker.getTintColor(
-                mActivity.getContext(), overflowViewColor);
+        int overflowTintColor = ColorChecker.getTintColor(mActivity, overflowViewColor);
         for (ImageView v : mCustomActionButtons) {
             v.setColorFilter(overflowTintColor, PorterDuff.Mode.SRC_IN);
         }
@@ -335,7 +332,7 @@
             }
             showInitialNoContentView(state.getErrorMessage() != null ?
                     state.getErrorMessage().toString() :
-                    mActivity.getContext().getString(R.string.unknown_error), true);
+                    mActivity.getString(R.string.unknown_error), true);
             return;
         }
 
@@ -400,7 +397,6 @@
                     icon == null || mReturnFromOnStop ? 0 : mShowTitleDelayMs);
         }
         Uri iconUri = getMetadataIconUri(metadata);
-        Context context = mActivity.getContext();
         if (icon != null) {
             Bitmap scaledIcon = cropAlbumArt(icon);
             if (scaledIcon != icon && !icon.isRecycled()) {
@@ -412,7 +408,7 @@
             mActivity.setBackgroundBitmap(scaledIcon, !mReturnFromOnStop /* showAnimation */);
         } else if (iconUri != null) {
             if (mDownloader == null) {
-                mDownloader = new BitmapDownloader(context);
+                mDownloader = new BitmapDownloader(mActivity);
             }
             final int flags = BitmapWorkerOptions.CACHE_FLAG_DISK_DISABLED
                     | BitmapWorkerOptions.CACHE_FLAG_MEM_DISABLED;
@@ -420,7 +416,7 @@
                 Log.v(TAG, "Album art size " + mAlbumArtWidth + "x" + mAlbumArtHeight);
             }
 
-            mDownloader.getBitmap(new BitmapWorkerOptions.Builder(context).resource(iconUri)
+            mDownloader.getBitmap(new BitmapWorkerOptions.Builder(mActivity).resource(iconUri)
                             .height(mAlbumArtHeight).width(mAlbumArtWidth).cacheFlag(flags).build(),
                     new BitmapDownloader.BitmapCallback() {
                         @Override
@@ -505,7 +501,7 @@
                         }
                     });
 
-            int tint = ColorChecker.getTintColor(mActivity.getContext(),
+            int tint = ColorChecker.getTintColor(mActivity,
                     mMediaPlaybackModel.getPrimaryColorDark());
             mSeekBar.getProgressDrawable().setColorFilter(tint, PorterDuff.Mode.SRC_IN);
         } else {
@@ -870,9 +866,7 @@
             } else {
                 switch (v.getId()) {
                     case R.id.play_queue:
-                        CharSequence title = mMediaPlaybackModel.getQueueTitle();
-                        mActivity.showMenu(MediaCarMenuCallbacks.QUEUE_ROOT, title.toString());
-                        mActivity.openDrawer();
+                        mActivity.showQueueInDrawer();
                         break;
                     case R.id.prev:
                         transportControls.skipToPrevious();
diff --git a/src/com/android/car/media/MediaPlaybackModel.java b/src/com/android/car/media/MediaPlaybackModel.java
index 6f234e2..03a8817 100644
--- a/src/com/android/car/media/MediaPlaybackModel.java
+++ b/src/com/android/car/media/MediaPlaybackModel.java
@@ -46,7 +46,7 @@
  * main thread. Intended to provide a much more usable model interface to UI code.
  */
 public class MediaPlaybackModel {
-    private static final String TAG = "GH.MediaPlaybackModel";
+    private static final String TAG = "MediaPlaybackModel";
 
     private final Context mContext;
     private final Bundle mBrowserExtras;
@@ -55,6 +55,7 @@
     private Handler mHandler;
     private MediaController mController;
     private MediaBrowser mBrowser;
+    private int mPrimaryColor;
     private int mPrimaryColorDark;
     private int mAccentColor;
     private ComponentName mCurrentComponentName;
@@ -94,6 +95,29 @@
         void onSessionDestroyed(CharSequence destroyedMediaClientName);
     }
 
+    /** Convenient Listener base class for extension */
+    public static abstract class AbstractListener implements Listener {
+        @Override
+        public void onMediaAppChanged(@Nullable ComponentName currentName,
+                @Nullable ComponentName newName) {}
+        @Override
+        public void onMediaAppStatusMessageChanged(@Nullable String message) {}
+        @Override
+        public void onMediaConnected() {}
+        @Override
+        public void onMediaConnectionSuspended() {}
+        @Override
+        public void onMediaConnectionFailed(CharSequence failedMediaClientName) {}
+        @Override
+        public void onPlaybackStateChanged(@Nullable PlaybackState state) {}
+        @Override
+        public void onMetadataChanged(@Nullable MediaMetadata metadata) {}
+        @Override
+        public void onQueueChanged(List<MediaSession.QueueItem> queue) {}
+        @Override
+        public void onSessionDestroyed(CharSequence destroyedMediaClientName) {}
+    }
+
     public MediaPlaybackModel(Context context, Bundle browserExtras) {
         mContext = context;
         mBrowserExtras = browserExtras;
@@ -101,13 +125,13 @@
     }
 
     @MainThread
-    public void onDestroy() {
+    public void start() {
         Assert.isMainThread();
-        mHandler = null;
+        MediaManager.getInstance(mContext).addListener(mMediaManagerListener);
     }
 
     @MainThread
-    public void onPause() {
+    public void stop() {
         Assert.isMainThread();
         MediaManager.getInstance(mContext).removeListener(mMediaManagerListener);
         if (mBrowser != null) {
@@ -126,12 +150,6 @@
     }
 
     @MainThread
-    public void onResume() {
-        Assert.isMainThread();
-        MediaManager.getInstance(mContext).addListener(mMediaManagerListener);
-    }
-
-    @MainThread
     public void addListener(MediaPlaybackModel.Listener listener) {
         Assert.isMainThread();
         mListeners.add(listener);
@@ -146,8 +164,11 @@
     @MainThread
     private void notifyListeners(Consumer<Listener> callback) {
         Assert.isMainThread();
+        // Clone mListeners in case any of the callbacks made triggers a listener to be added or
+        // removed to/from mListeners.
+        List<Listener> listenersCopy = new LinkedList<>(mListeners);
         // Invokes callback.accept(listener) for each listener.
-        mListeners.forEach(callback);
+        listenersCopy.forEach(callback);
     }
 
     @MainThread
@@ -157,6 +178,12 @@
     }
 
     @MainThread
+    public int getPrimaryColor() {
+        Assert.isMainThread();
+        return mPrimaryColor;
+    }
+
+    @MainThread
     public int getAccentColor() {
         Assert.isMainThread();
         return mAccentColor;
@@ -227,6 +254,12 @@
     }
 
     @MainThread
+    public MediaBrowser getMediaBrowser() {
+        Assert.isMainThread();
+        return mBrowser;
+    }
+
+    @MainThread
     public MediaController.TransportControls getTransportControls() {
         Assert.isMainThread();
         if (mController == null) {
@@ -266,10 +299,10 @@
                 mBrowser.connect();
 
                 // reset the colors and views if we switch to another app.
-                mAccentColor = MediaManager.getInstance(mContext)
-                        .getMediaClientAccentColor();
-                mPrimaryColorDark = MediaManager.getInstance(mContext)
-                        .getMediaClientPrimaryColorDark();
+                MediaManager manager = MediaManager.getInstance(mContext);
+                mPrimaryColor = manager.getMediaClientPrimaryColor();
+                mAccentColor = manager.getMediaClientAccentColor();
+                mPrimaryColorDark = manager.getMediaClientPrimaryColorDark();
 
                 final ComponentName currentName = mCurrentComponentName;
                 notifyListeners((listener) -> listener.onMediaAppChanged(currentName, name));
diff --git a/src/com/android/car/media/MediaProxyActivity.java b/src/com/android/car/media/MediaProxyActivity.java
deleted file mode 100644
index c1950db..0000000
--- a/src/com/android/car/media/MediaProxyActivity.java
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.media;
-
-import android.support.car.app.CarProxyActivity;
-
-
-public class MediaProxyActivity extends CarProxyActivity {
-
-    public MediaProxyActivity() {
-        super(MediaActivity.class);
-    }
-}
diff --git a/src/com/android/car/media/drawer/MediaBrowserItemsFetcher.java b/src/com/android/car/media/drawer/MediaBrowserItemsFetcher.java
new file mode 100644
index 0000000..80bc5d7
--- /dev/null
+++ b/src/com/android/car/media/drawer/MediaBrowserItemsFetcher.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.media.drawer;
+
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.media.browse.MediaBrowser;
+import android.media.session.MediaController;
+import android.media.session.MediaSession;
+import android.util.Log;
+
+import com.android.car.app.CarDrawerActivity;
+import com.android.car.app.DrawerItemViewHolder;
+import com.android.car.media.MediaPlaybackModel;
+import com.android.car.media.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * {@link MediaItemsFetcher} implementation that fetches items from a specific {@link MediaBrowser}
+ * node.
+ * <p>
+ * It optionally supports surfacing the Media app's queue as the last item.
+ */
+class MediaBrowserItemsFetcher implements MediaItemsFetcher {
+    private static final String TAG = "Media.BrowserFetcher";
+
+    private final CarDrawerActivity mActivity;
+    private final MediaPlaybackModel mMediaPlaybackModel;
+    private final String mMediaId;
+    private final boolean mShowQueueItem;
+    private ItemsUpdatedCallback mCallback;
+    private List<MediaBrowser.MediaItem> mItems = new ArrayList<>();
+    private boolean mQueueAvailable;
+
+    MediaBrowserItemsFetcher(CarDrawerActivity activity, MediaPlaybackModel model, String mediaId,
+            boolean showQueueItem) {
+        mActivity = activity;
+        mMediaPlaybackModel = model;
+        mMediaId = mediaId;
+        mShowQueueItem = showQueueItem;
+    }
+
+    @Override
+    public void start(ItemsUpdatedCallback callback) {
+        mCallback = callback;
+        updateQueueAvailability();
+        mMediaPlaybackModel.getMediaBrowser().subscribe(mMediaId, mSubscriptionCallback);
+        mMediaPlaybackModel.addListener(mModelListener);
+    }
+
+    private final MediaPlaybackModel.Listener mModelListener =
+            new MediaPlaybackModel.AbstractListener() {
+        @Override
+        public void onQueueChanged(List<MediaSession.QueueItem> queue) {
+            updateQueueAvailability();
+        }
+        @Override
+        public void onSessionDestroyed(CharSequence destroyedMediaClientName) {
+            updateQueueAvailability();
+        }
+    };
+
+    private final MediaBrowser.SubscriptionCallback mSubscriptionCallback =
+        new MediaBrowser.SubscriptionCallback() {
+            @Override
+            public void onChildrenLoaded(String parentId, List<MediaBrowser.MediaItem> children) {
+                mItems.clear();
+                mItems.addAll(children);
+                mCallback.onItemsUpdated();
+            }
+
+            @Override
+            public void onError(String parentId) {
+                Log.e(TAG, "Error loading children of: " + mMediaId);
+                mItems.clear();
+                mCallback.onItemsUpdated();
+            }
+        };
+
+    private void updateQueueAvailability() {
+        if (mShowQueueItem && !mMediaPlaybackModel.getQueue().isEmpty()) {
+            mQueueAvailable = true;
+        }
+    }
+
+    @Override
+    public int getItemCount() {
+        int size = mItems.size();
+        if (mQueueAvailable) {
+            size++;
+        }
+        return size;
+    }
+
+    @Override
+    public void populateViewHolder(DrawerItemViewHolder holder, int position) {
+        if (mQueueAvailable && position == mItems.size()) {
+            holder.getTitle().setText(mMediaPlaybackModel.getQueueTitle());
+            return;
+        }
+        MediaBrowser.MediaItem item = mItems.get(position);
+        MediaItemsFetcher.populateViewHolderFrom(holder, item.getDescription());
+
+        // TODO(sriniv): Once we use smallLayout, text and rightIcon fields may be unavailable.
+        // Related to b/36573125.
+        if (item.isBrowsable()) {
+            int iconColor = mActivity.getColor(R.color.car_tint);
+            Drawable drawable = mActivity.getDrawable(R.drawable.ic_chevron_right);
+            drawable.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN);
+            holder.getRightIcon().setImageDrawable(drawable);
+        } else {
+            holder.getRightIcon().setImageDrawable(null);
+        }
+    }
+
+    @Override
+    public void onItemClick(int position) {
+        if (mQueueAvailable && position == mItems.size()) {
+            MediaItemsFetcher fetcher = new MediaQueueItemsFetcher(mActivity, mMediaPlaybackModel);
+            setupAdapterAndSwitch(fetcher, mMediaPlaybackModel.getQueueTitle());
+            return;
+        }
+
+        MediaBrowser.MediaItem item = mItems.get(position);
+        if (item.isBrowsable()) {
+            MediaItemsFetcher fetcher = new MediaBrowserItemsFetcher(
+                    mActivity, mMediaPlaybackModel, item.getMediaId(), false /* showQueueItem */);
+            setupAdapterAndSwitch(fetcher, item.getDescription().getTitle());
+        } else if (item.isPlayable()) {
+            MediaController.TransportControls controls = mMediaPlaybackModel.getTransportControls();
+            if (controls != null) {
+                controls.pause();
+                controls.playFromMediaId(item.getMediaId(), item.getDescription().getExtras());
+            }
+            mActivity.closeDrawer();
+        } else {
+            Log.w(TAG, "Unknown item type; don't know how to handle!");
+        }
+    }
+
+    private void setupAdapterAndSwitch(MediaItemsFetcher fetcher, CharSequence title) {
+        MediaDrawerAdapter subAdapter = new MediaDrawerAdapter(mActivity, false /* smallLayout */);
+        subAdapter.setFetcher(fetcher);
+        subAdapter.setTitle(title);
+        mActivity.switchToAdapter(subAdapter);
+    }
+
+    @Override
+    public void cleanup() {
+        mMediaPlaybackModel.removeListener(mModelListener);
+        mMediaPlaybackModel.getMediaBrowser().unsubscribe(mMediaId);
+        mCallback = null;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/car/media/drawer/MediaDrawerAdapter.java b/src/com/android/car/media/drawer/MediaDrawerAdapter.java
new file mode 100644
index 0000000..dc483a2
--- /dev/null
+++ b/src/com/android/car/media/drawer/MediaDrawerAdapter.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.media.drawer;
+
+import com.android.car.app.CarDrawerActivity;
+import com.android.car.app.CarDrawerAdapter;
+import com.android.car.app.DrawerItemViewHolder;
+
+/**
+ * Subclass of CarDrawerAdapter used by the Media app.
+ * <p>
+ * This adapter delegates actual fetching of items (and other operations) to a
+ * {@link MediaItemsFetcher}. The current fetcher being used can be updated at runtime.
+ */
+class MediaDrawerAdapter extends CarDrawerAdapter {
+    private final CarDrawerActivity mActivity;
+    private MediaItemsFetcher mCurrentFetcher;
+
+    MediaDrawerAdapter(CarDrawerActivity activity, boolean useSmallLayout) {
+        super(activity, true /* showDisabledListOnEmpty */, useSmallLayout);
+        mActivity = activity;
+    }
+
+    /**
+     * Switch the {@link MediaItemsFetcher} being used to fetch items. The new fetcher is kicked-off
+     * and the drawer's content's will be updated to show newly loaded items. Any old fetcher is
+     * cleaned up and released.
+     *
+     * @param fetcher New {@link MediaItemsFetcher} to use for display Drawer items.
+     */
+    void setFetcher(MediaItemsFetcher fetcher) {
+        if (mCurrentFetcher != null) {
+            mCurrentFetcher.cleanup();
+        }
+        mCurrentFetcher = fetcher;
+        mCurrentFetcher.start(() -> {
+            mActivity.showLoadingProgressBar(false);
+            notifyDataSetChanged();
+        });
+        // Initially there will be no items and we don't want to show empty-list indicator briefly
+        // until items are fetched.
+        mActivity.showLoadingProgressBar(true);
+    }
+
+    @Override
+    protected int getActualItemCount() {
+        return mCurrentFetcher != null ? mCurrentFetcher.getItemCount() : 0;
+    }
+
+    @Override
+    protected void populateViewHolder(DrawerItemViewHolder holder, int position) {
+        if (mCurrentFetcher != null) {
+            mCurrentFetcher.populateViewHolder(holder, position);
+        }
+    }
+
+    @Override
+    public void onItemClick(int position) {
+        if (mCurrentFetcher != null) {
+            mCurrentFetcher.onItemClick(position);
+        }
+    }
+}
diff --git a/src/com/android/car/media/drawer/MediaDrawerController.java b/src/com/android/car/media/drawer/MediaDrawerController.java
new file mode 100644
index 0000000..e1715bd
--- /dev/null
+++ b/src/com/android/car/media/drawer/MediaDrawerController.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.media.drawer;
+
+import android.content.ComponentName;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v4.widget.DrawerLayout;
+import android.view.View;
+
+import com.android.car.app.CarDrawerActivity;
+import com.android.car.app.CarDrawerAdapter;
+import com.android.car.media.MediaManager;
+import com.android.car.media.MediaPlaybackModel;
+import com.android.car.media.R;
+
+/**
+ * Manages overall Drawer functionality.
+ * <p>
+ * Maintains separate MediaPlaybackModel for media browsing and control. Sets up root Drawer
+ * adapter with root of media-browse tree (using MediaBrowserItemsFetcher). Supports switching the
+ * rootAdapter to show the queue-items (using MediaQueueItemsFetcher).
+ */
+public class MediaDrawerController {
+    private static final String TAG = "MediaDrawerController";
+    private static final String EXTRA_ICON_SIZE =
+            "com.google.android.gms.car.media.BrowserIconSize";
+
+    private final CarDrawerActivity mActivity;
+    private final MediaPlaybackModel mMediaPlaybackModel;
+    private MediaDrawerAdapter mRootAdapter;
+
+    public MediaDrawerController(CarDrawerActivity activity) {
+        mActivity = activity;
+        Bundle extras = new Bundle();
+        extras.putInt(EXTRA_ICON_SIZE,
+                mActivity.getResources().getDimensionPixelSize(R.dimen.car_list_item_icon_size));
+        mMediaPlaybackModel = new MediaPlaybackModel(mActivity, extras);
+        mMediaPlaybackModel.addListener(mModelListener);
+
+        // TODO(sriniv): Needs smallLayout below. But breaks when showing queue items (b/36573125).
+        mRootAdapter = new MediaDrawerAdapter(mActivity, false /* useSmallLayout */);
+        // Start with a empty title since we depend on the mMediaManagerListener callback to
+        // know which app is being used and set the actual title there.
+        mRootAdapter.setTitle("");
+
+        // Kick off MediaBrowser/MediaController connection.
+        mMediaPlaybackModel.start();
+    }
+
+    public void cleanup() {
+        mRootAdapter.cleanup();
+        mMediaPlaybackModel.stop();
+    }
+
+    /**
+     * @return Adapter to display root items of MediaBrowse tree. {@link #showQueueInDrawer()} can
+     *      be used to display items from the queue.
+     */
+    public CarDrawerAdapter getRootAdapter() {
+        return mRootAdapter;
+    }
+
+    private MediaItemsFetcher createRootMediaItemsFetcher() {
+        return new MediaBrowserItemsFetcher(mActivity, mMediaPlaybackModel,
+                mMediaPlaybackModel.getMediaBrowser().getRoot(), true /* showQueueItem */);
+    }
+
+    private final MediaPlaybackModel.Listener mModelListener =
+            new MediaPlaybackModel.AbstractListener() {
+        @Override
+        public void onMediaAppChanged(@Nullable ComponentName currentName,
+                @Nullable ComponentName newName) {
+            // Only store MediaManager instance to a local variable when it is short lived.
+            MediaManager mediaManager = MediaManager.getInstance(mActivity);
+            mRootAdapter.setTitle(mediaManager.getMediaClientName());
+        }
+
+        @Override
+        public void onMediaConnected() {
+            mRootAdapter.setFetcher(createRootMediaItemsFetcher());
+        }
+    };
+
+    /**
+     * Switch the rootAdapter to show items from the currently playing Queue and open the drawer.
+     * When the drawer is closed, the adapter items are switched back to media-browse root.
+     */
+    public void showQueueInDrawer() {
+        mRootAdapter.setFetcher(new MediaQueueItemsFetcher(mActivity, mMediaPlaybackModel));
+        mRootAdapter.setTitle(mMediaPlaybackModel.getQueueTitle());
+        mActivity.openDrawer();
+        mActivity.addDrawerListener(new DrawerLayout.DrawerListener() {
+            @Override
+            public void onDrawerClosed(View drawerView) {
+                mRootAdapter.setFetcher(createRootMediaItemsFetcher());
+                mActivity.removeDrawerListener(this);
+                mRootAdapter.setTitle(
+                        MediaManager.getInstance(mActivity).getMediaClientName());
+            }
+
+            @Override
+            public void onDrawerSlide(View drawerView, float slideOffset) {}
+            @Override
+            public void onDrawerOpened(View drawerView) {}
+            @Override
+            public void onDrawerStateChanged(int newState) {}
+        });
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/car/media/drawer/MediaItemsFetcher.java b/src/com/android/car/media/drawer/MediaItemsFetcher.java
new file mode 100644
index 0000000..a712f7b
--- /dev/null
+++ b/src/com/android/car/media/drawer/MediaItemsFetcher.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.media.drawer;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.media.MediaDescription;
+
+import com.android.car.app.DrawerItemViewHolder;
+import com.android.car.apps.common.BitmapDownloader;
+import com.android.car.apps.common.BitmapWorkerOptions;
+import com.android.car.apps.common.UriUtils;
+import com.android.car.media.R;
+
+/**
+ * Component that handles fetching of items for {@link MediaDrawerAdapter}.
+ * <p>
+ * It also handles ViewHolder population and item clicks.
+ */
+interface MediaItemsFetcher {
+    /**
+     * Used to inform owning {@link MediaDrawerAdapter} that items have changed.
+     */
+    interface ItemsUpdatedCallback {
+        void onItemsUpdated();
+    }
+
+    /**
+     * Kick-off fetching/monitoring of items.
+     *
+     * @param callback Callback that is invoked when items are first loaded ar if they change
+     *                 subsequently.
+     */
+    void start(ItemsUpdatedCallback callback);
+
+    /**
+     * @return Number of items currently fetched.
+     */
+    int getItemCount();
+
+    /**
+     * Used by owning {@link MediaDrawerAdapter} to populate views.
+     *
+     * @param holder View-holder to populate.
+     * @param position Item position.
+     */
+    void populateViewHolder(DrawerItemViewHolder holder, int position);
+
+    /**
+     * Used by owning {@link MediaDrawerAdapter} to handle clicks.
+     *
+     * @param position Item position.
+     */
+    void onItemClick(int position);
+
+    /**
+     * Used when this instance is going to be released. Subclasses should release resources.
+     */
+    void cleanup();
+
+    /**
+     * Utility method to populate {@code holder} with details from {@code description}. It populates
+     * title, text and icon at most.
+     */
+    static void populateViewHolderFrom(DrawerItemViewHolder holder, MediaDescription description) {
+        Context context = holder.itemView.getContext();
+        // TODO(sriniv): Once we use smallLayout, text and rightIcon fields may be unavailable.
+        // Related to b/36573125.
+        holder.getTitle().setText(description.getTitle());
+        holder.getText().setText(description.getSubtitle());
+        Bitmap iconBitmap = description.getIconBitmap();
+        holder.getIcon().setImageBitmap(iconBitmap);    // Ok to set null here for clearing.
+        if (iconBitmap == null && description.getIconUri() != null) {
+            int bitmapSize =
+                    context.getResources().getDimensionPixelSize(R.dimen.car_list_item_icon_size);
+            // We don't want to cache android resources as they are needed to be refreshed after
+            // configuration changes.
+            int cacheFlag = UriUtils.isAndroidResourceUri(description.getIconUri())
+                    ? (BitmapWorkerOptions.CACHE_FLAG_DISK_DISABLED
+                    | BitmapWorkerOptions.CACHE_FLAG_MEM_DISABLED)
+                    : 0;
+            BitmapWorkerOptions options = new BitmapWorkerOptions.Builder(context)
+                    .resource(description.getIconUri())
+                    .height(bitmapSize)
+                    .width(bitmapSize)
+                    .cacheFlag(cacheFlag)
+                    .build();
+            BitmapDownloader.getInstance(context).loadBitmap(options, holder.getIcon());
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/car/media/drawer/MediaQueueItemsFetcher.java b/src/com/android/car/media/drawer/MediaQueueItemsFetcher.java
new file mode 100644
index 0000000..d27d092
--- /dev/null
+++ b/src/com/android/car/media/drawer/MediaQueueItemsFetcher.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.media.drawer;
+
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.media.session.MediaController;
+import android.media.session.MediaSession;
+import android.media.session.PlaybackState;
+import android.os.Handler;
+import android.support.annotation.Nullable;
+
+import com.android.car.app.CarDrawerActivity;
+import com.android.car.app.DrawerItemViewHolder;
+import com.android.car.media.MediaPlaybackModel;
+import com.android.car.media.R;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * {@link MediaItemsFetcher} implementation that fetches items from the {@link MediaController}'s
+ * currently playing queue.
+ */
+class MediaQueueItemsFetcher implements MediaItemsFetcher {
+    private static final String TAG = "MediaQueueItemsFetcher";
+
+    private final Handler mHandler = new Handler();
+    private final CarDrawerActivity mActivity;
+    private MediaPlaybackModel mMediaPlaybackModel;
+    private ItemsUpdatedCallback mCallback;
+    private List<MediaSession.QueueItem> mItems = new ArrayList<>();
+
+    MediaQueueItemsFetcher(CarDrawerActivity activity, MediaPlaybackModel model) {
+        mActivity = activity;
+        mMediaPlaybackModel = model;
+    }
+
+    @Override
+    public void start(ItemsUpdatedCallback callback) {
+        mCallback = callback;
+        if (mMediaPlaybackModel != null) {
+            mMediaPlaybackModel.addListener(listener);
+            updateItemsFrom(mMediaPlaybackModel.getQueue());
+        }
+        // Inform client of current items. Invoke async to avoid re-entrancy issues.
+        mHandler.post(mCallback::onItemsUpdated);
+    }
+
+    @Override
+    public int getItemCount() {
+        return mItems.size();
+    }
+
+    @Override
+    public void populateViewHolder(DrawerItemViewHolder holder, int position) {
+        MediaSession.QueueItem item = mItems.get(position);
+        MediaItemsFetcher.populateViewHolderFrom(holder, item.getDescription());
+
+        if (item.getQueueId() == getActiveQueueItemId()) {
+            int primaryColor = mMediaPlaybackModel.getPrimaryColor();
+            Drawable drawable =
+                    mActivity.getResources().getDrawable(R.drawable.ic_music_active);
+            drawable.setColorFilter(primaryColor, PorterDuff.Mode.SRC_IN);
+            holder.getRightIcon().setImageDrawable(drawable);
+        } else {
+            holder.getRightIcon().setImageBitmap(null);
+        }
+    }
+
+    @Override
+    public void onItemClick(int position) {
+        MediaController.TransportControls controls = mMediaPlaybackModel.getTransportControls();
+        if (controls != null) {
+            controls.skipToQueueItem(mItems.get(position).getQueueId());
+        }
+        mActivity.closeDrawer();
+    }
+
+    @Override
+    public void cleanup() {
+        mMediaPlaybackModel.removeListener(listener);
+    }
+
+    private void updateItemsFrom(List<MediaSession.QueueItem> queue) {
+        mItems.clear();
+        // We only show items starting from active-item in the queue.
+        final long activeItemId = getActiveQueueItemId();
+        boolean activeItemFound = false;
+        for (MediaSession.QueueItem item : queue) {
+            if (activeItemId == item.getQueueId()) {
+                activeItemFound = true;
+            }
+            if (activeItemFound) {
+                mItems.add(item);
+            }
+        }
+    }
+
+    private long getActiveQueueItemId() {
+        if (mMediaPlaybackModel != null) {
+            PlaybackState playbackState = mMediaPlaybackModel.getPlaybackState();
+            if (playbackState != null) {
+                return playbackState.getActiveQueueItemId();
+            }
+        }
+        return MediaSession.QueueItem.UNKNOWN_ID;
+    }
+
+    private final MediaPlaybackModel.Listener listener = new MediaPlaybackModel.AbstractListener() {
+        @Override
+        public void onQueueChanged(List<MediaSession.QueueItem> queue) {
+            updateItemsFrom(queue);
+            mCallback.onItemsUpdated();
+        }
+
+        @Override
+        public void onPlaybackStateChanged(@Nullable PlaybackState state) {
+            // Since active playing item may have changed, force re-draw of queue items.
+            mCallback.onItemsUpdated();
+        }
+
+        @Override
+        public void onSessionDestroyed(CharSequence destroyedMediaClientName) {
+            onQueueChanged(Collections.emptyList());
+        }
+    };
+}