1 /*
2  * Copyright (C) 2016 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  */
16 package com.android.car.media;
17 
18 import android.content.ComponentName;
19 import android.content.Context;
20 import android.content.pm.PackageManager;
21 import android.content.res.Resources;
22 import android.media.MediaMetadata;
23 import android.media.browse.MediaBrowser;
24 import android.media.session.MediaController;
25 import android.media.session.MediaSession;
26 import android.media.session.PlaybackState;
27 import android.os.Bundle;
28 import android.os.Handler;
29 import android.os.Looper;
30 import android.support.annotation.MainThread;
31 import android.support.annotation.NonNull;
32 import android.support.annotation.Nullable;
33 import android.util.Log;
34 
35 import com.android.car.apps.common.util.Assert;
36 
37 import java.util.ArrayList;
38 import java.util.LinkedList;
39 import java.util.List;
40 import java.util.function.Consumer;
41 
42 /**
43  * A model for controlling media playback. This model will take care of all Media Manager, Browser,
44  * and controller connection and callbacks. On each stage of the connection, error, or disconnect
45  * this model will call back to the presenter. All call backs to the presenter will be done on the
46  * main thread. Intended to provide a much more usable model interface to UI code.
47  */
48 public class MediaPlaybackModel {
49     private static final String TAG = "MediaPlaybackModel";
50 
51     private final Context mContext;
52     private final Bundle mBrowserExtras;
53     private final List<MediaPlaybackModel.Listener> mListeners = new LinkedList<>();
54 
55     private Handler mHandler;
56     private MediaController mController;
57     private MediaBrowser mBrowser;
58     private int mPrimaryColor;
59     private int mPrimaryColorDark;
60     private int mAccentColor;
61     private ComponentName mCurrentComponentName;
62     private Resources mPackageResources;
63 
64     /**
65      * This is the interface to listen to {@link MediaPlaybackModel} callbacks. All callbacks are
66      * done in the main thread.
67      */
68     public interface Listener {
69         /** Indicates active media app has changed. A new mediaBrowser is now connecting to the new
70           * app and mediaController has been released, pending connection to new service.
71           */
onMediaAppChanged(@ullable ComponentName currentName, @Nullable ComponentName newName)72         void onMediaAppChanged(@Nullable ComponentName currentName,
73                                @Nullable ComponentName newName);
onMediaAppStatusMessageChanged(@ullable String message)74         void onMediaAppStatusMessageChanged(@Nullable String message);
75 
76         /**
77          * Indicates the mediaBrowser is not connected and mediaController is available.
78          */
onMediaConnected()79         void onMediaConnected();
80         /**
81          * Indicates mediaBrowser connection is temporarily suspended.
82          * */
onMediaConnectionSuspended()83         void onMediaConnectionSuspended();
84         /**
85          * Indicates that the MediaBrowser connected failed. The mediaBrowser and controller have
86          * now been released.
87          */
onMediaConnectionFailed(CharSequence failedMediaClientName)88         void onMediaConnectionFailed(CharSequence failedMediaClientName);
onPlaybackStateChanged(@ullable PlaybackState state)89         void onPlaybackStateChanged(@Nullable PlaybackState state);
onMetadataChanged(@ullable MediaMetadata metadata)90         void onMetadataChanged(@Nullable MediaMetadata metadata);
onQueueChanged(List<MediaSession.QueueItem> queue)91         void onQueueChanged(List<MediaSession.QueueItem> queue);
92         /**
93          * Indicates that the MediaSession was destroyed. The mediaController has been released.
94          */
onSessionDestroyed(CharSequence destroyedMediaClientName)95         void onSessionDestroyed(CharSequence destroyedMediaClientName);
96     }
97 
98     /** Convenient Listener base class for extension */
99     public static abstract class AbstractListener implements Listener {
100         @Override
onMediaAppChanged(@ullable ComponentName currentName, @Nullable ComponentName newName)101         public void onMediaAppChanged(@Nullable ComponentName currentName,
102                 @Nullable ComponentName newName) {}
103         @Override
onMediaAppStatusMessageChanged(@ullable String message)104         public void onMediaAppStatusMessageChanged(@Nullable String message) {}
105         @Override
onMediaConnected()106         public void onMediaConnected() {}
107         @Override
onMediaConnectionSuspended()108         public void onMediaConnectionSuspended() {}
109         @Override
onMediaConnectionFailed(CharSequence failedMediaClientName)110         public void onMediaConnectionFailed(CharSequence failedMediaClientName) {}
111         @Override
onPlaybackStateChanged(@ullable PlaybackState state)112         public void onPlaybackStateChanged(@Nullable PlaybackState state) {}
113         @Override
onMetadataChanged(@ullable MediaMetadata metadata)114         public void onMetadataChanged(@Nullable MediaMetadata metadata) {}
115         @Override
onQueueChanged(List<MediaSession.QueueItem> queue)116         public void onQueueChanged(List<MediaSession.QueueItem> queue) {}
117         @Override
onSessionDestroyed(CharSequence destroyedMediaClientName)118         public void onSessionDestroyed(CharSequence destroyedMediaClientName) {}
119     }
120 
MediaPlaybackModel(Context context, Bundle browserExtras)121     public MediaPlaybackModel(Context context, Bundle browserExtras) {
122         mContext = context;
123         mBrowserExtras = browserExtras;
124         mHandler = new Handler(Looper.getMainLooper());
125     }
126 
127     @MainThread
start()128     public void start() {
129         Assert.isMainThread();
130         MediaManager.getInstance(mContext).addListener(mMediaManagerListener);
131     }
132 
133     @MainThread
stop()134     public void stop() {
135         Assert.isMainThread();
136         MediaManager.getInstance(mContext).removeListener(mMediaManagerListener);
137         if (mBrowser != null) {
138             mBrowser.disconnect();
139             mBrowser = null;
140         }
141         if (mController != null) {
142             mController.unregisterCallback(mMediaControllerCallback);
143             mController = null;
144         }
145         // Calling this with null will clear queue of callbacks and message. This needs to be done
146         // here because prior to the above lines to disconnect and unregister the browser and
147         // controller a posted runnable to do work maybe have happened and thus we need to clear it
148         // out to prevent race conditions.
149         mHandler.removeCallbacksAndMessages(null);
150     }
151 
152     @MainThread
addListener(MediaPlaybackModel.Listener listener)153     public void addListener(MediaPlaybackModel.Listener listener) {
154         Assert.isMainThread();
155         mListeners.add(listener);
156     }
157 
158     @MainThread
removeListener(MediaPlaybackModel.Listener listener)159     public void removeListener(MediaPlaybackModel.Listener listener) {
160         Assert.isMainThread();
161         mListeners.remove(listener);
162     }
163 
164     @MainThread
notifyListeners(Consumer<Listener> callback)165     private void notifyListeners(Consumer<Listener> callback) {
166         Assert.isMainThread();
167         // Clone mListeners in case any of the callbacks made triggers a listener to be added or
168         // removed to/from mListeners.
169         List<Listener> listenersCopy = new LinkedList<>(mListeners);
170         // Invokes callback.accept(listener) for each listener.
171         listenersCopy.forEach(callback);
172     }
173 
174     @MainThread
getPackageResources()175     public Resources getPackageResources() {
176         Assert.isMainThread();
177         return mPackageResources;
178     }
179 
180     @MainThread
getPrimaryColor()181     public int getPrimaryColor() {
182         Assert.isMainThread();
183         return mPrimaryColor;
184     }
185 
186     @MainThread
getAccentColor()187     public int getAccentColor() {
188         Assert.isMainThread();
189         return mAccentColor;
190     }
191 
192     @MainThread
getPrimaryColorDark()193     public int getPrimaryColorDark() {
194         Assert.isMainThread();
195         return mPrimaryColorDark;
196     }
197 
198     @MainThread
getMetadata()199     public MediaMetadata getMetadata() {
200         Assert.isMainThread();
201         if (mController == null) {
202             return null;
203         }
204         return mController.getMetadata();
205     }
206 
207     @MainThread
getQueue()208     public @NonNull List<MediaSession.QueueItem> getQueue() {
209         Assert.isMainThread();
210         if (mController == null) {
211             return new ArrayList<>();
212         }
213         List<MediaSession.QueueItem> currentQueue = mController.getQueue();
214         if (currentQueue == null) {
215             currentQueue = new ArrayList<>();
216         }
217         return currentQueue;
218     }
219 
220     @MainThread
getPlaybackState()221     public PlaybackState getPlaybackState() {
222         Assert.isMainThread();
223         if (mController == null) {
224             return null;
225         }
226         return mController.getPlaybackState();
227     }
228 
229     /**
230      * Return true if the slot of the action should be always reserved for it,
231      * even when the corresponding playbackstate action is disabled. This avoids
232      * an undesired reflow on the playback drawer when a temporary state
233      * disables some action. This information can be set on the MediaSession
234      * extras as a boolean for each default action that needs its slot
235      * reserved. Currently supported actions are ACTION_SKIP_TO_PREVIOUS,
236      * ACTION_SKIP_TO_NEXT and ACTION_SHOW_QUEUE.
237      */
238     @MainThread
isSlotForActionReserved(String actionExtraKey)239     public boolean isSlotForActionReserved(String actionExtraKey) {
240         Assert.isMainThread();
241         if (mController != null) {
242             Bundle extras = mController.getExtras();
243             if (extras != null) {
244                 return extras.getBoolean(actionExtraKey, false);
245             }
246         }
247         return false;
248     }
249 
250     @MainThread
isConnected()251     public boolean isConnected() {
252         Assert.isMainThread();
253         return mController != null;
254     }
255 
256     @MainThread
getMediaBrowser()257     public MediaBrowser getMediaBrowser() {
258         Assert.isMainThread();
259         return mBrowser;
260     }
261 
262     @MainThread
getTransportControls()263     public MediaController.TransportControls getTransportControls() {
264         Assert.isMainThread();
265         if (mController == null) {
266             return null;
267         }
268         return mController.getTransportControls();
269     }
270 
271     @MainThread
getQueueTitle()272     public @NonNull CharSequence getQueueTitle() {
273         Assert.isMainThread();
274         if (mController == null) {
275             return "";
276         }
277         return mController.getQueueTitle();
278     }
279 
280     private final MediaManager.Listener mMediaManagerListener = new MediaManager.Listener() {
281         @Override
282         public void onMediaAppChanged(final ComponentName name) {
283             mHandler.post(() -> {
284                 if (mBrowser != null) {
285                     mBrowser.disconnect();
286                 }
287                 mBrowser = new MediaBrowser(mContext, name, mConnectionCallback, mBrowserExtras);
288                 try {
289                     mPackageResources = mContext.getPackageManager().getResourcesForApplication(
290                             name.getPackageName());
291                 } catch (PackageManager.NameNotFoundException e) {
292                     Log.e(TAG, "Unable to get resources for " + name.getPackageName());
293                 }
294 
295                 if (mController != null) {
296                     mController.unregisterCallback(mMediaControllerCallback);
297                     mController = null;
298                 }
299                 mBrowser.connect();
300 
301                 // reset the colors and views if we switch to another app.
302                 MediaManager manager = MediaManager.getInstance(mContext);
303                 mPrimaryColor = manager.getMediaClientPrimaryColor();
304                 mAccentColor = manager.getMediaClientAccentColor();
305                 mPrimaryColorDark = manager.getMediaClientPrimaryColorDark();
306 
307                 final ComponentName currentName = mCurrentComponentName;
308                 notifyListeners((listener) -> listener.onMediaAppChanged(currentName, name));
309                 mCurrentComponentName = name;
310             });
311         }
312 
313         @Override
314         public void onStatusMessageChanged(final String message) {
315             mHandler.post(() -> {
316                 notifyListeners((listener) -> listener.onMediaAppStatusMessageChanged(message));
317             });
318         }
319     };
320 
321     private final MediaBrowser.ConnectionCallback mConnectionCallback =
322             new MediaBrowser.ConnectionCallback() {
323                 @Override
324                 public void onConnected() {
325                     mHandler.post(()->{
326                         // Existing mController has already been disconnected before we call
327                         // MediaBrowser.connect()
328                         // getSessionToken returns a non null token
329                         MediaSession.Token token = mBrowser.getSessionToken();
330                         if (mController != null) {
331                             mController.unregisterCallback(mMediaControllerCallback);
332                         }
333                         mController = new MediaController(mContext, token);
334                         mController.registerCallback(mMediaControllerCallback);
335                         notifyListeners(Listener::onMediaConnected);
336                     });
337                 }
338 
339                 @Override
340                 public void onConnectionSuspended() {
341                     mHandler.post(() -> {
342                         if (Log.isLoggable(TAG, Log.VERBOSE)) {
343                             Log.v(TAG, "Media browser service connection suspended."
344                                     + " Waiting to be reconnected....");
345                         }
346                         notifyListeners(Listener::onMediaConnectionSuspended);
347                     });
348                 }
349 
350                 @Override
351                 public void onConnectionFailed() {
352                     mHandler.post(() -> {
353                         Log.e(TAG, "Media browser service connection FAILED!");
354                         // disconnect anyway to make sure we get into a sanity state
355                         mBrowser.disconnect();
356                         mBrowser = null;
357                         mCurrentComponentName = null;
358 
359                         CharSequence failedClientName = MediaManager.getInstance(mContext)
360                                 .getMediaClientName();
361                         notifyListeners(
362                                 (listener) -> listener.onMediaConnectionFailed(failedClientName));
363                     });
364                 }
365             };
366 
367     private final MediaController.Callback mMediaControllerCallback =
368             new MediaController.Callback() {
369                 @Override
370                 public void onPlaybackStateChanged(final PlaybackState state) {
371                     mHandler.post(() -> {
372                         notifyListeners((listener) -> listener.onPlaybackStateChanged(state));
373                     });
374                 }
375 
376                 @Override
377                 public void onMetadataChanged(final MediaMetadata metadata) {
378                     mHandler.post(() -> {
379                         notifyListeners((listener) -> listener.onMetadataChanged(metadata));
380                     });
381                 }
382 
383                 @Override
384                 public void onQueueChanged(final List<MediaSession.QueueItem> queue) {
385                     mHandler.post(() -> {
386                         final List<MediaSession.QueueItem> currentQueue =
387                                 queue != null ? queue : new ArrayList<>();
388                         notifyListeners((listener) -> listener.onQueueChanged(currentQueue));
389                     });
390                 }
391 
392                 @Override
393                 public void onSessionDestroyed() {
394                     mHandler.post(() -> {
395                         if (Log.isLoggable(TAG, Log.VERBOSE)) {
396                             Log.v(TAG, "onSessionDestroyed()");
397                         }
398                         mCurrentComponentName = null;
399                         if (mController != null) {
400                             mController.unregisterCallback(mMediaControllerCallback);
401                             mController = null;
402                         }
403 
404                         CharSequence destroyedClientName = MediaManager.getInstance(
405                                 mContext).getMediaClientName();
406                         notifyListeners(
407                                 (listener) -> listener.onSessionDestroyed(destroyedClientName));
408                     });
409                 }
410             };
411 }
412