/* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.car; import static android.car.media.CarMediaManager.MEDIA_SOURCE_MODE_BROWSE; import static android.car.media.CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.TestApi; import android.app.ActivityManager; import android.car.Car; import android.car.hardware.power.CarPowerPolicy; import android.car.hardware.power.CarPowerPolicyFilter; import android.car.hardware.power.ICarPowerPolicyListener; import android.car.hardware.power.PowerComponent; import android.car.media.CarMediaManager; import android.car.media.CarMediaManager.MediaSourceChangedListener; import android.car.media.CarMediaManager.MediaSourceMode; import android.car.media.ICarMedia; import android.car.media.ICarMediaSourceListener; import android.car.user.CarUserManager; import android.car.user.CarUserManager.UserLifecycleListener; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.media.session.MediaController; import android.media.session.MediaController.TransportControls; import android.media.session.MediaSession; import android.media.session.MediaSession.Token; import android.media.session.MediaSessionManager; import android.media.session.MediaSessionManager.OnActiveSessionsChangedListener; import android.media.session.PlaybackState; import android.os.Bundle; import android.os.Handler; import android.os.HandlerExecutor; import android.os.HandlerThread; import android.os.Looper; import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.service.media.MediaBrowserService; import android.text.TextUtils; import android.util.IndentingPrintWriter; import android.util.Log; import android.util.Slog; import com.android.car.power.CarPowerManagementService; import com.android.car.user.CarUserService; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Deque; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; /** * CarMediaService manages the currently active media source for car apps. This is different from * the MediaSessionManager's active sessions, as there can only be one active source in the car, * through both browse and playback. * * In the car, the active media source does not necessarily have an active MediaSession, e.g. if * it were being browsed only. However, that source is still considered the active source, and * should be the source displayed in any Media related UIs (Media Center, home screen, etc). */ public class CarMediaService extends ICarMedia.Stub implements CarServiceBase { private static final String SOURCE_KEY = "media_source_component"; private static final String SOURCE_KEY_SEPARATOR = "_"; private static final String PLAYBACK_STATE_KEY = "playback_state"; private static final String SHARED_PREF = "com.android.car.media.car_media_service"; private static final String COMPONENT_NAME_SEPARATOR = ","; private static final String MEDIA_CONNECTION_ACTION = "com.android.car.media.MEDIA_CONNECTION"; private static final String EXTRA_AUTOPLAY = "com.android.car.media.autoplay"; private static final int MEDIA_SOURCE_MODES = 2; // XML configuration options for autoplay on media source change. private static final int AUTOPLAY_CONFIG_NEVER = 0; private static final int AUTOPLAY_CONFIG_ALWAYS = 1; // This mode uses the current source's last stored playback state to resume playback private static final int AUTOPLAY_CONFIG_RETAIN_PER_SOURCE = 2; // This mode uses the previous source's playback state to resume playback private static final int AUTOPLAY_CONFIG_RETAIN_PREVIOUS = 3; private final Context mContext; private final CarUserService mUserService; private final UserManager mUserManager; private final MediaSessionManager mMediaSessionManager; private final MediaSessionUpdater mMediaSessionUpdater = new MediaSessionUpdater(); @GuardedBy("mLock") private ComponentName[] mPrimaryMediaComponents = new ComponentName[MEDIA_SOURCE_MODES]; // MediaController for the current active user's active media session. This controller can be // null if playback has not been started yet. @GuardedBy("mLock") private MediaController mActiveUserMediaController; @GuardedBy("mLock") private int mCurrentPlaybackState; @GuardedBy("mLock") private boolean mIsDisabledByPowerPolicy; @GuardedBy("mLock") private boolean mWasPreviouslyDisabledByPowerPolicy; @GuardedBy("mLock") private boolean mWasPlayingBeforeDisabled; private SharedPreferences mSharedPrefs; private SessionChangedListener mSessionsListener; private int mPlayOnMediaSourceChangedConfig; private int mPlayOnBootConfig; private boolean mIndependentPlaybackConfig; private boolean mPendingInit; @GuardedBy("mLock") private final RemoteCallbackList[] mMediaSourceListeners = new RemoteCallbackList[MEDIA_SOURCE_MODES]; private final Handler mMainHandler = new Handler(Looper.getMainLooper()); private final HandlerThread mHandlerThread = CarServiceUtils.getHandlerThread( getClass().getSimpleName()); // Handler to receive PlaybackState callbacks from the active media controller. private final Handler mHandler = new Handler(mHandlerThread.getLooper()); private final Object mLock = new Object(); /** The component name of the last media source that was removed while being primary. */ private ComponentName[] mRemovedMediaSourceComponents = new ComponentName[MEDIA_SOURCE_MODES]; private final IntentFilter mPackageUpdateFilter; private boolean mIsPackageUpdateReceiverRegistered; /** * Listens to {@link Intent#ACTION_PACKAGE_REMOVED}, so we can fall back to a previously used * media source when the active source is uninstalled. */ private final BroadcastReceiver mPackageUpdateReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (intent.getData() == null) { return; } String intentPackage = intent.getData().getSchemeSpecificPart(); if (Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) { synchronized (mLock) { for (int i = 0; i < MEDIA_SOURCE_MODES; i++) { if (mPrimaryMediaComponents[i] != null && mPrimaryMediaComponents[i].getPackageName().equals( intentPackage)) { if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { // If package is being replaced, it may not be removed from // PackageManager queries when we check for available // MediaBrowseServices, so we iterate to find the next available // source. for (ComponentName component : getLastMediaSources(i)) { if (!mPrimaryMediaComponents[i].getPackageName() .equals(component.getPackageName())) { mRemovedMediaSourceComponents[i] = mPrimaryMediaComponents[i]; if (Log.isLoggable(CarLog.TAG_MEDIA, Log.DEBUG)) { Slog.d(CarLog.TAG_MEDIA, "temporarily replacing updated media source " + mPrimaryMediaComponents[i] + "with backup source: " + component); } setPrimaryMediaSource(component, i); return; } } Slog.e(CarLog.TAG_MEDIA, "No available backup media source"); } else { if (Log.isLoggable(CarLog.TAG_MEDIA, Log.DEBUG)) { Slog.d(CarLog.TAG_MEDIA, "replacing removed media source " + mPrimaryMediaComponents[i] + "with backup source: " + getLastMediaSource(i)); } mRemovedMediaSourceComponents[i] = null; setPrimaryMediaSource(getLastMediaSource(i), i); } } } } } else if (Intent.ACTION_PACKAGE_REPLACED.equals(intent.getAction()) || Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction())) { for (int i = 0; i < MEDIA_SOURCE_MODES; i++) { if (mRemovedMediaSourceComponents[i] != null && mRemovedMediaSourceComponents[i] .getPackageName().equals(intentPackage)) { if (Log.isLoggable(CarLog.TAG_MEDIA, Log.DEBUG)) { Slog.d(CarLog.TAG_MEDIA, "restoring removed source: " + mRemovedMediaSourceComponents[i]); } setPrimaryMediaSource(mRemovedMediaSourceComponents[i], i); } } } } }; private final UserLifecycleListener mUserLifecycleListener = event -> { if (Log.isLoggable(CarLog.TAG_MEDIA, Log.DEBUG)) { Slog.d(CarLog.TAG_MEDIA, "CarMediaService.onEvent(" + event + ")"); } switch (event.getEventType()) { case CarUserManager.USER_LIFECYCLE_EVENT_TYPE_SWITCHING: maybeInitUser(event.getUserId()); break; case CarUserManager.USER_LIFECYCLE_EVENT_TYPE_UNLOCKED: onUserUnlock(event.getUserId()); break; } }; private final ICarPowerPolicyListener mPowerPolicyListener = new ICarPowerPolicyListener.Stub() { @Override public void onPolicyChanged(CarPowerPolicy appliedPolicy, CarPowerPolicy accumulatedPolicy) { boolean shouldBePlaying; MediaController mediaController; boolean isOff = !accumulatedPolicy.isComponentEnabled(PowerComponent.MEDIA); synchronized (mLock) { boolean weArePlaying = mCurrentPlaybackState == PlaybackState.STATE_PLAYING; mIsDisabledByPowerPolicy = isOff; if (isOff) { if (!mWasPreviouslyDisabledByPowerPolicy) { // We're disabling media component. // Remember if we are playing at this transition. mWasPlayingBeforeDisabled = weArePlaying; mWasPreviouslyDisabledByPowerPolicy = true; } shouldBePlaying = false; } else { mWasPreviouslyDisabledByPowerPolicy = false; shouldBePlaying = mWasPlayingBeforeDisabled; } if (shouldBePlaying == weArePlaying) { return; } // Make a change mediaController = mActiveUserMediaController; if (mediaController == null) { return; } } PlaybackState oldState = mediaController.getPlaybackState(); savePlaybackState( // The new state is the same as the old state, except for play/pause new PlaybackState.Builder(oldState) .setState(shouldBePlaying ? PlaybackState.STATE_PLAYING : PlaybackState.STATE_PAUSED, oldState.getPosition(), oldState.getPlaybackSpeed()) .build()); if (shouldBePlaying) { mediaController.getTransportControls().play(); } else { mediaController.getTransportControls().pause(); } } }; public CarMediaService(Context context, CarUserService userService) { mContext = context; mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); mMediaSessionManager = mContext.getSystemService(MediaSessionManager.class); mMediaSourceListeners[MEDIA_SOURCE_MODE_PLAYBACK] = new RemoteCallbackList(); mMediaSourceListeners[MEDIA_SOURCE_MODE_BROWSE] = new RemoteCallbackList(); mIndependentPlaybackConfig = mContext.getResources().getBoolean( R.bool.config_mediaSourceIndependentPlayback); mPackageUpdateFilter = new IntentFilter(); mPackageUpdateFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); mPackageUpdateFilter.addAction(Intent.ACTION_PACKAGE_REPLACED); mPackageUpdateFilter.addAction(Intent.ACTION_PACKAGE_ADDED); mPackageUpdateFilter.addDataScheme("package"); mUserService = userService; mUserService.addUserLifecycleListener(mUserLifecycleListener); mPlayOnMediaSourceChangedConfig = mContext.getResources().getInteger(R.integer.config_mediaSourceChangedAutoplay); mPlayOnBootConfig = mContext.getResources().getInteger(R.integer.config_mediaBootAutoplay); } @Override // This method is called from ICarImpl after CarMediaService is created. public void init() { int currentUser = ActivityManager.getCurrentUser(); maybeInitUser(currentUser); setPowerPolicyListener(); } private void maybeInitUser(int userId) { if (userId == UserHandle.USER_SYSTEM) { return; } if (mUserManager.isUserUnlocked(userId)) { initUser(userId); } else { mPendingInit = true; } } private void initUser(int userId) { // SharedPreferences are shared among different users thus only need initialized once. And // they should be initialized after user 0 is unlocked because SharedPreferences in // credential encrypted storage are not available until after user 0 is unlocked. // initUser() is called when the current foreground user is unlocked, and by that time user // 0 has been unlocked already, so initializing SharedPreferences in initUser() is fine. synchronized (mLock) { if (mSharedPrefs == null) { mSharedPrefs = mContext.getSharedPreferences(SHARED_PREF, Context.MODE_PRIVATE); } if (mIsPackageUpdateReceiverRegistered) { mContext.unregisterReceiver(mPackageUpdateReceiver); } UserHandle currentUser = new UserHandle(userId); mContext.registerReceiverAsUser(mPackageUpdateReceiver, currentUser, mPackageUpdateFilter, null, null); mIsPackageUpdateReceiverRegistered = true; mPrimaryMediaComponents[MEDIA_SOURCE_MODE_PLAYBACK] = isCurrentUserEphemeral() ? getDefaultMediaSource() : getLastMediaSource(MEDIA_SOURCE_MODE_PLAYBACK); mPrimaryMediaComponents[MEDIA_SOURCE_MODE_BROWSE] = isCurrentUserEphemeral() ? getDefaultMediaSource() : getLastMediaSource(MEDIA_SOURCE_MODE_BROWSE); mActiveUserMediaController = null; updateMediaSessionCallbackForCurrentUser(); notifyListeners(MEDIA_SOURCE_MODE_PLAYBACK); notifyListeners(MEDIA_SOURCE_MODE_BROWSE); startMediaConnectorService(shouldStartPlayback(mPlayOnBootConfig), currentUser); } } /** * Starts a service on the current user that binds to the media browser of the current media * source. We start a new service because this one runs on user 0, and MediaBrowser doesn't * provide an API to connect on a specific user. Additionally, this service will attempt to * resume playback using the MediaSession obtained via the media browser connection, which * is more reliable than using active MediaSessions from MediaSessionManager. */ private void startMediaConnectorService(boolean startPlayback, UserHandle currentUser) { Intent serviceStart = new Intent(MEDIA_CONNECTION_ACTION); serviceStart.setPackage(mContext.getResources().getString(R.string.serviceMediaConnection)); serviceStart.putExtra(EXTRA_AUTOPLAY, startPlayback); mContext.startForegroundServiceAsUser(serviceStart, currentUser); } private boolean sharedPrefsInitialized() { if (mSharedPrefs == null) { // It shouldn't reach this but let's be cautious. Slog.e(CarLog.TAG_MEDIA, "SharedPreferences are not initialized!"); String className = getClass().getName(); for (StackTraceElement ste : Thread.currentThread().getStackTrace()) { // Let's print the useful logs only. String log = ste.toString(); if (log.contains(className)) { Slog.e(CarLog.TAG_MEDIA, log); } } return false; } return true; } private boolean isCurrentUserEphemeral() { return mUserManager.getUserInfo(ActivityManager.getCurrentUser()).isEphemeral(); } // Sets a listener to be notified when the current power policy changes. // Basically, the listener pauses the audio when a media component is disabled and resumes // the audio when a media component is enabled. // This is called only from init(). private void setPowerPolicyListener() { CarPowerPolicyFilter filter = new CarPowerPolicyFilter.Builder() .setComponents(PowerComponent.MEDIA).build(); CarLocalServices.getService(CarPowerManagementService.class) .addPowerPolicyListener(filter, mPowerPolicyListener); } @Override public void release() { mMediaSessionUpdater.unregisterCallbacks(); mUserService.removeUserLifecycleListener(mUserLifecycleListener); CarLocalServices.getService(CarPowerManagementService.class) .removePowerPolicyListener(mPowerPolicyListener); } @Override public void dump(IndentingPrintWriter writer) { synchronized (mLock) { writer.println("*CarMediaService*"); writer.increaseIndent(); writer.printf("Current playback media component: %s\n", mPrimaryMediaComponents[MEDIA_SOURCE_MODE_PLAYBACK] == null ? "-" : mPrimaryMediaComponents[MEDIA_SOURCE_MODE_PLAYBACK].flattenToString()); writer.printf("Current browse media component: %s\n", mPrimaryMediaComponents[MEDIA_SOURCE_MODE_BROWSE] == null ? "-" : mPrimaryMediaComponents[MEDIA_SOURCE_MODE_BROWSE].flattenToString()); if (mActiveUserMediaController != null) { writer.printf("Current media controller: %s\n", mActiveUserMediaController.getPackageName()); writer.printf("Current browse service extra: %s\n", getClassName(mActiveUserMediaController)); } writer.printf("Number of active media sessions: %s\n", mMediaSessionManager .getActiveSessionsForUser(null, new UserHandle(ActivityManager.getCurrentUser())).size()); writer.println("Playback media source history:"); writer.increaseIndent(); for (ComponentName name : getLastMediaSources(MEDIA_SOURCE_MODE_PLAYBACK)) { writer.println(name.flattenToString()); } writer.decreaseIndent(); writer.println("Browse media source history:"); writer.increaseIndent(); for (ComponentName name : getLastMediaSources(MEDIA_SOURCE_MODE_BROWSE)) { writer.println(name.flattenToString()); } writer.decreaseIndent(); writer.printf("Disabled by power policy: %s\n", mIsDisabledByPowerPolicy); if (mIsDisabledByPowerPolicy) { writer.printf("Before being disabled by power policy, audio was %s\n", mWasPlayingBeforeDisabled ? "active" : "inactive"); } } } /** * @see {@link CarMediaManager#setMediaSource(ComponentName)} */ @Override public void setMediaSource(@NonNull ComponentName componentName, @MediaSourceMode int mode) { ICarImpl.assertPermission(mContext, android.Manifest.permission.MEDIA_CONTENT_CONTROL); if (Log.isLoggable(CarLog.TAG_MEDIA, Log.DEBUG)) { Slog.d(CarLog.TAG_MEDIA, "Changing media source to: " + componentName.getPackageName()); } setPrimaryMediaSource(componentName, mode); } /** * @see {@link CarMediaManager#getMediaSource()} */ @Override public ComponentName getMediaSource(@CarMediaManager.MediaSourceMode int mode) { ICarImpl.assertPermission(mContext, android.Manifest.permission.MEDIA_CONTENT_CONTROL); synchronized (mLock) { return mPrimaryMediaComponents[mode]; } } /** * @see {@link CarMediaManager#registerMediaSourceListener(MediaSourceChangedListener)} */ @Override public void registerMediaSourceListener(ICarMediaSourceListener callback, @MediaSourceMode int mode) { ICarImpl.assertPermission(mContext, android.Manifest.permission.MEDIA_CONTENT_CONTROL); synchronized (mLock) { mMediaSourceListeners[mode].register(callback); } } /** * @see {@link CarMediaManager#unregisterMediaSourceListener(ICarMediaSourceListener)} */ @Override public void unregisterMediaSourceListener(ICarMediaSourceListener callback, @MediaSourceMode int mode) { ICarImpl.assertPermission(mContext, android.Manifest.permission.MEDIA_CONTENT_CONTROL); synchronized (mLock) { mMediaSourceListeners[mode].unregister(callback); } } @Override public List getLastMediaSources(@CarMediaManager.MediaSourceMode int mode) { ICarImpl.assertPermission(mContext, android.Manifest.permission.MEDIA_CONTENT_CONTROL); String key = getMediaSourceKey(mode); String serialized = mSharedPrefs.getString(key, ""); return getComponentNameList(serialized).stream() .map(name -> ComponentName.unflattenFromString(name)).collect(Collectors.toList()); } /** See {@link CarMediaManager#isIndependentPlaybackConfig}. */ @Override @TestApi public boolean isIndependentPlaybackConfig() { ICarImpl.assertPermission(mContext, android.Manifest.permission.MEDIA_CONTENT_CONTROL); synchronized (mLock) { return mIndependentPlaybackConfig; } } /** See {@link CarMediaManager#setIndependentPlaybackConfig}. */ @Override @TestApi public void setIndependentPlaybackConfig(boolean independent) { ICarImpl.assertPermission(mContext, android.Manifest.permission.MEDIA_CONTENT_CONTROL); synchronized (mLock) { mIndependentPlaybackConfig = independent; } } // TODO(b/153115826): this method was used to be called from the ICar binder thread, but it's // now called by UserCarService. Currently UserCarServie is calling every listener in one // non-main thread, but it's not clear how the final behavior will be. So, for now it's ok // to post it to mMainHandler, but once b/145689885 is fixed, we might not need it. private void onUserUnlock(int userId) { mMainHandler.post(() -> { // No need to handle system user, non current foreground user. if (userId == UserHandle.USER_SYSTEM || userId != ActivityManager.getCurrentUser()) { return; } if (mPendingInit) { initUser(userId); mPendingInit = false; if (Log.isLoggable(CarLog.TAG_MEDIA, Log.DEBUG)) { Slog.d(CarLog.TAG_MEDIA, "User " + userId + " is now unlocked"); } } }); } private void updateMediaSessionCallbackForCurrentUser() { if (mSessionsListener != null) { mMediaSessionManager.removeOnActiveSessionsChangedListener(mSessionsListener); } mSessionsListener = new SessionChangedListener(ActivityManager.getCurrentUser()); UserHandle currentUserHandle = new UserHandle(ActivityManager.getCurrentUser()); mMediaSessionManager.addOnActiveSessionsChangedListener(null, currentUserHandle, new HandlerExecutor(mHandler), mSessionsListener); mMediaSessionUpdater.registerCallbacks(mMediaSessionManager.getActiveSessionsForUser(null, currentUserHandle)); } /** * Attempts to stop the current source using MediaController.TransportControls.stop() * This method also unregisters callbacks to the active media controller before calling stop(), * to preserve the PlaybackState before stopping. */ private void stopAndUnregisterCallback() { synchronized (mLock) { if (mActiveUserMediaController != null) { mActiveUserMediaController.unregisterCallback(mMediaControllerCallback); if (Log.isLoggable(CarLog.TAG_MEDIA, Log.DEBUG)) { Slog.d(CarLog.TAG_MEDIA, "stopping " + mActiveUserMediaController.getPackageName()); } TransportControls controls = mActiveUserMediaController.getTransportControls(); if (controls != null) { controls.stop(); } else { Slog.e(CarLog.TAG_MEDIA, "Can't stop playback, transport controls unavailable " + mActiveUserMediaController.getPackageName()); } } } } private class SessionChangedListener implements OnActiveSessionsChangedListener { private final int mCurrentUser; SessionChangedListener(int currentUser) { mCurrentUser = currentUser; } @Override public void onActiveSessionsChanged(List controllers) { if (ActivityManager.getCurrentUser() != mCurrentUser) { Slog.e(CarLog.TAG_MEDIA, "Active session callback for old user: " + mCurrentUser); return; } mMediaSessionUpdater.registerCallbacks(controllers); } } private class MediaControllerCallback extends MediaController.Callback { private final MediaController mMediaController; private int mPreviousPlaybackState; private MediaControllerCallback(MediaController mediaController) { mMediaController = mediaController; PlaybackState state = mediaController.getPlaybackState(); mPreviousPlaybackState = (state == null) ? PlaybackState.STATE_NONE : state.getState(); } private void register() { mMediaController.registerCallback(this); } private void unregister() { mMediaController.unregisterCallback(this); } @Override public void onPlaybackStateChanged(@Nullable PlaybackState state) { if (state.getState() == PlaybackState.STATE_PLAYING && state.getState() != mPreviousPlaybackState) { ComponentName mediaSource = getMediaSource(mMediaController.getPackageName(), getClassName(mMediaController)); if (mediaSource != null && !mediaSource.equals(mPrimaryMediaComponents[MEDIA_SOURCE_MODE_PLAYBACK]) && Log.isLoggable(CarLog.TAG_MEDIA, Log.INFO)) { Slog.i(CarLog.TAG_MEDIA, "Changing media source due to playback state change: " + mediaSource.flattenToString()); } setPrimaryMediaSource(mediaSource, MEDIA_SOURCE_MODE_PLAYBACK); } mPreviousPlaybackState = state.getState(); } } private class MediaSessionUpdater { private Map mCallbacks = new HashMap<>(); /** * Register a {@link MediaControllerCallback} for each given controller. Note that if a * controller was already watched, we don't register a callback again. This prevents an * undesired revert of the primary media source. Callbacks for previously watched * controllers that are not present in the given list are unregistered. */ private void registerCallbacks(List newControllers) { List additions = new ArrayList<>(newControllers.size()); Map updatedCallbacks = new HashMap<>(newControllers.size()); for (MediaController controller : newControllers) { MediaSession.Token token = controller.getSessionToken(); MediaControllerCallback callback = mCallbacks.get(token); if (callback == null) { callback = new MediaControllerCallback(controller); callback.register(); additions.add(controller); } updatedCallbacks.put(token, callback); } for (MediaSession.Token token : mCallbacks.keySet()) { if (!updatedCallbacks.containsKey(token)) { mCallbacks.get(token).unregister(); } } mCallbacks = updatedCallbacks; updatePrimaryMediaSourceWithCurrentlyPlaying(additions); // If there are no playing media sources, and we don't currently have the controller // for the active source, check the active sessions for a matching controller. If this // is called after a user switch, its possible for a matching controller to already be // active before the user is unlocked, so we check all of the current controllers synchronized (mLock) { if (mActiveUserMediaController == null) { updateActiveMediaControllerLocked(newControllers); } } } /** * Unregister all MediaController callbacks */ private void unregisterCallbacks() { for (Map.Entry entry : mCallbacks.entrySet()) { entry.getValue().unregister(); } } } /** * Updates the primary media source, then notifies content observers of the change * Will update both the playback and browse sources if independent playback is not supported */ private void setPrimaryMediaSource(@NonNull ComponentName componentName, @CarMediaManager.MediaSourceMode int mode) { synchronized (mLock) { if (mPrimaryMediaComponents[mode] != null && mPrimaryMediaComponents[mode].equals((componentName))) { return; } } if (!mIndependentPlaybackConfig) { setPlaybackMediaSource(componentName); setBrowseMediaSource(componentName); } else if (mode == MEDIA_SOURCE_MODE_PLAYBACK) { setPlaybackMediaSource(componentName); } else if (mode == MEDIA_SOURCE_MODE_BROWSE) { setBrowseMediaSource(componentName); } } private void setPlaybackMediaSource(ComponentName playbackMediaSource) { stopAndUnregisterCallback(); synchronized (mLock) { mActiveUserMediaController = null; mPrimaryMediaComponents[MEDIA_SOURCE_MODE_PLAYBACK] = playbackMediaSource; } if (playbackMediaSource != null && !TextUtils.isEmpty(playbackMediaSource.flattenToString())) { if (!isCurrentUserEphemeral()) { saveLastMediaSource(playbackMediaSource, MEDIA_SOURCE_MODE_PLAYBACK); } if (playbackMediaSource .equals(mRemovedMediaSourceComponents[MEDIA_SOURCE_MODE_PLAYBACK])) { mRemovedMediaSourceComponents[MEDIA_SOURCE_MODE_PLAYBACK] = null; } } notifyListeners(MEDIA_SOURCE_MODE_PLAYBACK); startMediaConnectorService(shouldStartPlayback(mPlayOnMediaSourceChangedConfig), new UserHandle(ActivityManager.getCurrentUser())); // Reset current playback state for the new source, in the case that the app is in an error // state (e.g. not signed in). This state will be updated from the app callback registered // below, to make sure mCurrentPlaybackState reflects the current source only. synchronized (mLock) { mCurrentPlaybackState = PlaybackState.STATE_NONE; updateActiveMediaControllerLocked(mMediaSessionManager .getActiveSessionsForUser(null, new UserHandle(ActivityManager.getCurrentUser()))); } } private void setBrowseMediaSource(ComponentName browseMediaSource) { synchronized (mLock) { mPrimaryMediaComponents[MEDIA_SOURCE_MODE_BROWSE] = browseMediaSource; } if (browseMediaSource != null && !TextUtils.isEmpty(browseMediaSource.flattenToString())) { if (!isCurrentUserEphemeral()) { saveLastMediaSource(browseMediaSource, MEDIA_SOURCE_MODE_BROWSE); } if (browseMediaSource .equals(mRemovedMediaSourceComponents[MEDIA_SOURCE_MODE_BROWSE])) { mRemovedMediaSourceComponents[MEDIA_SOURCE_MODE_BROWSE] = null; } } notifyListeners(MEDIA_SOURCE_MODE_BROWSE); } private void notifyListeners(@CarMediaManager.MediaSourceMode int mode) { synchronized (mLock) { int i = mMediaSourceListeners[mode].beginBroadcast(); while (i-- > 0) { try { ICarMediaSourceListener callback = mMediaSourceListeners[mode].getBroadcastItem(i); callback.onMediaSourceChanged(mPrimaryMediaComponents[mode]); } catch (RemoteException e) { Slog.e(CarLog.TAG_MEDIA, "calling onMediaSourceChanged failed " + e); } } mMediaSourceListeners[mode].finishBroadcast(); } } private MediaController.Callback mMediaControllerCallback = new MediaController.Callback() { @Override public void onPlaybackStateChanged(PlaybackState state) { savePlaybackState(state); } }; /** * Finds the currently playing media source, then updates the active source if the component * name is different. */ private void updatePrimaryMediaSourceWithCurrentlyPlaying( List controllers) { for (MediaController controller : controllers) { if (controller.getPlaybackState() != null && controller.getPlaybackState().getState() == PlaybackState.STATE_PLAYING) { String newPackageName = controller.getPackageName(); String newClassName = getClassName(controller); if (!matchPrimaryMediaSource(newPackageName, newClassName, MEDIA_SOURCE_MODE_PLAYBACK)) { ComponentName mediaSource = getMediaSource(newPackageName, newClassName); if (Log.isLoggable(CarLog.TAG_MEDIA, Log.INFO)) { if (mediaSource != null) { Slog.i(CarLog.TAG_MEDIA, "MediaController changed, updating media source to: " + mediaSource.flattenToString()); } else { // Some apps, like Chrome, have a MediaSession but no // MediaBrowseService. Media Center doesn't consider such apps as // valid media sources. Slog.i(CarLog.TAG_MEDIA, "MediaController changed, but no media browse service found " + "in package: " + newPackageName); } } setPrimaryMediaSource(mediaSource, MEDIA_SOURCE_MODE_PLAYBACK); } return; } } } private boolean matchPrimaryMediaSource(@NonNull String newPackageName, @NonNull String newClassName, @CarMediaManager.MediaSourceMode int mode) { synchronized (mLock) { if (mPrimaryMediaComponents[mode] != null && mPrimaryMediaComponents[mode].getPackageName().equals(newPackageName)) { // If the class name of currently active source is not specified, only checks // package name; otherwise checks both package name and class name. if (TextUtils.isEmpty(newClassName)) { return true; } else { return newClassName.equals(mPrimaryMediaComponents[mode].getClassName()); } } } return false; } /** * Returns {@code true} if the provided component has a valid {@link MediaBrowseService}. */ @VisibleForTesting public boolean isMediaService(@NonNull ComponentName componentName) { return getMediaService(componentName) != null; } /* * Gets the media service that matches the componentName for the current foreground user. */ private ComponentName getMediaService(@NonNull ComponentName componentName) { String packageName = componentName.getPackageName(); String className = componentName.getClassName(); PackageManager packageManager = mContext.getPackageManager(); Intent mediaIntent = new Intent(); mediaIntent.setPackage(packageName); mediaIntent.setAction(MediaBrowserService.SERVICE_INTERFACE); List mediaServices = packageManager.queryIntentServicesAsUser(mediaIntent, PackageManager.GET_RESOLVED_FILTER, ActivityManager.getCurrentUser()); for (ResolveInfo service : mediaServices) { String serviceName = service.serviceInfo.name; if (!TextUtils.isEmpty(serviceName) // If className is not specified, returns the first service in the package; // otherwise returns the matched service. // TODO(b/136274456): find a proper way to handle the case where there are // multiple services and the className is not specified. && (TextUtils.isEmpty(className) || serviceName.equals(className))) { return new ComponentName(packageName, serviceName); } } if (Log.isLoggable(CarLog.TAG_MEDIA, Log.DEBUG)) { Slog.d(CarLog.TAG_MEDIA, "No MediaBrowseService with ComponentName: " + componentName.flattenToString()); } return null; } /* * Gets the component name of the media service. */ @Nullable private ComponentName getMediaSource(@NonNull String packageName, @NonNull String className) { return getMediaService(new ComponentName(packageName, className)); } private void saveLastMediaSource(@NonNull ComponentName component, int mode) { if (!sharedPrefsInitialized()) { return; } String componentName = component.flattenToString(); String key = getMediaSourceKey(mode); String serialized = mSharedPrefs.getString(key, null); if (serialized == null) { mSharedPrefs.edit().putString(key, componentName).apply(); } else { Deque componentNames = new ArrayDeque<>(getComponentNameList(serialized)); componentNames.remove(componentName); componentNames.addFirst(componentName); mSharedPrefs.edit().putString(key, serializeComponentNameList(componentNames)).apply(); } } private @NonNull ComponentName getLastMediaSource(int mode) { if (sharedPrefsInitialized()) { String key = getMediaSourceKey(mode); String serialized = mSharedPrefs.getString(key, ""); if (!TextUtils.isEmpty(serialized)) { for (String name : getComponentNameList(serialized)) { ComponentName componentName = ComponentName.unflattenFromString(name); if (isMediaService(componentName)) { return componentName; } } } } return getDefaultMediaSource(); } private ComponentName getDefaultMediaSource() { String defaultMediaSource = mContext.getString(R.string.config_defaultMediaSource); ComponentName defaultComponent = ComponentName.unflattenFromString(defaultMediaSource); if (isMediaService(defaultComponent)) { return defaultComponent; } return null; } private String serializeComponentNameList(Deque componentNames) { return componentNames.stream().collect(Collectors.joining(COMPONENT_NAME_SEPARATOR)); } private List getComponentNameList(@NonNull String serialized) { String[] componentNames = serialized.split(COMPONENT_NAME_SEPARATOR); return (Arrays.asList(componentNames)); } private void savePlaybackState(PlaybackState playbackState) { if (!sharedPrefsInitialized()) { return; } if (isCurrentUserEphemeral()) { return; } int state = playbackState != null ? playbackState.getState() : PlaybackState.STATE_NONE; synchronized (mLock) { mCurrentPlaybackState = state; } String key = getPlaybackStateKey(); mSharedPrefs.edit().putInt(key, state).apply(); } /** * Builds a string key for saving the playback state for a specific media source (and user) */ private String getPlaybackStateKey() { synchronized (mLock) { return PLAYBACK_STATE_KEY + ActivityManager.getCurrentUser() + (mPrimaryMediaComponents[MEDIA_SOURCE_MODE_PLAYBACK] == null ? "" : mPrimaryMediaComponents[MEDIA_SOURCE_MODE_PLAYBACK].flattenToString()); } } private String getMediaSourceKey(int mode) { return SOURCE_KEY + mode + SOURCE_KEY_SEPARATOR + ActivityManager.getCurrentUser(); } /** * Updates active media controller from the list that has the same component name as the primary * media component. Clears callback and resets media controller to null if not found. */ private void updateActiveMediaControllerLocked(List mediaControllers) { if (mPrimaryMediaComponents[MEDIA_SOURCE_MODE_PLAYBACK] == null) { return; } if (mActiveUserMediaController != null) { mActiveUserMediaController.unregisterCallback(mMediaControllerCallback); mActiveUserMediaController = null; } for (MediaController controller : mediaControllers) { if (matchPrimaryMediaSource(controller.getPackageName(), getClassName(controller), MEDIA_SOURCE_MODE_PLAYBACK)) { mActiveUserMediaController = controller; PlaybackState state = mActiveUserMediaController.getPlaybackState(); savePlaybackState(state); // Specify Handler to receive callbacks on, to avoid defaulting to the calling // thread; this method can be called from the MediaSessionManager callback. // Using the version of this method without passing a handler causes a // RuntimeException for failing to create a Handler. mActiveUserMediaController.registerCallback(mMediaControllerCallback, mHandler); return; } } } /** * Returns whether we should autoplay the current media source */ private boolean shouldStartPlayback(int config) { switch (config) { case AUTOPLAY_CONFIG_NEVER: return false; case AUTOPLAY_CONFIG_ALWAYS: return true; case AUTOPLAY_CONFIG_RETAIN_PER_SOURCE: if (!sharedPrefsInitialized()) { return false; } return mSharedPrefs.getInt(getPlaybackStateKey(), PlaybackState.STATE_NONE) == PlaybackState.STATE_PLAYING; case AUTOPLAY_CONFIG_RETAIN_PREVIOUS: synchronized (mLock) { return mCurrentPlaybackState == PlaybackState.STATE_PLAYING; } default: Slog.e(CarLog.TAG_MEDIA, "Unsupported playback configuration: " + config); return false; } } @NonNull private static String getClassName(@NonNull MediaController controller) { Bundle sessionExtras = controller.getExtras(); String value = sessionExtras == null ? "" : sessionExtras.getString( Car.CAR_EXTRA_BROWSE_SERVICE_FOR_SESSION); return value != null ? value : ""; } }