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