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