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