Home | History | Annotate | Download | only in media
      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           */
     72         void onMediaAppChanged(@Nullable ComponentName currentName,
     73                                @Nullable ComponentName newName);
     74         void onMediaAppStatusMessageChanged(@Nullable String message);
     75 
     76         /**
     77          * Indicates the mediaBrowser is not connected and mediaController is available.
     78          */
     79         void onMediaConnected();
     80         /**
     81          * Indicates mediaBrowser connection is temporarily suspended.
     82          * */
     83         void onMediaConnectionSuspended();
     84         /**
     85          * Indicates that the MediaBrowser connected failed. The mediaBrowser and controller have
     86          * now been released.
     87          */
     88         void onMediaConnectionFailed(CharSequence failedMediaClientName);
     89         void onPlaybackStateChanged(@Nullable PlaybackState state);
     90         void onMetadataChanged(@Nullable MediaMetadata metadata);
     91         void onQueueChanged(List<MediaSession.QueueItem> queue);
     92         /**
     93          * Indicates that the MediaSession was destroyed. The mediaController has been released.
     94          */
     95         void onSessionDestroyed(CharSequence destroyedMediaClientName);
     96     }
     97 
     98     /** Convenient Listener base class for extension */
     99     public static abstract class AbstractListener implements Listener {
    100         @Override
    101         public void onMediaAppChanged(@Nullable ComponentName currentName,
    102                 @Nullable ComponentName newName) {}
    103         @Override
    104         public void onMediaAppStatusMessageChanged(@Nullable String message) {}
    105         @Override
    106         public void onMediaConnected() {}
    107         @Override
    108         public void onMediaConnectionSuspended() {}
    109         @Override
    110         public void onMediaConnectionFailed(CharSequence failedMediaClientName) {}
    111         @Override
    112         public void onPlaybackStateChanged(@Nullable PlaybackState state) {}
    113         @Override
    114         public void onMetadataChanged(@Nullable MediaMetadata metadata) {}
    115         @Override
    116         public void onQueueChanged(List<MediaSession.QueueItem> queue) {}
    117         @Override
    118         public void onSessionDestroyed(CharSequence destroyedMediaClientName) {}
    119     }
    120 
    121     public MediaPlaybackModel(Context context, Bundle browserExtras) {
    122         mContext = context;
    123         mBrowserExtras = browserExtras;
    124         mHandler = new Handler(Looper.getMainLooper());
    125     }
    126 
    127     @MainThread
    128     public void start() {
    129         Assert.isMainThread();
    130         MediaManager.getInstance(mContext).addListener(mMediaManagerListener);
    131     }
    132 
    133     @MainThread
    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
    153     public void addListener(MediaPlaybackModel.Listener listener) {
    154         Assert.isMainThread();
    155         mListeners.add(listener);
    156     }
    157 
    158     @MainThread
    159     public void removeListener(MediaPlaybackModel.Listener listener) {
    160         Assert.isMainThread();
    161         mListeners.remove(listener);
    162     }
    163 
    164     @MainThread
    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
    175     public Resources getPackageResources() {
    176         Assert.isMainThread();
    177         return mPackageResources;
    178     }
    179 
    180     @MainThread
    181     public int getPrimaryColor() {
    182         Assert.isMainThread();
    183         return mPrimaryColor;
    184     }
    185 
    186     @MainThread
    187     public int getAccentColor() {
    188         Assert.isMainThread();
    189         return mAccentColor;
    190     }
    191 
    192     @MainThread
    193     public int getPrimaryColorDark() {
    194         Assert.isMainThread();
    195         return mPrimaryColorDark;
    196     }
    197 
    198     @MainThread
    199     public MediaMetadata getMetadata() {
    200         Assert.isMainThread();
    201         if (mController == null) {
    202             return null;
    203         }
    204         return mController.getMetadata();
    205     }
    206 
    207     @MainThread
    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
    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
    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
    251     public boolean isConnected() {
    252         Assert.isMainThread();
    253         return mController != null;
    254     }
    255 
    256     @MainThread
    257     public MediaBrowser getMediaBrowser() {
    258         Assert.isMainThread();
    259         return mBrowser;
    260     }
    261 
    262     @MainThread
    263     public MediaController.TransportControls getTransportControls() {
    264         Assert.isMainThread();
    265         if (mController == null) {
    266             return null;
    267         }
    268         return mController.getTransportControls();
    269     }
    270 
    271     @MainThread
    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