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