1 /* 2 * Copyright (C) 2019 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; 17 18 import static android.car.media.CarMediaManager.MEDIA_SOURCE_MODE_BROWSE; 19 import static android.car.media.CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK; 20 21 import android.annotation.TestApi; 22 import android.app.ActivityManager; 23 import android.car.Car; 24 import android.car.media.CarMediaManager; 25 import android.car.media.CarMediaManager.MediaSourceChangedListener; 26 import android.car.media.CarMediaManager.MediaSourceMode; 27 import android.car.media.ICarMedia; 28 import android.car.media.ICarMediaSourceListener; 29 import android.car.user.CarUserManager; 30 import android.car.user.CarUserManager.UserLifecycleListener; 31 import android.content.BroadcastReceiver; 32 import android.content.ComponentName; 33 import android.content.Context; 34 import android.content.Intent; 35 import android.content.IntentFilter; 36 import android.content.SharedPreferences; 37 import android.content.pm.PackageManager; 38 import android.content.pm.ResolveInfo; 39 import android.media.session.MediaController; 40 import android.media.session.MediaController.TransportControls; 41 import android.media.session.MediaSession; 42 import android.media.session.MediaSession.Token; 43 import android.media.session.MediaSessionManager; 44 import android.media.session.MediaSessionManager.OnActiveSessionsChangedListener; 45 import android.media.session.PlaybackState; 46 import android.os.Bundle; 47 import android.os.Handler; 48 import android.os.HandlerThread; 49 import android.os.Looper; 50 import android.os.RemoteCallbackList; 51 import android.os.RemoteException; 52 import android.os.UserHandle; 53 import android.os.UserManager; 54 import android.service.media.MediaBrowserService; 55 import android.text.TextUtils; 56 import android.util.Log; 57 58 import androidx.annotation.NonNull; 59 import androidx.annotation.Nullable; 60 61 import com.android.car.user.CarUserService; 62 import com.android.internal.annotations.GuardedBy; 63 import com.android.internal.annotations.VisibleForTesting; 64 65 import java.io.PrintWriter; 66 import java.util.ArrayDeque; 67 import java.util.ArrayList; 68 import java.util.Arrays; 69 import java.util.Deque; 70 import java.util.HashMap; 71 import java.util.List; 72 import java.util.Map; 73 import java.util.stream.Collectors; 74 75 /** 76 * CarMediaService manages the currently active media source for car apps. This is different from 77 * the MediaSessionManager's active sessions, as there can only be one active source in the car, 78 * through both browse and playback. 79 * 80 * In the car, the active media source does not necessarily have an active MediaSession, e.g. if 81 * it were being browsed only. However, that source is still considered the active source, and 82 * should be the source displayed in any Media related UIs (Media Center, home screen, etc). 83 */ 84 public class CarMediaService extends ICarMedia.Stub implements CarServiceBase { 85 86 private static final String SOURCE_KEY = "media_source_component"; 87 private static final String SOURCE_KEY_SEPARATOR = "_"; 88 private static final String PLAYBACK_STATE_KEY = "playback_state"; 89 private static final String SHARED_PREF = "com.android.car.media.car_media_service"; 90 private static final String COMPONENT_NAME_SEPARATOR = ","; 91 private static final String MEDIA_CONNECTION_ACTION = "com.android.car.media.MEDIA_CONNECTION"; 92 private static final String EXTRA_AUTOPLAY = "com.android.car.media.autoplay"; 93 94 private static final int MEDIA_SOURCE_MODES = 2; 95 96 // XML configuration options for autoplay on media source change. 97 private static final int AUTOPLAY_CONFIG_NEVER = 0; 98 private static final int AUTOPLAY_CONFIG_ALWAYS = 1; 99 // This mode uses the current source's last stored playback state to resume playback 100 private static final int AUTOPLAY_CONFIG_RETAIN_PER_SOURCE = 2; 101 // This mode uses the previous source's playback state to resume playback 102 private static final int AUTOPLAY_CONFIG_RETAIN_PREVIOUS = 3; 103 104 private final Context mContext; 105 private final CarUserService mUserService; 106 private final UserManager mUserManager; 107 private final MediaSessionManager mMediaSessionManager; 108 private final MediaSessionUpdater mMediaSessionUpdater = new MediaSessionUpdater(); 109 @GuardedBy("mLock") 110 private ComponentName[] mPrimaryMediaComponents = new ComponentName[MEDIA_SOURCE_MODES]; 111 private SharedPreferences mSharedPrefs; 112 // MediaController for the current active user's active media session. This controller can be 113 // null if playback has not been started yet. 114 private MediaController mActiveUserMediaController; 115 private SessionChangedListener mSessionsListener; 116 private int mPlayOnMediaSourceChangedConfig; 117 private int mPlayOnBootConfig; 118 private boolean mIndependentPlaybackConfig; 119 private int mCurrentPlaybackState; 120 121 private boolean mPendingInit; 122 123 @GuardedBy("mLock") 124 private final RemoteCallbackList<ICarMediaSourceListener>[] mMediaSourceListeners = 125 new RemoteCallbackList[MEDIA_SOURCE_MODES]; 126 127 private final Handler mMainHandler = new Handler(Looper.getMainLooper()); 128 129 130 private final HandlerThread mHandlerThread = CarServiceUtils.getHandlerThread( 131 getClass().getSimpleName()); 132 // Handler to receive PlaybackState callbacks from the active media controller. 133 private final Handler mHandler = new Handler(mHandlerThread.getLooper()); 134 private final Object mLock = new Object(); 135 136 /** The component name of the last media source that was removed while being primary. */ 137 private ComponentName[] mRemovedMediaSourceComponents = new ComponentName[MEDIA_SOURCE_MODES]; 138 139 private final IntentFilter mPackageUpdateFilter; 140 private boolean mIsPackageUpdateReceiverRegistered; 141 142 /** 143 * Listens to {@link Intent#ACTION_PACKAGE_REMOVED}, so we can fall back to a previously used 144 * media source when the active source is uninstalled. 145 */ 146 private final BroadcastReceiver mPackageUpdateReceiver = new BroadcastReceiver() { 147 @Override 148 public void onReceive(Context context, Intent intent) { 149 if (intent.getData() == null) { 150 return; 151 } 152 String intentPackage = intent.getData().getSchemeSpecificPart(); 153 if (Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) { 154 synchronized (mLock) { 155 for (int i = 0; i < MEDIA_SOURCE_MODES; i++) { 156 if (mPrimaryMediaComponents[i] != null 157 && mPrimaryMediaComponents[i].getPackageName().equals( 158 intentPackage)) { 159 if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { 160 // If package is being replaced, it may not be removed from 161 // PackageManager queries when we check for available 162 // MediaBrowseServices, so we iterate to find the next available 163 // source. 164 for (ComponentName component : getLastMediaSources(i)) { 165 if (!mPrimaryMediaComponents[i].getPackageName() 166 .equals(component.getPackageName())) { 167 mRemovedMediaSourceComponents[i] = 168 mPrimaryMediaComponents[i]; 169 if (Log.isLoggable(CarLog.TAG_MEDIA, Log.DEBUG)) { 170 Log.d(CarLog.TAG_MEDIA, 171 "temporarily replacing updated media source " 172 + mPrimaryMediaComponents[i] 173 + "with backup source: " 174 + component); 175 } 176 setPrimaryMediaSource(component, i); 177 return; 178 } 179 } 180 Log.e(CarLog.TAG_MEDIA, "No available backup media source"); 181 } else { 182 if (Log.isLoggable(CarLog.TAG_MEDIA, Log.DEBUG)) { 183 Log.d(CarLog.TAG_MEDIA, "replacing removed media source " 184 + mPrimaryMediaComponents[i] + "with backup source: " 185 + getLastMediaSource(i)); 186 } 187 mRemovedMediaSourceComponents[i] = null; 188 setPrimaryMediaSource(getLastMediaSource(i), i); 189 } 190 } 191 } 192 } 193 } else if (Intent.ACTION_PACKAGE_REPLACED.equals(intent.getAction()) 194 || Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction())) { 195 for (int i = 0; i < MEDIA_SOURCE_MODES; i++) { 196 if (mRemovedMediaSourceComponents[i] != null && mRemovedMediaSourceComponents[i] 197 .getPackageName().equals(intentPackage)) { 198 if (Log.isLoggable(CarLog.TAG_MEDIA, Log.DEBUG)) { 199 Log.d(CarLog.TAG_MEDIA, "restoring removed source: " 200 + mRemovedMediaSourceComponents[i]); 201 } 202 setPrimaryMediaSource(mRemovedMediaSourceComponents[i], i); 203 } 204 } 205 } 206 } 207 }; 208 209 private final UserLifecycleListener mUserLifecycleListener = event -> { 210 if (Log.isLoggable(CarLog.TAG_MEDIA, Log.DEBUG)) { 211 Log.d(CarLog.TAG_MEDIA, "CarMediaService.onEvent(" + event + ")"); 212 } 213 if (CarUserManager.USER_LIFECYCLE_EVENT_TYPE_SWITCHING == event.getEventType()) { 214 maybeInitUser(event.getUserId()); 215 } else if (CarUserManager.USER_LIFECYCLE_EVENT_TYPE_UNLOCKING == event.getEventType()) { 216 onUserUnlock(event.getUserId()); 217 } 218 }; 219 CarMediaService(Context context, CarUserService userService)220 public CarMediaService(Context context, CarUserService userService) { 221 mContext = context; 222 mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); 223 mMediaSessionManager = mContext.getSystemService(MediaSessionManager.class); 224 mMediaSourceListeners[MEDIA_SOURCE_MODE_PLAYBACK] = new RemoteCallbackList(); 225 mMediaSourceListeners[MEDIA_SOURCE_MODE_BROWSE] = new RemoteCallbackList(); 226 mIndependentPlaybackConfig = mContext.getResources().getBoolean( 227 R.bool.config_mediaSourceIndependentPlayback); 228 229 mPackageUpdateFilter = new IntentFilter(); 230 mPackageUpdateFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); 231 mPackageUpdateFilter.addAction(Intent.ACTION_PACKAGE_REPLACED); 232 mPackageUpdateFilter.addAction(Intent.ACTION_PACKAGE_ADDED); 233 mPackageUpdateFilter.addDataScheme("package"); 234 mUserService = userService; 235 mUserService.addUserLifecycleListener(mUserLifecycleListener); 236 237 mPlayOnMediaSourceChangedConfig = 238 mContext.getResources().getInteger(R.integer.config_mediaSourceChangedAutoplay); 239 mPlayOnBootConfig = mContext.getResources().getInteger(R.integer.config_mediaBootAutoplay); 240 } 241 242 @Override 243 // This method is called from ICarImpl after CarMediaService is created. init()244 public void init() { 245 int currentUser = ActivityManager.getCurrentUser(); 246 maybeInitUser(currentUser); 247 } 248 maybeInitUser(int userId)249 private void maybeInitUser(int userId) { 250 if (userId == UserHandle.USER_SYSTEM) { 251 return; 252 } 253 if (mUserManager.isUserUnlocked(userId)) { 254 initUser(userId); 255 } else { 256 mPendingInit = true; 257 } 258 } 259 initUser(int userId)260 private void initUser(int userId) { 261 // SharedPreferences are shared among different users thus only need initialized once. And 262 // they should be initialized after user 0 is unlocked because SharedPreferences in 263 // credential encrypted storage are not available until after user 0 is unlocked. 264 // initUser() is called when the current foreground user is unlocked, and by that time user 265 // 0 has been unlocked already, so initializing SharedPreferences in initUser() is fine. 266 if (mSharedPrefs == null) { 267 mSharedPrefs = mContext.getSharedPreferences(SHARED_PREF, Context.MODE_PRIVATE); 268 } 269 270 if (mIsPackageUpdateReceiverRegistered) { 271 mContext.unregisterReceiver(mPackageUpdateReceiver); 272 } 273 UserHandle currentUser = new UserHandle(userId); 274 mContext.registerReceiverAsUser(mPackageUpdateReceiver, currentUser, 275 mPackageUpdateFilter, null, null); 276 mIsPackageUpdateReceiverRegistered = true; 277 278 mPrimaryMediaComponents[MEDIA_SOURCE_MODE_PLAYBACK] = isCurrentUserEphemeral() 279 ? getDefaultMediaSource() : getLastMediaSource(MEDIA_SOURCE_MODE_PLAYBACK); 280 mPrimaryMediaComponents[MEDIA_SOURCE_MODE_BROWSE] = isCurrentUserEphemeral() 281 ? getDefaultMediaSource() : getLastMediaSource(MEDIA_SOURCE_MODE_BROWSE); 282 mActiveUserMediaController = null; 283 284 updateMediaSessionCallbackForCurrentUser(); 285 notifyListeners(MEDIA_SOURCE_MODE_PLAYBACK); 286 notifyListeners(MEDIA_SOURCE_MODE_BROWSE); 287 288 startMediaConnectorService(shouldStartPlayback(mPlayOnBootConfig), currentUser); 289 } 290 291 /** 292 * Starts a service on the current user that binds to the media browser of the current media 293 * source. We start a new service because this one runs on user 0, and MediaBrowser doesn't 294 * provide an API to connect on a specific user. Additionally, this service will attempt to 295 * resume playback using the MediaSession obtained via the media browser connection, which 296 * is more reliable than using active MediaSessions from MediaSessionManager. 297 */ startMediaConnectorService(boolean startPlayback, UserHandle currentUser)298 private void startMediaConnectorService(boolean startPlayback, UserHandle currentUser) { 299 Intent serviceStart = new Intent(MEDIA_CONNECTION_ACTION); 300 serviceStart.setPackage(mContext.getResources().getString(R.string.serviceMediaConnection)); 301 serviceStart.putExtra(EXTRA_AUTOPLAY, startPlayback); 302 mContext.startForegroundServiceAsUser(serviceStart, currentUser); 303 } 304 sharedPrefsInitialized()305 private boolean sharedPrefsInitialized() { 306 if (mSharedPrefs == null) { 307 // It shouldn't reach this but let's be cautious. 308 Log.e(CarLog.TAG_MEDIA, "SharedPreferences are not initialized!"); 309 String className = getClass().getName(); 310 for (StackTraceElement ste : Thread.currentThread().getStackTrace()) { 311 // Let's print the useful logs only. 312 String log = ste.toString(); 313 if (log.contains(className)) { 314 Log.e(CarLog.TAG_MEDIA, log); 315 } 316 } 317 return false; 318 } 319 return true; 320 } 321 isCurrentUserEphemeral()322 private boolean isCurrentUserEphemeral() { 323 return mUserManager.getUserInfo(ActivityManager.getCurrentUser()).isEphemeral(); 324 } 325 326 @Override release()327 public void release() { 328 mMediaSessionUpdater.unregisterCallbacks(); 329 mUserService.removeUserLifecycleListener(mUserLifecycleListener); 330 } 331 332 @Override dump(PrintWriter writer)333 public void dump(PrintWriter writer) { 334 writer.println("*CarMediaService*"); 335 writer.println("\tCurrent playback media component: " 336 + (mPrimaryMediaComponents[MEDIA_SOURCE_MODE_PLAYBACK] == null ? "-" 337 : mPrimaryMediaComponents[MEDIA_SOURCE_MODE_PLAYBACK].flattenToString())); 338 writer.println("\tCurrent browse media component: " 339 + (mPrimaryMediaComponents[MEDIA_SOURCE_MODE_BROWSE] == null ? "-" 340 : mPrimaryMediaComponents[MEDIA_SOURCE_MODE_BROWSE].flattenToString())); 341 if (mActiveUserMediaController != null) { 342 writer.println( 343 "\tCurrent media controller: " + mActiveUserMediaController.getPackageName()); 344 writer.println( 345 "\tCurrent browse service extra: " + getClassName(mActiveUserMediaController)); 346 } 347 writer.println("\tNumber of active media sessions: " + mMediaSessionManager 348 .getActiveSessionsForUser(null, ActivityManager.getCurrentUser()).size()); 349 350 writer.println("\tPlayback media source history: "); 351 for (ComponentName name : getLastMediaSources(MEDIA_SOURCE_MODE_PLAYBACK)) { 352 writer.println("\t" + name.flattenToString()); 353 } 354 writer.println("\tBrowse media source history: "); 355 for (ComponentName name : getLastMediaSources(MEDIA_SOURCE_MODE_BROWSE)) { 356 writer.println("\t" + name.flattenToString()); 357 } 358 359 } 360 361 /** 362 * @see {@link CarMediaManager#setMediaSource(ComponentName)} 363 */ 364 @Override setMediaSource(@onNull ComponentName componentName, @MediaSourceMode int mode)365 public void setMediaSource(@NonNull ComponentName componentName, 366 @MediaSourceMode int mode) { 367 ICarImpl.assertPermission(mContext, android.Manifest.permission.MEDIA_CONTENT_CONTROL); 368 if (Log.isLoggable(CarLog.TAG_MEDIA, Log.DEBUG)) { 369 Log.d(CarLog.TAG_MEDIA, "Changing media source to: " + componentName.getPackageName()); 370 } 371 setPrimaryMediaSource(componentName, mode); 372 } 373 374 /** 375 * @see {@link CarMediaManager#getMediaSource()} 376 */ 377 @Override getMediaSource(@arMediaManager.MediaSourceMode int mode)378 public ComponentName getMediaSource(@CarMediaManager.MediaSourceMode int mode) { 379 ICarImpl.assertPermission(mContext, android.Manifest.permission.MEDIA_CONTENT_CONTROL); 380 synchronized (mLock) { 381 return mPrimaryMediaComponents[mode]; 382 } 383 } 384 385 /** 386 * @see {@link CarMediaManager#registerMediaSourceListener(MediaSourceChangedListener)} 387 */ 388 @Override registerMediaSourceListener(ICarMediaSourceListener callback, @MediaSourceMode int mode)389 public void registerMediaSourceListener(ICarMediaSourceListener callback, 390 @MediaSourceMode int mode) { 391 ICarImpl.assertPermission(mContext, android.Manifest.permission.MEDIA_CONTENT_CONTROL); 392 synchronized (mLock) { 393 mMediaSourceListeners[mode].register(callback); 394 } 395 } 396 397 /** 398 * @see {@link CarMediaManager#unregisterMediaSourceListener(ICarMediaSourceListener)} 399 */ 400 @Override unregisterMediaSourceListener(ICarMediaSourceListener callback, @MediaSourceMode int mode)401 public void unregisterMediaSourceListener(ICarMediaSourceListener callback, 402 @MediaSourceMode int mode) { 403 ICarImpl.assertPermission(mContext, android.Manifest.permission.MEDIA_CONTENT_CONTROL); 404 synchronized (mLock) { 405 mMediaSourceListeners[mode].unregister(callback); 406 } 407 } 408 409 @Override getLastMediaSources(@arMediaManager.MediaSourceMode int mode)410 public List<ComponentName> getLastMediaSources(@CarMediaManager.MediaSourceMode int mode) { 411 String key = getMediaSourceKey(mode); 412 String serialized = mSharedPrefs.getString(key, null); 413 return getComponentNameList(serialized).stream() 414 .map(name -> ComponentName.unflattenFromString(name)).collect(Collectors.toList()); 415 } 416 417 /** See {@link CarMediaManager#isIndependentPlaybackConfig}. */ 418 @Override 419 @TestApi isIndependentPlaybackConfig()420 public boolean isIndependentPlaybackConfig() { 421 ICarImpl.assertPermission(mContext, android.Manifest.permission.MEDIA_CONTENT_CONTROL); 422 synchronized (mLock) { 423 return mIndependentPlaybackConfig; 424 } 425 } 426 427 /** See {@link CarMediaManager#setIndependentPlaybackConfig}. */ 428 @Override 429 @TestApi setIndependentPlaybackConfig(boolean independent)430 public void setIndependentPlaybackConfig(boolean independent) { 431 ICarImpl.assertPermission(mContext, android.Manifest.permission.MEDIA_CONTENT_CONTROL); 432 synchronized (mLock) { 433 mIndependentPlaybackConfig = independent; 434 } 435 } 436 437 // TODO(b/153115826): this method was used to be called from the ICar binder thread, but it's 438 // now called by UserCarService. Currently UserCarServie is calling every listener in one 439 // non-main thread, but it's not clear how the final behavior will be. So, for now it's ok 440 // to post it to mMainHandler, but once b/145689885 is fixed, we might not need it. onUserUnlock(int userId)441 private void onUserUnlock(int userId) { 442 mMainHandler.post(() -> { 443 // No need to handle system user, non current foreground user. 444 if (userId == UserHandle.USER_SYSTEM 445 || userId != ActivityManager.getCurrentUser()) { 446 return; 447 } 448 if (mPendingInit) { 449 initUser(userId); 450 mPendingInit = false; 451 if (Log.isLoggable(CarLog.TAG_MEDIA, Log.DEBUG)) { 452 Log.d(CarLog.TAG_MEDIA, 453 "User " + userId + " is now unlocked"); 454 } 455 } 456 }); 457 } 458 updateMediaSessionCallbackForCurrentUser()459 private void updateMediaSessionCallbackForCurrentUser() { 460 if (mSessionsListener != null) { 461 mMediaSessionManager.removeOnActiveSessionsChangedListener(mSessionsListener); 462 } 463 mSessionsListener = new SessionChangedListener(ActivityManager.getCurrentUser()); 464 mMediaSessionManager.addOnActiveSessionsChangedListener(mSessionsListener, null, 465 ActivityManager.getCurrentUser(), null); 466 mMediaSessionUpdater.registerCallbacks(mMediaSessionManager.getActiveSessionsForUser( 467 null, ActivityManager.getCurrentUser())); 468 } 469 470 /** 471 * Attempts to stop the current source using MediaController.TransportControls.stop() 472 * This method also unregisters callbacks to the active media controller before calling stop(), 473 * to preserve the PlaybackState before stopping. 474 */ stopAndUnregisterCallback()475 private void stopAndUnregisterCallback() { 476 if (mActiveUserMediaController != null) { 477 mActiveUserMediaController.unregisterCallback(mMediaControllerCallback); 478 if (Log.isLoggable(CarLog.TAG_MEDIA, Log.DEBUG)) { 479 Log.d(CarLog.TAG_MEDIA, "stopping " + mActiveUserMediaController.getPackageName()); 480 } 481 TransportControls controls = mActiveUserMediaController.getTransportControls(); 482 if (controls != null) { 483 controls.stop(); 484 } else { 485 Log.e(CarLog.TAG_MEDIA, "Can't stop playback, transport controls unavailable " 486 + mActiveUserMediaController.getPackageName()); 487 } 488 } 489 } 490 491 private class SessionChangedListener implements OnActiveSessionsChangedListener { 492 private final int mCurrentUser; 493 SessionChangedListener(int currentUser)494 SessionChangedListener(int currentUser) { 495 mCurrentUser = currentUser; 496 } 497 498 @Override onActiveSessionsChanged(List<MediaController> controllers)499 public void onActiveSessionsChanged(List<MediaController> controllers) { 500 if (ActivityManager.getCurrentUser() != mCurrentUser) { 501 Log.e(CarLog.TAG_MEDIA, "Active session callback for old user: " + mCurrentUser); 502 return; 503 } 504 mMediaSessionUpdater.registerCallbacks(controllers); 505 } 506 } 507 508 private class MediaControllerCallback extends MediaController.Callback { 509 510 private final MediaController mMediaController; 511 private int mPreviousPlaybackState; 512 MediaControllerCallback(MediaController mediaController)513 private MediaControllerCallback(MediaController mediaController) { 514 mMediaController = mediaController; 515 PlaybackState state = mediaController.getPlaybackState(); 516 mPreviousPlaybackState = (state == null) ? PlaybackState.STATE_NONE : state.getState(); 517 } 518 register()519 private void register() { 520 mMediaController.registerCallback(this); 521 } 522 unregister()523 private void unregister() { 524 mMediaController.unregisterCallback(this); 525 } 526 527 @Override onPlaybackStateChanged(@ullable PlaybackState state)528 public void onPlaybackStateChanged(@Nullable PlaybackState state) { 529 if (state.getState() == PlaybackState.STATE_PLAYING 530 && state.getState() != mPreviousPlaybackState) { 531 ComponentName mediaSource = getMediaSource(mMediaController.getPackageName(), 532 getClassName(mMediaController)); 533 if (mediaSource != null 534 && !mediaSource.equals(mPrimaryMediaComponents[MEDIA_SOURCE_MODE_PLAYBACK]) 535 && Log.isLoggable(CarLog.TAG_MEDIA, Log.INFO)) { 536 Log.i(CarLog.TAG_MEDIA, "Changing media source due to playback state change: " 537 + mediaSource.flattenToString()); 538 } 539 setPrimaryMediaSource(mediaSource, MEDIA_SOURCE_MODE_PLAYBACK); 540 } 541 mPreviousPlaybackState = state.getState(); 542 } 543 } 544 545 private class MediaSessionUpdater { 546 private Map<Token, MediaControllerCallback> mCallbacks = new HashMap<>(); 547 548 /** 549 * Register a {@link MediaControllerCallback} for each given controller. Note that if a 550 * controller was already watched, we don't register a callback again. This prevents an 551 * undesired revert of the primary media source. Callbacks for previously watched 552 * controllers that are not present in the given list are unregistered. 553 */ registerCallbacks(List<MediaController> newControllers)554 private void registerCallbacks(List<MediaController> newControllers) { 555 556 List<MediaController> additions = new ArrayList<>(newControllers.size()); 557 Map<MediaSession.Token, MediaControllerCallback> updatedCallbacks = 558 new HashMap<>(newControllers.size()); 559 560 for (MediaController controller : newControllers) { 561 MediaSession.Token token = controller.getSessionToken(); 562 MediaControllerCallback callback = mCallbacks.get(token); 563 if (callback == null) { 564 callback = new MediaControllerCallback(controller); 565 callback.register(); 566 additions.add(controller); 567 } 568 updatedCallbacks.put(token, callback); 569 } 570 571 for (MediaSession.Token token : mCallbacks.keySet()) { 572 if (!updatedCallbacks.containsKey(token)) { 573 mCallbacks.get(token).unregister(); 574 } 575 } 576 577 mCallbacks = updatedCallbacks; 578 updatePrimaryMediaSourceWithCurrentlyPlaying(additions); 579 // If there are no playing media sources, and we don't currently have the controller 580 // for the active source, check the active sessions for a matching controller. If this 581 // is called after a user switch, its possible for a matching controller to already be 582 // active before the user is unlocked, so we check all of the current controllers 583 if (mActiveUserMediaController == null) { 584 updateActiveMediaController(newControllers); 585 } 586 } 587 588 /** 589 * Unregister all MediaController callbacks 590 */ unregisterCallbacks()591 private void unregisterCallbacks() { 592 for (Map.Entry<Token, MediaControllerCallback> entry : mCallbacks.entrySet()) { 593 entry.getValue().unregister(); 594 } 595 } 596 } 597 598 /** 599 * Updates the primary media source, then notifies content observers of the change 600 * Will update both the playback and browse sources if independent playback is not supported 601 */ setPrimaryMediaSource(@onNull ComponentName componentName, @CarMediaManager.MediaSourceMode int mode)602 private void setPrimaryMediaSource(@NonNull ComponentName componentName, 603 @CarMediaManager.MediaSourceMode int mode) { 604 synchronized (mLock) { 605 if (mPrimaryMediaComponents[mode] != null 606 && mPrimaryMediaComponents[mode].equals((componentName))) { 607 return; 608 } 609 } 610 611 if (!mIndependentPlaybackConfig) { 612 setPlaybackMediaSource(componentName); 613 setBrowseMediaSource(componentName); 614 } else if (mode == MEDIA_SOURCE_MODE_PLAYBACK) { 615 setPlaybackMediaSource(componentName); 616 } else if (mode == MEDIA_SOURCE_MODE_BROWSE) { 617 setBrowseMediaSource(componentName); 618 } 619 } 620 setPlaybackMediaSource(ComponentName playbackMediaSource)621 private void setPlaybackMediaSource(ComponentName playbackMediaSource) { 622 stopAndUnregisterCallback(); 623 624 mActiveUserMediaController = null; 625 synchronized (mLock) { 626 mPrimaryMediaComponents[MEDIA_SOURCE_MODE_PLAYBACK] = playbackMediaSource; 627 } 628 629 if (playbackMediaSource != null 630 && !TextUtils.isEmpty(playbackMediaSource.flattenToString())) { 631 if (!isCurrentUserEphemeral()) { 632 saveLastMediaSource(playbackMediaSource, MEDIA_SOURCE_MODE_PLAYBACK); 633 } 634 if (playbackMediaSource 635 .equals(mRemovedMediaSourceComponents[MEDIA_SOURCE_MODE_PLAYBACK])) { 636 mRemovedMediaSourceComponents[MEDIA_SOURCE_MODE_PLAYBACK] = null; 637 } 638 } 639 640 notifyListeners(MEDIA_SOURCE_MODE_PLAYBACK); 641 642 startMediaConnectorService(shouldStartPlayback(mPlayOnMediaSourceChangedConfig), 643 new UserHandle(ActivityManager.getCurrentUser())); 644 // Reset current playback state for the new source, in the case that the app is in an error 645 // state (e.g. not signed in). This state will be updated from the app callback registered 646 // below, to make sure mCurrentPlaybackState reflects the current source only. 647 mCurrentPlaybackState = PlaybackState.STATE_NONE; 648 updateActiveMediaController(mMediaSessionManager 649 .getActiveSessionsForUser(null, ActivityManager.getCurrentUser())); 650 } 651 setBrowseMediaSource(ComponentName browseMediaSource)652 private void setBrowseMediaSource(ComponentName browseMediaSource) { 653 synchronized (mLock) { 654 mPrimaryMediaComponents[MEDIA_SOURCE_MODE_BROWSE] = browseMediaSource; 655 } 656 657 if (browseMediaSource != null && !TextUtils.isEmpty(browseMediaSource.flattenToString())) { 658 if (!isCurrentUserEphemeral()) { 659 saveLastMediaSource(browseMediaSource, MEDIA_SOURCE_MODE_BROWSE); 660 } 661 if (browseMediaSource 662 .equals(mRemovedMediaSourceComponents[MEDIA_SOURCE_MODE_BROWSE])) { 663 mRemovedMediaSourceComponents[MEDIA_SOURCE_MODE_BROWSE] = null; 664 } 665 } 666 667 notifyListeners(MEDIA_SOURCE_MODE_BROWSE); 668 } 669 notifyListeners(@arMediaManager.MediaSourceMode int mode)670 private void notifyListeners(@CarMediaManager.MediaSourceMode int mode) { 671 synchronized (mLock) { 672 int i = mMediaSourceListeners[mode].beginBroadcast(); 673 while (i-- > 0) { 674 try { 675 ICarMediaSourceListener callback = 676 mMediaSourceListeners[mode].getBroadcastItem(i); 677 callback.onMediaSourceChanged(mPrimaryMediaComponents[mode]); 678 } catch (RemoteException e) { 679 Log.e(CarLog.TAG_MEDIA, "calling onMediaSourceChanged failed " + e); 680 } 681 } 682 mMediaSourceListeners[mode].finishBroadcast(); 683 } 684 } 685 686 private MediaController.Callback mMediaControllerCallback = new MediaController.Callback() { 687 @Override 688 public void onPlaybackStateChanged(PlaybackState state) { 689 if (!isCurrentUserEphemeral()) { 690 savePlaybackState(state); 691 } 692 } 693 }; 694 695 /** 696 * Finds the currently playing media source, then updates the active source if the component 697 * name is different. 698 */ updatePrimaryMediaSourceWithCurrentlyPlaying( List<MediaController> controllers)699 private void updatePrimaryMediaSourceWithCurrentlyPlaying( 700 List<MediaController> controllers) { 701 for (MediaController controller : controllers) { 702 if (controller.getPlaybackState() != null 703 && controller.getPlaybackState().getState() == PlaybackState.STATE_PLAYING) { 704 String newPackageName = controller.getPackageName(); 705 String newClassName = getClassName(controller); 706 if (!matchPrimaryMediaSource(newPackageName, newClassName, 707 MEDIA_SOURCE_MODE_PLAYBACK)) { 708 ComponentName mediaSource = getMediaSource(newPackageName, newClassName); 709 if (Log.isLoggable(CarLog.TAG_MEDIA, Log.INFO)) { 710 if (mediaSource != null) { 711 Log.i(CarLog.TAG_MEDIA, 712 "MediaController changed, updating media source to: " 713 + mediaSource.flattenToString()); 714 } else { 715 // Some apps, like Chrome, have a MediaSession but no 716 // MediaBrowseService. Media Center doesn't consider such apps as 717 // valid media sources. 718 Log.i(CarLog.TAG_MEDIA, 719 "MediaController changed, but no media browse service found " 720 + "in package: " + newPackageName); 721 } 722 } 723 setPrimaryMediaSource(mediaSource, MEDIA_SOURCE_MODE_PLAYBACK); 724 } 725 return; 726 } 727 } 728 } 729 matchPrimaryMediaSource(@onNull String newPackageName, @NonNull String newClassName, @CarMediaManager.MediaSourceMode int mode)730 private boolean matchPrimaryMediaSource(@NonNull String newPackageName, 731 @NonNull String newClassName, @CarMediaManager.MediaSourceMode int mode) { 732 synchronized (mLock) { 733 if (mPrimaryMediaComponents[mode] != null 734 && mPrimaryMediaComponents[mode].getPackageName().equals(newPackageName)) { 735 // If the class name of currently active source is not specified, only checks 736 // package name; otherwise checks both package name and class name. 737 if (TextUtils.isEmpty(newClassName)) { 738 return true; 739 } else { 740 return newClassName.equals(mPrimaryMediaComponents[mode].getClassName()); 741 } 742 } 743 } 744 return false; 745 } 746 747 /** 748 * Returns {@code true} if the provided component has a valid {@link MediaBrowseService}. 749 */ 750 @VisibleForTesting isMediaService(@onNull ComponentName componentName)751 public boolean isMediaService(@NonNull ComponentName componentName) { 752 return getMediaService(componentName) != null; 753 } 754 755 /* 756 * Gets the media service that matches the componentName for the current foreground user. 757 */ getMediaService(@onNull ComponentName componentName)758 private ComponentName getMediaService(@NonNull ComponentName componentName) { 759 String packageName = componentName.getPackageName(); 760 String className = componentName.getClassName(); 761 762 PackageManager packageManager = mContext.getPackageManager(); 763 Intent mediaIntent = new Intent(); 764 mediaIntent.setPackage(packageName); 765 mediaIntent.setAction(MediaBrowserService.SERVICE_INTERFACE); 766 List<ResolveInfo> mediaServices = packageManager.queryIntentServicesAsUser(mediaIntent, 767 PackageManager.GET_RESOLVED_FILTER, ActivityManager.getCurrentUser()); 768 769 for (ResolveInfo service : mediaServices) { 770 String serviceName = service.serviceInfo.name; 771 if (!TextUtils.isEmpty(serviceName) 772 // If className is not specified, returns the first service in the package; 773 // otherwise returns the matched service. 774 // TODO(b/136274456): find a proper way to handle the case where there are 775 // multiple services and the className is not specified. 776 777 && (TextUtils.isEmpty(className) || serviceName.equals(className))) { 778 return new ComponentName(packageName, serviceName); 779 } 780 } 781 782 if (Log.isLoggable(CarLog.TAG_MEDIA, Log.DEBUG)) { 783 Log.d(CarLog.TAG_MEDIA, "No MediaBrowseService with ComponentName: " 784 + componentName.flattenToString()); 785 } 786 return null; 787 } 788 789 /* 790 * Gets the component name of the media service. 791 */ 792 @Nullable getMediaSource(@onNull String packageName, @NonNull String className)793 private ComponentName getMediaSource(@NonNull String packageName, @NonNull String className) { 794 return getMediaService(new ComponentName(packageName, className)); 795 } 796 saveLastMediaSource(@onNull ComponentName component, int mode)797 private void saveLastMediaSource(@NonNull ComponentName component, int mode) { 798 if (!sharedPrefsInitialized()) { 799 return; 800 } 801 String componentName = component.flattenToString(); 802 String key = getMediaSourceKey(mode); 803 String serialized = mSharedPrefs.getString(key, null); 804 if (serialized == null) { 805 mSharedPrefs.edit().putString(key, componentName).apply(); 806 } else { 807 Deque<String> componentNames = new ArrayDeque<>(getComponentNameList(serialized)); 808 componentNames.remove(componentName); 809 componentNames.addFirst(componentName); 810 mSharedPrefs.edit().putString(key, serializeComponentNameList(componentNames)).apply(); 811 } 812 } 813 getLastMediaSource(int mode)814 private @NonNull ComponentName getLastMediaSource(int mode) { 815 if (sharedPrefsInitialized()) { 816 String key = getMediaSourceKey(mode); 817 String serialized = mSharedPrefs.getString(key, null); 818 if (!TextUtils.isEmpty(serialized)) { 819 for (String name : getComponentNameList(serialized)) { 820 ComponentName componentName = ComponentName.unflattenFromString(name); 821 if (isMediaService(componentName)) { 822 return componentName; 823 } 824 } 825 } 826 } 827 return getDefaultMediaSource(); 828 } 829 getDefaultMediaSource()830 private ComponentName getDefaultMediaSource() { 831 String defaultMediaSource = mContext.getString(R.string.config_defaultMediaSource); 832 ComponentName defaultComponent = ComponentName.unflattenFromString(defaultMediaSource); 833 if (isMediaService(defaultComponent)) { 834 return defaultComponent; 835 } 836 return null; 837 } 838 serializeComponentNameList(Deque<String> componentNames)839 private String serializeComponentNameList(Deque<String> componentNames) { 840 return componentNames.stream().collect(Collectors.joining(COMPONENT_NAME_SEPARATOR)); 841 } 842 getComponentNameList(String serialized)843 private List<String> getComponentNameList(String serialized) { 844 String[] componentNames = serialized.split(COMPONENT_NAME_SEPARATOR); 845 return (Arrays.asList(componentNames)); 846 } 847 savePlaybackState(PlaybackState playbackState)848 private void savePlaybackState(PlaybackState playbackState) { 849 if (!sharedPrefsInitialized()) { 850 return; 851 } 852 int state = playbackState != null ? playbackState.getState() : PlaybackState.STATE_NONE; 853 mCurrentPlaybackState = state; 854 String key = getPlaybackStateKey(); 855 mSharedPrefs.edit().putInt(key, state).apply(); 856 } 857 858 /** 859 * Builds a string key for saving the playback state for a specific media source (and user) 860 */ getPlaybackStateKey()861 private String getPlaybackStateKey() { 862 synchronized (mLock) { 863 return PLAYBACK_STATE_KEY + ActivityManager.getCurrentUser() 864 + (mPrimaryMediaComponents[MEDIA_SOURCE_MODE_PLAYBACK] == null ? "" 865 : mPrimaryMediaComponents[MEDIA_SOURCE_MODE_PLAYBACK].flattenToString()); 866 } 867 } 868 getMediaSourceKey(int mode)869 private String getMediaSourceKey(int mode) { 870 return SOURCE_KEY + mode + SOURCE_KEY_SEPARATOR + ActivityManager.getCurrentUser(); 871 } 872 873 /** 874 * Updates active media controller from the list that has the same component name as the primary 875 * media component. Clears callback and resets media controller to null if not found. 876 */ updateActiveMediaController(List<MediaController> mediaControllers)877 private void updateActiveMediaController(List<MediaController> mediaControllers) { 878 if (mPrimaryMediaComponents[MEDIA_SOURCE_MODE_PLAYBACK] == null) { 879 return; 880 } 881 if (mActiveUserMediaController != null) { 882 mActiveUserMediaController.unregisterCallback(mMediaControllerCallback); 883 mActiveUserMediaController = null; 884 } 885 for (MediaController controller : mediaControllers) { 886 if (matchPrimaryMediaSource(controller.getPackageName(), getClassName(controller), 887 MEDIA_SOURCE_MODE_PLAYBACK)) { 888 mActiveUserMediaController = controller; 889 PlaybackState state = mActiveUserMediaController.getPlaybackState(); 890 if (!isCurrentUserEphemeral()) { 891 savePlaybackState(state); 892 } 893 // Specify Handler to receive callbacks on, to avoid defaulting to the calling 894 // thread; this method can be called from the MediaSessionManager callback. 895 // Using the version of this method without passing a handler causes a 896 // RuntimeException for failing to create a Handler. 897 mActiveUserMediaController.registerCallback(mMediaControllerCallback, mHandler); 898 return; 899 } 900 } 901 } 902 903 /** 904 * Returns whether we should autoplay the current media source 905 */ shouldStartPlayback(int config)906 private boolean shouldStartPlayback(int config) { 907 switch (config) { 908 case AUTOPLAY_CONFIG_NEVER: 909 return false; 910 case AUTOPLAY_CONFIG_ALWAYS: 911 return true; 912 case AUTOPLAY_CONFIG_RETAIN_PER_SOURCE: 913 if (!sharedPrefsInitialized()) { 914 return false; 915 } 916 return mSharedPrefs.getInt(getPlaybackStateKey(), PlaybackState.STATE_NONE) 917 == PlaybackState.STATE_PLAYING; 918 case AUTOPLAY_CONFIG_RETAIN_PREVIOUS: 919 return mCurrentPlaybackState == PlaybackState.STATE_PLAYING; 920 default: 921 Log.e(CarLog.TAG_MEDIA, "Unsupported playback configuration: " + config); 922 return false; 923 } 924 } 925 926 @NonNull getClassName(@onNull MediaController controller)927 private static String getClassName(@NonNull MediaController controller) { 928 Bundle sessionExtras = controller.getExtras(); 929 String value = 930 sessionExtras == null ? "" : sessionExtras.getString( 931 Car.CAR_EXTRA_BROWSE_SERVICE_FOR_SESSION); 932 return value != null ? value : ""; 933 } 934 } 935