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