1/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package com.android.car.media.drawer;
17
18import android.content.ComponentName;
19import android.content.Context;
20import android.media.browse.MediaBrowser;
21import android.media.session.MediaController;
22import android.media.session.MediaSession;
23import android.os.Bundle;
24import android.support.annotation.Nullable;
25import android.support.v4.widget.DrawerLayout;
26import android.util.Log;
27import android.view.View;
28
29import com.android.car.media.MediaManager;
30import com.android.car.media.MediaPlaybackModel;
31import com.android.car.media.R;
32
33import androidx.car.drawer.CarDrawerAdapter;
34import androidx.car.drawer.CarDrawerController;
35
36/**
37 * Manages drawer navigation and item selection.
38 * <p>
39 * Maintains separate MediaPlaybackModel for media browsing and control. Sets up root Drawer
40 * adapter with root of media-browse tree (using MediaBrowserItemsFetcher). Supports switching the
41 * rootAdapter to show the queue-items (using MediaQueueItemsFetcher).
42 */
43public class MediaDrawerController implements MediaDrawerAdapter.MediaFetchCallback,
44        MediaItemOnClickListener {
45    private static final String TAG = "MediaDrawerController";
46
47    private static final String EXTRA_ICON_SIZE =
48            "com.google.android.gms.car.media.BrowserIconSize";
49
50    private final Context mContext;
51    private final CarDrawerController mDrawerController;
52    private final MediaPlaybackModel mMediaPlaybackModel;
53    private MediaDrawerAdapter mRootAdapter;
54
55    public MediaDrawerController(Context context, CarDrawerController drawerController) {
56        mContext = context;
57        mDrawerController = drawerController;
58
59        Bundle extras = new Bundle();
60        extras.putInt(EXTRA_ICON_SIZE,
61                mContext.getResources().getDimensionPixelSize(R.dimen.car_primary_icon_size));
62
63        mMediaPlaybackModel = new MediaPlaybackModel(mContext, extras);
64        mMediaPlaybackModel.addListener(mModelListener);
65
66        mRootAdapter = new MediaDrawerAdapter(mContext, mDrawerController);
67        // Start with a empty title since we depend on the mMediaManagerListener callback to
68        // know which app is being used and set the actual title there.
69        mRootAdapter.setTitle("");
70        mRootAdapter.setFetchCallback(this);
71
72        // Kick off MediaBrowser/MediaController connection.
73        mMediaPlaybackModel.start();
74    }
75
76    @Override
77    public void onQueueItemClicked(MediaSession.QueueItem queueItem) {
78        MediaController.TransportControls controls = mMediaPlaybackModel.getTransportControls();
79
80        if (controls != null) {
81            controls.skipToQueueItem(queueItem.getQueueId());
82        }
83
84        mDrawerController.closeDrawer();
85    }
86
87    @Override
88    public void onMediaItemClicked(MediaBrowser.MediaItem item) {
89        if (item.isBrowsable()) {
90            MediaItemsFetcher fetcher;
91            if (MediaBrowserItemsFetcher.PLAY_QUEUE_MEDIA_ID.equals(item.getMediaId())) {
92                fetcher = createMediaQueueItemsFetcher();
93            } else {
94                fetcher = createMediaBrowserItemFetcher(item.getMediaId(),
95                        false /* showQueueItem */);
96            }
97            setupAdapterAndSwitch(fetcher, item.getDescription().getTitle());
98        } else if (item.isPlayable()) {
99            MediaController.TransportControls controls = mMediaPlaybackModel.getTransportControls();
100            if (controls != null) {
101                controls.playFromMediaId(item.getMediaId(), item.getDescription().getExtras());
102            }
103            mDrawerController.closeDrawer();
104        } else {
105            Log.w(TAG, "Unknown item type; don't know how to handle!");
106        }
107    }
108
109    @Override
110    public void onFetchStart() {
111        // Initially there will be no items and we don't want to show empty-list indicator
112        // briefly until items are fetched.
113        mDrawerController.showLoadingProgressBar(true);
114    }
115
116    @Override
117    public void onFetchEnd() {
118        mDrawerController.showLoadingProgressBar(false);
119    }
120
121    /**
122     * Creates a new sub-level in the drawer and switches to that as the currently displayed view.
123     *
124     * @param fetcher The {@link MediaItemsFetcher} that is responsible for fetching the items to be
125     *                displayed in the new view.
126     * @param title The title text of the new view in the drawer.
127     */
128    private void setupAdapterAndSwitch(MediaItemsFetcher fetcher, CharSequence title) {
129        MediaDrawerAdapter subAdapter = new MediaDrawerAdapter(mContext, mDrawerController);
130        subAdapter.setFetcher(fetcher);
131        subAdapter.setTitle(title);
132        subAdapter.setFetchCallback(this);
133        mDrawerController.pushAdapter(subAdapter);
134    }
135
136    /**
137     * Opens the drawer and displays the current playing queue of items. When the drawer is closed,
138     * the view is switched back to the drawer root.
139     */
140    public void showPlayQueue() {
141        mRootAdapter.setFetcherAndInvoke(createMediaQueueItemsFetcher());
142        mRootAdapter.setTitle(mMediaPlaybackModel.getQueueTitle());
143        mDrawerController.openDrawer();
144        mRootAdapter.scrollToCurrent();
145        mDrawerController.addDrawerListener(mQueueDrawerListener);
146    }
147
148    public void cleanup() {
149        mDrawerController.removeDrawerListener(mQueueDrawerListener);
150        mRootAdapter.cleanup();
151        mMediaPlaybackModel.removeListener(mModelListener);
152        mMediaPlaybackModel.stop();
153    }
154
155    /**
156     * @return Adapter to display root items of MediaBrowse tree. {@link #showPlayQueue()} can
157     *      be used to display items from the queue.
158     */
159    public CarDrawerAdapter getRootAdapter() {
160        return mRootAdapter;
161    }
162
163    /**
164     * Creates a {@link MediaBrowserItemsFetcher} that whose root is the given {@code mediaId}.
165     */
166    private MediaBrowserItemsFetcher createMediaBrowserItemFetcher(String mediaId,
167            boolean showQueueItem) {
168        return new MediaBrowserItemsFetcher(mContext, mMediaPlaybackModel, this /* listener */,
169                mediaId, showQueueItem);
170    }
171
172    /**
173     * Creates a {@link MediaQueueItemsFetcher} that is responsible for fetching items in the user's
174     * current play queue.
175     */
176    private MediaQueueItemsFetcher createMediaQueueItemsFetcher() {
177        return new MediaQueueItemsFetcher(mContext, mMediaPlaybackModel, this /* listener */);
178    }
179
180    /**
181     * Creates a {@link MediaItemsFetcher} that will display the top-most level of the drawer.
182     */
183    private MediaItemsFetcher createRootMediaItemsFetcher() {
184        return createMediaBrowserItemFetcher(mMediaPlaybackModel.getMediaBrowser().getRoot(),
185                true /* showQueueItem */);
186    }
187
188    /**
189     * A {@link android.support.v4.widget.DrawerLayout.DrawerListener} specifically to be used when
190     * the play queue has been shown in the drawer. When the drawer is closed following this
191     * display, this listener will reset the drawer to display the root view.
192     */
193    private final DrawerLayout.DrawerListener mQueueDrawerListener =
194            new DrawerLayout.DrawerListener() {
195        @Override
196        public void onDrawerClosed(View drawerView) {
197            mRootAdapter.setFetcherAndInvoke(createRootMediaItemsFetcher());
198            mRootAdapter.setTitle(
199                    MediaManager.getInstance(mContext).getMediaClientName());
200            mDrawerController.removeDrawerListener(this);
201        }
202
203        @Override
204        public void onDrawerSlide(View drawerView, float slideOffset) {}
205        @Override
206        public void onDrawerOpened(View drawerView) {}
207        @Override
208        public void onDrawerStateChanged(int newState) {}
209    };
210
211    private final MediaPlaybackModel.Listener mModelListener =
212            new MediaPlaybackModel.AbstractListener() {
213        @Override
214        public void onMediaAppChanged(@Nullable ComponentName currentName,
215                @Nullable ComponentName newName) {
216            // Only store MediaManager instance to a local variable when it is short lived.
217            MediaManager mediaManager = MediaManager.getInstance(mContext);
218            mRootAdapter.cleanup();
219            mRootAdapter.setTitle(mediaManager.getMediaClientName());
220        }
221
222        @Override
223        public void onMediaConnected() {
224            mRootAdapter.setFetcherAndInvoke(createRootMediaItemsFetcher());
225        }
226    };
227}
228