/* * 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.CarOccupantZoneManager.INVALID_USER_ID; import static android.car.CarOccupantZoneManager.OccupantZoneInfo.INVALID_ZONE_ID; import static android.car.builtin.os.UserManagerHelper.getMaxRunningUsers; import static android.car.media.CarMediaIntents.EXTRA_MEDIA_COMPONENT; import static android.car.media.CarMediaManager.MEDIA_SOURCE_MODE_BROWSE; import static android.car.media.CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK; import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_INVISIBLE; import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_SWITCHING; import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_UNLOCKED; import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_VISIBLE; import static com.android.car.CarServiceUtils.assertPermission; import static com.android.car.CarServiceUtils.getCommonHandlerThread; import static com.android.car.CarServiceUtils.getHandlerThread; import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.TestApi; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.usage.UsageStatsManager; import android.car.Car; import android.car.builtin.util.Slogf; import android.car.builtin.util.TimeUtils; import android.car.builtin.util.UsageStatsManagerHelper; 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.MediaSourceMode; import android.car.media.ICarMedia; import android.car.media.ICarMediaSourceListener; import android.car.user.CarUserManager.UserLifecycleListener; import android.car.user.UserLifecycleEventFilter; 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.HandlerThread; import android.os.PersistableBundle; 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.Log; import android.util.SparseArray; import android.util.proto.ProtoOutputStream; import android.view.KeyEvent; import com.android.car.CarInputService.KeyEventListener; import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; import com.android.car.internal.os.HandlerExecutor; import com.android.car.internal.util.DebugUtils; import com.android.car.internal.util.IndentingPrintWriter; import com.android.car.power.CarPowerManagementService; import com.android.car.user.CarUserService; import com.android.car.user.UserHandleHelper; 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.Collections; import java.util.Deque; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 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 final class CarMediaService extends ICarMedia.Stub implements CarServiceBase { private static final String TAG = CarLog.TAG_MEDIA; private static final boolean DEBUG = Slogf.isLoggable(TAG, Log.DEBUG); 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 String LAST_UPDATE_KEY = "last_update"; 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 CarOccupantZoneService mOccupantZoneService; private final CarUserService mUserService; private final CarPowerManagementService mPowerManagementService; private final UserManager mUserManager; private final MediaSessionManager mMediaSessionManager; private final UsageStatsManager mUsageStatsManager; /** * An array to store all per-user media data. * *

In most cases there will be one entry for the current user. * On a {@link UserManager#isUsersOnSecondaryDisplaysSupported() MUMD} (multi-user * multi-display) device, there will be multiple entries, one per each visible user. */ // TODO(b/262734537) Specify the initial capacity. @GuardedBy("mLock") private final SparseArray mUserMediaPlayContexts; // NOTE: must use getSharedPrefsForWriting() to write to it private SharedPreferences mSharedPrefs; private int mPlayOnMediaSourceChangedConfig; private int mPlayOnBootConfig; private boolean mDefaultIndependentPlaybackConfig; private final Handler mCommonThreadHandler = new Handler( getCommonHandlerThread().getLooper()); private final HandlerThread mHandlerThread = 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(); private final IntentFilter mPackageUpdateFilter; /** * Listens to {@link Intent#ACTION_PACKAGE_REMOVED}, so we can fall back to a previously used * media source when the active source is uninstalled. */ // TODO(b/262734537) Refactor this receiver using PackageMonitor. 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 (DEBUG) { Slogf.d(TAG, "Received a package update for package: %s, action: %s", intentPackage, intent.getAction()); } if (Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) { synchronized (mLock) { int userArraySize = mUserMediaPlayContexts.size(); for (int i = 0; i < userArraySize; i++) { int userId = mUserMediaPlayContexts.keyAt(i); UserMediaPlayContext userMediaContext = mUserMediaPlayContexts.valueAt(i); ComponentName[] primaryComponents = userMediaContext.mPrimaryMediaComponents; for (int j = 0; j < MEDIA_SOURCE_MODES; j++) { if (primaryComponents[j] != null && primaryComponents[j].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 : getLastMediaSourcesInternal(j, userId)) { if (!primaryComponents[j].getPackageName() .equals(component.getPackageName())) { userMediaContext.mRemovedMediaSourceComponents[j] = primaryComponents[j]; if (DEBUG) { Slogf.d(TAG, "temporarily replacing updated media " + "source %s for user %d with " + "backup source: %s", primaryComponents[j], userId, component); } setPrimaryMediaSource(component, j, userId); return; } } Slogf.e(TAG, "No available backup media source for user %d", userId); } else { if (DEBUG) { Slogf.d(TAG, "replacing removed media source" + " %s with backup source: %s for user %d", primaryComponents[j], getLastMediaSource(j, userId), userId); } userMediaContext.mRemovedMediaSourceComponents[j] = null; setPrimaryMediaSource(getLastMediaSource(j, userId), j, userId); } } } } } } else if (Intent.ACTION_PACKAGE_REPLACED.equals(intent.getAction()) || Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction())) { synchronized (mLock) { int userArraySize = mUserMediaPlayContexts.size(); for (int i = 0; i < userArraySize; i++) { int userId = mUserMediaPlayContexts.keyAt(i); ComponentName[] removedComponents = getRemovedMediaSourceComponentsForUser(userId); for (int j = 0; j < MEDIA_SOURCE_MODES; j++) { if (removedComponents[j] != null && removedComponents[j] .getPackageName().equals(intentPackage)) { if (DEBUG) { Slogf.d(TAG, "restoring removed source: %s for user %d", removedComponents[j], userId); } setPrimaryMediaSource(removedComponents[j], j, userId); } } } } } } }; private final UserLifecycleListener mUserLifecycleListener = event -> { if (DEBUG) { Slogf.d(TAG, "CarMediaService.onEvent(%s)", event); } // Note that we receive different event types based on the platform version, beacause of // the way we build the filter when registering the listener. // // Before U: // Receives USER_SWITCHING and USER UNLOCKED // U and after: // Receives USER_VISIBLE, USER_INVISIBLE, and USER_UNLOCKED // // See the constructor of this class to see how the UserLifecycleEventFilter is built // differently based on the platform version. switch (event.getEventType()) { case USER_LIFECYCLE_EVENT_TYPE_SWITCHING: onUserSwitch(event.getPreviousUserId(), event.getUserId()); break; case USER_LIFECYCLE_EVENT_TYPE_VISIBLE: onUserVisible(event.getUserId()); break; case USER_LIFECYCLE_EVENT_TYPE_UNLOCKED: onUserUnlocked(event.getUserId()); break; case USER_LIFECYCLE_EVENT_TYPE_INVISIBLE: onUserInvisible(event.getUserId()); break; default: 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); Slogf.i(TAG, "onPolicyChanged(); MEDIA is " + (isOff ? "OFF" : "ON")); synchronized (mLock) { int userArraySize = mUserMediaPlayContexts.size(); // Apply power policy to all users. for (int i = 0; i < userArraySize; i++) { int userId = mUserMediaPlayContexts.keyAt(i); UserMediaPlayContext userMediaContext = mUserMediaPlayContexts.valueAt(i); boolean isUserPlaying = (userMediaContext.mCurrentPlaybackState == PlaybackState.STATE_PLAYING); userMediaContext.mIsDisabledByPowerPolicy = isOff; if (isOff) { if (!userMediaContext.mWasPreviouslyDisabledByPowerPolicy) { // We're disabling media component. // Remember if we are playing at this transition. userMediaContext.mWasPlayingBeforeDisabled = isUserPlaying; userMediaContext.mWasPreviouslyDisabledByPowerPolicy = true; } shouldBePlaying = false; } else { userMediaContext.mWasPreviouslyDisabledByPowerPolicy = false; shouldBePlaying = userMediaContext.mWasPlayingBeforeDisabled; } if (shouldBePlaying == isUserPlaying) { return; } ComponentName source = userMediaContext.mPrimaryMediaComponents[ MEDIA_SOURCE_MODE_PLAYBACK]; try { if (DEBUG) { Slogf.d(TAG, "Starting media connector service from power " + "policy listener. source:%s, playing:%b, userId:%d", source, shouldBePlaying, userId); } startMediaConnectorServiceLocked( source, shouldBePlaying, userId); } catch (Exception e) { Slogf.e(TAG, e, "onPolicyChanged(): Failed to " + "startMediaConnectorService. Source:%s user:%d", source, userId); } // Should still pause the media when shouldBePlaying is false. if (!shouldBePlaying) { mediaController = userMediaContext.mActiveMediaController; if (mediaController == null) { if (DEBUG) { Slogf.d(TAG, "No active media controller for user %d. " + "Power policy change does not affect this user's " + "media.", userId); } return; } mediaController.getTransportControls().pause(); } } } } }; private final UserHandleHelper mUserHandleHelper; private static final PersistableBundle USER_INTERACTION_EXTRAS; static { USER_INTERACTION_EXTRAS = new PersistableBundle(); USER_INTERACTION_EXTRAS.putString(UsageStatsManager.EXTRA_EVENT_CATEGORY, CarMediaService.class.getCanonicalName()); USER_INTERACTION_EXTRAS.putString(UsageStatsManager.EXTRA_EVENT_ACTION, "setPrimaryMediaSource"); } public CarMediaService(Context context, CarOccupantZoneService occupantZoneService, CarUserService userService, CarPowerManagementService powerManagementService) { this(context, occupantZoneService, userService, powerManagementService, new UserHandleHelper(context, context.getSystemService(UserManager.class))); } @VisibleForTesting public CarMediaService(Context context, CarOccupantZoneService occupantZoneService, CarUserService userService, CarPowerManagementService powerManagementService, @NonNull UserHandleHelper userHandleHelper) { mContext = context; mUserManager = mContext.getSystemService(UserManager.class); mMediaSessionManager = mContext.getSystemService(MediaSessionManager.class); mUsageStatsManager = mContext.getSystemService(UsageStatsManager.class); mDefaultIndependentPlaybackConfig = mContext.getResources().getBoolean( R.bool.config_mediaSourceIndependentPlayback); mUserMediaPlayContexts = new SparseArray(getMaxRunningUsers(context)); mPackageUpdateFilter = new IntentFilter(); mPackageUpdateFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); mPackageUpdateFilter.addAction(Intent.ACTION_PACKAGE_REPLACED); mPackageUpdateFilter.addAction(Intent.ACTION_PACKAGE_ADDED); mPackageUpdateFilter.addDataScheme("package"); mOccupantZoneService = occupantZoneService; mUserService = userService; mPowerManagementService = powerManagementService; // Before U, only listen to USER_SWITCHING and USER_UNLOCKED. // U and after, only listen to USER_VISIBLE, USER_INVISIBLE, and USER_UNLOCKED. UserLifecycleEventFilter.Builder userLifecycleEventFilterBuilder = new UserLifecycleEventFilter.Builder() .addEventType(USER_LIFECYCLE_EVENT_TYPE_UNLOCKED); userLifecycleEventFilterBuilder.addEventType(USER_LIFECYCLE_EVENT_TYPE_INVISIBLE) .addEventType(USER_LIFECYCLE_EVENT_TYPE_VISIBLE); mUserService.addUserLifecycleListener(userLifecycleEventFilterBuilder.build(), mUserLifecycleListener); mPlayOnMediaSourceChangedConfig = mContext.getResources().getInteger(R.integer.config_mediaSourceChangedAutoplay); mPlayOnBootConfig = mContext.getResources().getInteger(R.integer.config_mediaBootAutoplay); mUserHandleHelper = userHandleHelper; } @Override // This method is called from ICarImpl after CarMediaService is created. public void init() { int currentUserId = ActivityManager.getCurrentUser(); if (DEBUG) { Slogf.d(TAG, "init(): currentUser=%d", currentUserId); } // Initialize media service for the current user. maybeInitUser(currentUserId); setKeyEventListener(); setPowerPolicyListener(); } private void maybeInitUser(@UserIdInt int userId) { if (userId == UserHandle.SYSTEM.getIdentifier() && UserManager.isHeadlessSystemUserMode()) { if (DEBUG) { Slogf.d(TAG, "maybeInitUser(%d): No need to initialize for the" + " headless system user", userId); } return; } if (mUserManager.isUserUnlocked(UserHandle.of(userId))) { initUser(userId); } else { synchronized (mLock) { getOrCreateUserMediaPlayContextLocked(userId).mPendingInit = true; } } } /** Initializes car media service data for the specified user. */ private void initUser(@UserIdInt int userId) { if (DEBUG) { Slogf.d(TAG, "initUser(): userId=%d, mSharedPrefs=%s", userId, mSharedPrefs); } UserHandle userHandle = UserHandle.of(userId); maybeInitSharedPrefs(userId); ComponentName playbackSource; synchronized (mLock) { UserMediaPlayContext userMediaContext = getOrCreateUserMediaPlayContextLocked(userId); if (userMediaContext.mContext != null) { userMediaContext.mContext.unregisterReceiver(mPackageUpdateReceiver); } userMediaContext.mContext = mContext.createContextAsUser(userHandle, /* flags= */ 0); userMediaContext.mContext.registerReceiver(mPackageUpdateReceiver, mPackageUpdateFilter, Context.RECEIVER_NOT_EXPORTED); boolean isEphemeral = isUserEphemeral(userHandle); if (isEphemeral) { ComponentName defaultMediaSource = getDefaultMediaSource(userId); userMediaContext.mPrimaryMediaComponents[MEDIA_SOURCE_MODE_PLAYBACK] = defaultMediaSource; userMediaContext.mPrimaryMediaComponents[MEDIA_SOURCE_MODE_BROWSE] = defaultMediaSource; playbackSource = defaultMediaSource; } else { playbackSource = getLastMediaSource(MEDIA_SOURCE_MODE_PLAYBACK, userId); userMediaContext.mPrimaryMediaComponents[MEDIA_SOURCE_MODE_PLAYBACK] = playbackSource; userMediaContext.mPrimaryMediaComponents[MEDIA_SOURCE_MODE_BROWSE] = getLastMediaSource(MEDIA_SOURCE_MODE_BROWSE, userId); } userMediaContext.mActiveMediaController = null; updateMediaSessionCallbackForUserLocked(userHandle); } notifyListeners(MEDIA_SOURCE_MODE_PLAYBACK, userId); notifyListeners(MEDIA_SOURCE_MODE_BROWSE, userId); boolean shouldPlay = shouldStartPlayback(mPlayOnBootConfig, userId); if (isMediaPowerEnabled()) { try { startMediaConnectorService(playbackSource, shouldPlay, userId); } catch (Exception e) { Slogf.e(TAG, e, "Failed to startMediaConnectorService. Source:%s user:%d", playbackSource, userId); } } else { synchronized (mLock) { UserMediaPlayContext userMediaContext = getOrCreateUserMediaPlayContextLocked(userId); // When media is disabled by power policy, mark the necessary fields such that later // the media play can be resumed when power policy is enabled. userMediaContext.mWasPlayingBeforeDisabled = shouldPlay; userMediaContext.mWasPreviouslyDisabledByPowerPolicy = true; } } } @GuardedBy("mLock") private ComponentName[] getPrimaryMediaComponentsForUserLocked(@UserIdInt int userId) { UserMediaPlayContext userMediaContext = getOrCreateUserMediaPlayContextLocked(userId); return userMediaContext.mPrimaryMediaComponents; } private ComponentName[] getRemovedMediaSourceComponentsForUser(@UserIdInt int userId) { synchronized (mLock) { UserMediaPlayContext userMediaContext = getOrCreateUserMediaPlayContextLocked(userId); return userMediaContext.mRemovedMediaSourceComponents; } } private void maybeInitSharedPrefs(@UserIdInt 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. if (mSharedPrefs != null) { Slogf.i(TAG, "Shared preferences already set (on directory %s)" + " when initializing user %d", mContext.getDataDir(), userId); return; } Slogf.i(TAG, "Getting shared preferences when initializing user %d", userId); mSharedPrefs = mContext.getSharedPreferences(SHARED_PREF, Context.MODE_PRIVATE); // Try to access the properties to make sure they were properly open if (DEBUG) { Slogf.d(TAG, "Number of prefs: %d", mSharedPrefs.getAll().size()); } } /** Checks if {@link PowerComponent.MEDIA} is currently enabled. */ private boolean isMediaPowerEnabled() { CarPowerPolicy currentPolicy = mPowerManagementService.getCurrentPowerPolicy(); return currentPolicy.isComponentEnabled(PowerComponent.MEDIA); } /** * 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(@Nullable ComponentName playbackMediaSource, boolean startPlayback, @UserIdInt int userId) { synchronized (mLock) { startMediaConnectorServiceLocked(playbackMediaSource, startPlayback, userId); } } @GuardedBy("mLock") private void startMediaConnectorServiceLocked(@Nullable ComponentName playbackMediaSource, boolean startPlayback, @UserIdInt int userId) { UserMediaPlayContext userMediaPlayContext = getOrCreateUserMediaPlayContextLocked(userId); Context userContext = userMediaPlayContext.mContext; if (userContext == null) { Slogf.wtf(TAG, "Cannot start MediaConnection service. User %d has not been initialized", userId); return; } Intent serviceStart = new Intent(MEDIA_CONNECTION_ACTION); serviceStart.setPackage( mContext.getResources().getString(R.string.serviceMediaConnection)); serviceStart.putExtra(EXTRA_AUTOPLAY, startPlayback); if (playbackMediaSource != null) { serviceStart.putExtra(EXTRA_MEDIA_COMPONENT, playbackMediaSource.flattenToString()); } ComponentName result = userContext.startForegroundService(serviceStart); userMediaPlayContext.mStartedMediaConnectorService = result; Slogf.i(TAG, "startMediaConnectorService user: %d, source: %s, result: %s", userId, playbackMediaSource, result); } private boolean sharedPrefsInitialized() { if (mSharedPrefs != null) return true; // It shouldn't reach this but let's be cautious. Slogf.e(TAG, "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)) { Slogf.e(TAG, log); } } return false; } private boolean isUserEphemeral(UserHandle userHandle) { return mUserHandleHelper.isEphemeralUser(userHandle); } private void setKeyEventListener() { int maxKeyCode = KeyEvent.getMaxKeyCode(); ArrayList mediaKeyCodes = new ArrayList<>(15); for (int key = 1; key <= maxKeyCode; key++) { if (KeyEvent.isMediaSessionKey(key)) { mediaKeyCodes.add(key); } } CarLocalServices.getService(CarInputService.class) .registerKeyEventListener(new MediaKeyEventListener(), mediaKeyCodes); } // 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(); mPowerManagementService.addPowerPolicyListener(filter, mPowerPolicyListener); } @Override public void release() { synchronized (mLock) { int userArraySize = mUserMediaPlayContexts.size(); for (int i = 0; i < userArraySize; i++) { clearUserDataLocked(mUserMediaPlayContexts.keyAt(i)); } } mUserService.removeUserLifecycleListener(mUserLifecycleListener); mPowerManagementService.removePowerPolicyListener(mPowerPolicyListener); } /** Clears the user data for {@code userId}. */ @GuardedBy("mLock") private void clearUserDataLocked(@UserIdInt int userId) { if (DEBUG) { Slogf.d(TAG, "clearUserDataLocked() for user %d", userId); } UserMediaPlayContext userMediaContext = mUserMediaPlayContexts.get(userId); if (userMediaContext == null) { return; } if (userMediaContext.mContext != null) { userMediaContext.mContext.unregisterReceiver(mPackageUpdateReceiver); userMediaContext.mContext = null; } userMediaContext.mMediaSessionUpdater.unregisterCallbacks(); mMediaSessionManager.removeOnActiveSessionsChangedListener( userMediaContext.mSessionsListener); } @Override @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) public void dump(IndentingPrintWriter writer) { writer.println("*CarMediaService*"); writer.increaseIndent(); writer.printf("DEBUG=%b\n", DEBUG); writer.printf("mPlayOnBootConfig=%d\n", mPlayOnBootConfig); writer.printf("mPlayOnMediaSourceChangedConfig=%d\n", mPlayOnMediaSourceChangedConfig); writer.printf("mDefaultIndependentPlaybackConfig=%b\n", mDefaultIndependentPlaybackConfig); writer.println(); boolean hasSharedPrefs = mSharedPrefs != null; synchronized (mLock) { int userArraySize = mUserMediaPlayContexts.size(); for (int i = 0; i < userArraySize; i++) { int userId = mUserMediaPlayContexts.keyAt(i); writer.printf("For user %d:\n", userId); writer.increaseIndent(); UserMediaPlayContext userMediaContext = mUserMediaPlayContexts.valueAt(i); writer.printf("Pending init: %b\n", userMediaContext.mPendingInit); writer.printf("MediaConnectorService: %s\n", userMediaContext.mStartedMediaConnectorService != null ? userMediaContext.mStartedMediaConnectorService .flattenToShortString() : ""); dumpCurrentMediaComponentLocked(writer, "playback", MEDIA_SOURCE_MODE_PLAYBACK, userId); dumpCurrentMediaComponentLocked(writer, "browse", MEDIA_SOURCE_MODE_BROWSE, userId); MediaController mediaController = userMediaContext.mActiveMediaController; if (mediaController != null) { writer.printf("Current media controller: %s\n", mediaController.getPackageName()); writer.printf("Current browse service extra: %s\n", getClassName(mediaController)); } else { writer.println("no active user media controller"); } writer.printf("Number of active media sessions (for user %d): %d\n", userId, mMediaSessionManager.getActiveSessionsForUser( /* notificationListener= */ null, UserHandle.of(userId)).size()); writer.printf("Disabled by power policy: %b\n", userMediaContext.mIsDisabledByPowerPolicy); if (userMediaContext.mIsDisabledByPowerPolicy) { writer.printf("Before being disabled by power policy, audio was %s\n", userMediaContext.mWasPlayingBeforeDisabled ? "active" : "inactive"); } if (hasSharedPrefs) { dumpLastUpdateTime(writer, userId); dumpLastMediaSources(writer, "Playback", MEDIA_SOURCE_MODE_PLAYBACK, userId); dumpLastMediaSources(writer, "Browse", MEDIA_SOURCE_MODE_BROWSE, userId); dumpPlaybackState(writer, userId); } writer.decreaseIndent(); } } if (hasSharedPrefs) { dumpSharedPrefs(writer); } else { writer.println("No shared preferences"); } writer.decreaseIndent(); } @Override @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) public void dumpProto(ProtoOutputStream proto) {} @GuardedBy("mLock") @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) private void dumpCurrentMediaComponentLocked(IndentingPrintWriter writer, String name, @CarMediaManager.MediaSourceMode int mode, @UserIdInt int userId) { ComponentName componentName = getPrimaryMediaComponentsForUserLocked(userId)[mode]; writer.printf("For user %d, current %s media component: %s\n", userId, name, (componentName == null ? "-" : componentName.flattenToString())); } @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) private void dumpLastUpdateTime(IndentingPrintWriter writer, @UserIdInt int userId) { long lastUpdate = mSharedPrefs.getLong(getLastUpdateKey(userId), -1); writer.printf("For user %d, shared preference last updated on %d / ", userId, lastUpdate); TimeUtils.dumpTime(writer, lastUpdate); writer.println(); } @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) private void dumpLastMediaSources(IndentingPrintWriter writer, String name, @CarMediaManager.MediaSourceMode int mode, @UserIdInt int userId) { writer.printf("%s media source history:\n", name); writer.increaseIndent(); List lastMediaSources = getLastMediaSourcesInternal(mode, userId); for (int i = 0; i < lastMediaSources.size(); i++) { ComponentName componentName = lastMediaSources.get(i); if (componentName == null) { Slogf.e(TAG, "dump(): for user %d, empty last media source of %s" + " at index %d: %s", userId, mediaModeToString(mode), i, lastMediaSources); continue; } writer.println(componentName.flattenToString()); } writer.decreaseIndent(); } @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) private void dumpPlaybackState(IndentingPrintWriter writer, @UserIdInt int userId) { String key = getPlaybackStateKey(userId); int playbackState = mSharedPrefs.getInt(key, PlaybackState.STATE_NONE); writer.printf("media playback state: %d\n", playbackState); } @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) private void dumpSharedPrefs(IndentingPrintWriter writer) { Map allPrefs = mSharedPrefs.getAll(); writer.printf("%d shared preferences (saved on directory %s)", allPrefs.size(), mContext.getDataDir()); if (!Slogf.isLoggable(TAG, Log.VERBOSE) || allPrefs.isEmpty()) { writer.println(); return; } writer.println(':'); } /** * @see CarMediaManager#setMediaSource(ComponentName, int) */ @Override public void setMediaSource(@NonNull ComponentName componentName, @MediaSourceMode int mode, @UserIdInt int userId) { enforceValidCallingUser(userId); assertPermission(mContext, android.Manifest.permission.MEDIA_CONTENT_CONTROL); if (DEBUG) { Slogf.d(TAG, "Changing media source to: %s for user %d", componentName.getPackageName(), userId); } setPrimaryMediaSource(componentName, mode, userId); } /** * @see CarMediaManager#getMediaSource */ @Override public ComponentName getMediaSource(@CarMediaManager.MediaSourceMode int mode, @UserIdInt int userId) { enforceValidCallingUser(userId); assertPermission(mContext, android.Manifest.permission.MEDIA_CONTENT_CONTROL); ComponentName componentName; synchronized (mLock) { componentName = getPrimaryMediaComponentsForUserLocked(userId)[mode]; } if (componentName == null) { componentName = getDefaultMediaSource(userId); Slogf.e(TAG, "User %d has not been initialized. Returning the default media " + "source %s.", userId, componentName); } if (DEBUG) { Slogf.d(TAG, "Getting media source mode %d for user %d: %s ", mode, userId, componentName); } return componentName; } /** * @see * CarMediaManager#removeMediaSourceListener(CarMediaManager.MediaSourceChangedListener, int) */ @Override public void registerMediaSourceListener(ICarMediaSourceListener callback, @MediaSourceMode int mode, @UserIdInt int userId) { enforceValidCallingUser(userId); assertPermission(mContext, android.Manifest.permission.MEDIA_CONTENT_CONTROL); UserMediaPlayContext userMediaContext; synchronized (mLock) { userMediaContext = getOrCreateUserMediaPlayContextLocked(userId); } userMediaContext.mMediaSourceListeners[mode].register(callback); } /** * @see CarMediaManager#removeMediaSourceListener */ @Override public void unregisterMediaSourceListener(ICarMediaSourceListener callback, @MediaSourceMode int mode, @UserIdInt int userId) { enforceValidCallingUser(userId); assertPermission(mContext, android.Manifest.permission.MEDIA_CONTENT_CONTROL); UserMediaPlayContext userMediaContext; synchronized (mLock) { userMediaContext = getOrCreateUserMediaPlayContextLocked(userId); } userMediaContext.mMediaSourceListeners[mode].unregister(callback); } @Override public List getLastMediaSources(@CarMediaManager.MediaSourceMode int mode, @UserIdInt int userId) { enforceValidCallingUser(userId); assertPermission(mContext, android.Manifest.permission.MEDIA_CONTENT_CONTROL); ArrayList results = getLastMediaSourcesInternal(mode, userId); // Always add the default media source at the end, if it is not already in the list. // This is to match the results of getLastMediaSources() with what getMediaSource() returns. // For example, when a user is first initialized and no media source has been stored yet, // getLastMediaSources() will return the default media source, instead of an empty list. ComponentName defaultMediaSource = getDefaultMediaSource(userId); if (defaultMediaSource != null && results.indexOf(defaultMediaSource) < 0) { results.add(defaultMediaSource); } return results; } /** * Returns the last media sources for the specified {@code mode} and {@code userId}. * *

Anywhere in this file, do not use the public {@link #getLastMediaSources(int, int)} * method. Use this method instead. */ private ArrayList getLastMediaSourcesInternal( @CarMediaManager.MediaSourceMode int mode, @UserIdInt int userId) { String key = getMediaSourceKey(mode, userId); String serialized = mSharedPrefs == null ? null : mSharedPrefs.getString(key, ""); List componentNames = getComponentNameList(serialized); // Set the initial capacity to componentNames.size() + 1, to avoid re-allocation in case // the default media source needs to be added by getLastMediaSources(). ArrayList results = new ArrayList<>(componentNames.size() + 1); for (int i = 0; i < componentNames.size(); i++) { results.add(ComponentName.unflattenFromString(componentNames.get(i))); } return results; } /** See {@link CarMediaManager#isIndependentPlaybackConfig}. */ @Override @TestApi public boolean isIndependentPlaybackConfig(@UserIdInt int userId) { enforceValidCallingUser(userId); assertPermission(mContext, android.Manifest.permission.MEDIA_CONTENT_CONTROL); return isIndependentPlaybackConfigInternal(userId); } /** * Returns independent playback config for the specified {@code userId}. * *

Anywhere in this file, do not use the public {@link isIndependentPlaybackConfig(int)} * method. */ private boolean isIndependentPlaybackConfigInternal(@UserIdInt int userId) { synchronized (mLock) { return getOrCreateUserMediaPlayContextLocked(userId).mIndependentPlaybackConfig; } } /** See {@link CarMediaManager#setIndependentPlaybackConfig}. */ @Override @TestApi public void setIndependentPlaybackConfig(boolean independent, @UserIdInt int userId) { enforceValidCallingUser(userId); assertPermission(mContext, android.Manifest.permission.MEDIA_CONTENT_CONTROL); synchronized (mLock) { getOrCreateUserMediaPlayContextLocked(userId).mIndependentPlaybackConfig = independent; } } private void enforceValidCallingUser(@UserIdInt int userId) throws SecurityException { UserHandle callingUser = getCallingUserHandle(); // The calling user is valid if it is the system user or the specified userId matches // the calling user. if (!callingUser.isSystem() && callingUser.getIdentifier() != userId) { throw new SecurityException("The calling user " + callingUser.getIdentifier() + " cannot access media data of user " + userId); } } /** Clears data for {@code fromUserId}, and initializes data for {@code toUserId}. */ private void onUserSwitch(@UserIdInt int fromUserId, @UserIdInt int toUserId) { if (DEBUG) { Slogf.d(TAG, "onUserSwitch() fromUserId=%d, toUserId=%d", fromUserId, toUserId); } // Clean up the data of the fromUser. if (fromUserId != UserHandle.SYSTEM.getIdentifier()) { onUserInvisible(fromUserId); } // Initialize the data of the toUser. onUserVisible(toUserId); } private void onUserVisible(@UserIdInt int userId) { if (DEBUG) { Slogf.d(TAG, "onUserVisible() for user=%d", userId); } maybeInitUser(userId); } /** Clears the user data when the user becomes invisible. */ private void onUserInvisible(@UserIdInt int userId) { if (DEBUG) { Slogf.d(TAG, "onUserInvisible(): userId=%d. Clearing data for the user.", userId); } synchronized (mLock) { clearUserDataLocked(userId); mUserMediaPlayContexts.delete(userId); } } // TODO(b/153115826): this method was used to be called from the ICar binder thread, but it's // now called by UserCarService. Currently UserCarService 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 onUserUnlocked(@UserIdInt int userId) { if (DEBUG) { Slogf.d(TAG, "onUserUnlocked(): userId=%d", userId); } mCommonThreadHandler.post(() -> { // No need to handle system user, but still need to handle background users. if (userId == UserHandle.SYSTEM.getIdentifier()) { return; } UserMediaPlayContext userMediaPlayContext; boolean isPendingInit; synchronized (mLock) { userMediaPlayContext = getOrCreateUserMediaPlayContextLocked(userId); isPendingInit = userMediaPlayContext.mPendingInit; } if (DEBUG) { Slogf.d(TAG, "onUserUnlocked(): userId=%d pendingInit=%b", userId, userMediaPlayContext.mPendingInit); } if (isPendingInit) { initUser(userId); synchronized (mLock) { userMediaPlayContext.mPendingInit = false; } if (DEBUG) { Slogf.d(TAG, "User %d is now unlocked", userId); } } }); } @GuardedBy("mLock") private void updateMediaSessionCallbackForUserLocked(UserHandle userHandle) { int userId = userHandle.getIdentifier(); UserMediaPlayContext userMediaContext = getOrCreateUserMediaPlayContextLocked(userId); SessionChangedListener sessionsListener = userMediaContext.mSessionsListener; if (sessionsListener != null) { mMediaSessionManager.removeOnActiveSessionsChangedListener(sessionsListener); } sessionsListener = new SessionChangedListener(userId); userMediaContext.mSessionsListener = sessionsListener; mMediaSessionManager.addOnActiveSessionsChangedListener(null, userHandle, new HandlerExecutor(mHandler), sessionsListener); MediaSessionUpdater sessionUpdater = new MediaSessionUpdater(userId); userMediaContext.mMediaSessionUpdater = sessionUpdater; sessionUpdater.registerCallbacks(mMediaSessionManager.getActiveSessionsForUser(null, userHandle)); } /** * 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(@UserIdInt int userId) { UserMediaPlayContext userMediaContext; MediaController mediaController; synchronized (mLock) { userMediaContext = getOrCreateUserMediaPlayContextLocked(userId); mediaController = userMediaContext.mActiveMediaController; } if (mediaController == null) { if (DEBUG) { Slogf.d(TAG, "stopAndUnregisterCallback() for user %d." + " Do nothing as there is no active media controller.", userId); } return; } mediaController.unregisterCallback(userMediaContext.mMediaControllerCallback); if (DEBUG) { Slogf.d(TAG, "stopping %s", mediaController.getPackageName()); } TransportControls controls = mediaController.getTransportControls(); if (controls != null) { // In order to prevent some apps from taking back the audio focus after being stopped, // first call pause, if the app supports pause. This does not affect the saved source // or the playback state, because the callback has already been unregistered. PlaybackState playbackState = mediaController.getPlaybackState(); if (playbackState != null && (playbackState.getActions() & PlaybackState.ACTION_PAUSE) != 0) { if (DEBUG) { Slogf.d(TAG, "Call pause before stop"); } controls.pause(); } controls.stop(); } else { Slogf.e(TAG, "Can't stop playback, transport controls unavailable %s", mediaController.getPackageName()); } } @GuardedBy("mLock") private UserMediaPlayContext getOrCreateUserMediaPlayContextLocked(@UserIdInt int userId) { UserMediaPlayContext userMediaContext = mUserMediaPlayContexts.get(userId); if (userMediaContext == null) { userMediaContext = new UserMediaPlayContext(userId, mDefaultIndependentPlaybackConfig); mUserMediaPlayContexts.set(userId, userMediaContext); if (DEBUG) { Slogf.d(TAG, "Create a UserMediaPlayContext for user %d", userId); } } return userMediaContext; } /** A container to store per-user media play context data. */ private final class UserMediaPlayContext { @Nullable private Context mContext; // MediaController for the user's active media session. This controller can be null // if playback has not been started yet. private MediaController mActiveMediaController; private int mCurrentPlaybackState; private boolean mIsDisabledByPowerPolicy; private boolean mWasPreviouslyDisabledByPowerPolicy; private boolean mWasPlayingBeforeDisabled; private boolean mIndependentPlaybackConfig; private final ComponentName[] mPrimaryMediaComponents; // The component name of the last media source that was removed while being primary. private final ComponentName[] mRemovedMediaSourceComponents; private boolean mPendingInit; // This field is used for test/debugging. private ComponentName mStartedMediaConnectorService; private final RemoteCallbackList[] mMediaSourceListeners; private MediaSessionUpdater mMediaSessionUpdater; private SessionChangedListener mSessionsListener; private final MediaController.Callback mMediaControllerCallback; UserMediaPlayContext(@UserIdInt int userId, boolean independentPlaybackConfig) { mIndependentPlaybackConfig = independentPlaybackConfig; mPrimaryMediaComponents = new ComponentName[MEDIA_SOURCE_MODES]; mRemovedMediaSourceComponents = new ComponentName[MEDIA_SOURCE_MODES]; mMediaSourceListeners = new RemoteCallbackList[] {new RemoteCallbackList(), new RemoteCallbackList()}; mMediaSessionUpdater = new MediaSessionUpdater(userId); mSessionsListener = new SessionChangedListener(userId); mMediaControllerCallback = new ActiveMediaControllerCallback(userId); } } private final class ActiveMediaControllerCallback extends MediaController.Callback { private final @UserIdInt int mUserId; ActiveMediaControllerCallback(@UserIdInt int userId) { mUserId = userId; } @Override public void onPlaybackStateChanged(PlaybackState state) { savePlaybackState(state, mUserId); } } private class SessionChangedListener implements OnActiveSessionsChangedListener { private final @UserIdInt int mUserId; SessionChangedListener(int userId) { mUserId = userId; } @Override public void onActiveSessionsChanged(List controllers) { if (DEBUG) { Slogf.d(TAG, "onActiveSessionsChanged() for user %d, controllers: %s", mUserId, controllers); } // Filter controllers based on their user ids. ArrayList userControllers = new ArrayList<>(controllers.size()); for (int i = 0; i < controllers.size(); i++) { MediaController controller = controllers.get(i); int userId = UserHandle.getUserHandleForUid( controller.getSessionToken().getUid()).getIdentifier(); if (userId == mUserId) { userControllers.add(controller); } else { Slogf.w(TAG, "onActiveSessionsChanged() received a change for " + "a different user: listener user %d, controller %s for user %d", mUserId, controller, userId); } } UserMediaPlayContext userMediaContext; synchronized (mLock) { userMediaContext = getOrCreateUserMediaPlayContextLocked(mUserId); } userMediaContext.mMediaSessionUpdater.registerCallbacks(userControllers); } } private class MediaControllerCallback extends MediaController.Callback { private final @UserIdInt int mUserId; private final MediaController mMediaController; private PlaybackState mPreviousPlaybackState; private MediaControllerCallback(MediaController mediaController, @UserIdInt int userId) { mUserId = userId; mMediaController = mediaController; PlaybackState state = mediaController.getPlaybackState(); mPreviousPlaybackState = state; } private void register() { mMediaController.registerCallback(this); } private void unregister() { mMediaController.unregisterCallback(this); } @Override public void onPlaybackStateChanged(@Nullable PlaybackState state) { if (DEBUG) { Slogf.d(TAG, "onPlaybackStateChanged() for user %d; previous state: %s," + " new state: %s", mUserId, mPreviousPlaybackState, state.getState()); } if (state != null && state.isActive() && (mPreviousPlaybackState == null || !mPreviousPlaybackState.isActive())) { ComponentName mediaSource = getMediaSource(mMediaController.getPackageName(), getClassName(mMediaController), mUserId); if (mediaSource != null && Slogf.isLoggable(TAG, Log.INFO)) { synchronized (mLock) { if (!mediaSource.equals(getPrimaryMediaComponentsForUserLocked(mUserId) [MEDIA_SOURCE_MODE_PLAYBACK])) { Slogf.i(TAG, "Changing media source for user %d due to playback state " + "change: %s", mUserId, mediaSource.flattenToString()); } } } setPrimaryMediaSource(mediaSource, MEDIA_SOURCE_MODE_PLAYBACK, mUserId); } mPreviousPlaybackState = state; } } private class MediaSessionUpdater { private final @UserIdInt int mUserId; private Map mCallbacks = new HashMap<>(); MediaSessionUpdater(@UserIdInt int userId) { mUserId = userId; } /** * 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, mUserId); 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, mUserId); // 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) { UserMediaPlayContext userMediaContext = getOrCreateUserMediaPlayContextLocked(mUserId); if (userMediaContext.mActiveMediaController == null) { updateActiveMediaControllerLocked(newControllers, mUserId); } } } /** * 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(ComponentName componentName, @CarMediaManager.MediaSourceMode int mode, @UserIdInt int userId) { if (DEBUG) { Slogf.d(TAG, "setPrimaryMediaSource(component=%s, mode=%d, userId=%d)", componentName, mode, userId); } ComponentName mediaComponent; synchronized (mLock) { mediaComponent = getPrimaryMediaComponentsForUserLocked(userId)[mode]; } if (mediaComponent != null && mediaComponent.equals((componentName))) { return; } if (!isIndependentPlaybackConfigInternal(userId)) { setPlaybackMediaSource(componentName, userId); setBrowseMediaSource(componentName, userId); } else if (mode == MEDIA_SOURCE_MODE_PLAYBACK) { setPlaybackMediaSource(componentName, userId); } else if (mode == MEDIA_SOURCE_MODE_BROWSE) { setBrowseMediaSource(componentName, userId); } // Android logs app usage into UsageStatsManager. ACTIVITY_RESUMED and ACTIVITY_STOPPED // events do not capture media app usage on AAOS because apps are hosted by a proxy such as // Media Center. Reporting a USER_INTERACTION event in setPrimaryMediaSource allows // attribution of non-foreground media app interactions to the app's package name if (componentName != null) { UsageStatsManagerHelper.reportUserInteraction(mUsageStatsManager, componentName.getPackageName(), userId, USER_INTERACTION_EXTRAS); } } private void setPlaybackMediaSource(ComponentName playbackMediaSource, @UserIdInt int userId) { stopAndUnregisterCallback(userId); UserMediaPlayContext userMediaContext; synchronized (mLock) { userMediaContext = getOrCreateUserMediaPlayContextLocked(userId); userMediaContext.mActiveMediaController = null; userMediaContext.mPrimaryMediaComponents[MEDIA_SOURCE_MODE_PLAYBACK] = playbackMediaSource; } UserHandle userHandle = UserHandle.of(userId); if (playbackMediaSource != null && !TextUtils.isEmpty(playbackMediaSource.flattenToString())) { if (!isUserEphemeral(userHandle)) { saveLastMediaSource(playbackMediaSource, MEDIA_SOURCE_MODE_PLAYBACK, userId); } if (playbackMediaSource .equals(getRemovedMediaSourceComponentsForUser(userId) [MEDIA_SOURCE_MODE_PLAYBACK])) { getRemovedMediaSourceComponentsForUser(userId)[MEDIA_SOURCE_MODE_PLAYBACK] = null; } notifyListeners(MEDIA_SOURCE_MODE_PLAYBACK, userId); startMediaConnectorService(playbackMediaSource, shouldStartPlayback(mPlayOnMediaSourceChangedConfig, userId), userId); } else { Slogf.i(TAG, "Media source is null for user %d, skip starting media " + "connector service", userId); // We will still notify the listeners that playback changed notifyListeners(MEDIA_SOURCE_MODE_PLAYBACK, userId); } // 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) { userMediaContext.mCurrentPlaybackState = PlaybackState.STATE_NONE; updateActiveMediaControllerLocked(mMediaSessionManager .getActiveSessionsForUser(null, userHandle), userId); } } private void setBrowseMediaSource(ComponentName browseMediaSource, @UserIdInt int userId) { synchronized (mLock) { getPrimaryMediaComponentsForUserLocked(userId)[MEDIA_SOURCE_MODE_BROWSE] = browseMediaSource; } if (browseMediaSource != null && !TextUtils.isEmpty(browseMediaSource.flattenToString())) { if (!isUserEphemeral(UserHandle.of(userId))) { saveLastMediaSource(browseMediaSource, MEDIA_SOURCE_MODE_BROWSE, userId); } if (browseMediaSource .equals(getRemovedMediaSourceComponentsForUser( userId)[MEDIA_SOURCE_MODE_BROWSE])) { getRemovedMediaSourceComponentsForUser(userId)[MEDIA_SOURCE_MODE_BROWSE] = null; } } notifyListeners(MEDIA_SOURCE_MODE_BROWSE, userId); } private void notifyListeners(@CarMediaManager.MediaSourceMode int mode, @UserIdInt int userId) { synchronized (mLock) { UserMediaPlayContext userMediaContext = getOrCreateUserMediaPlayContextLocked(userId); RemoteCallbackList callbackList = userMediaContext.mMediaSourceListeners[mode]; ComponentName primaryMediaComponent = userMediaContext.mPrimaryMediaComponents[mode]; int i = callbackList.beginBroadcast(); if (DEBUG) { Slogf.d(TAG, "Notify %d media source listeners for mode %d, user %d", i, mode, userId); } while (i-- > 0) { try { ICarMediaSourceListener callback = callbackList.getBroadcastItem(i); callback.onMediaSourceChanged(primaryMediaComponent); } catch (RemoteException e) { Slogf.e(TAG, e, "calling onMediaSourceChanged failed for user %d", userId); } } callbackList.finishBroadcast(); } } /** * Finds the currently playing media source, then updates the active source if the component * name is different. */ private void updatePrimaryMediaSourceWithCurrentlyPlaying( List controllers, @UserIdInt int userId) { for (MediaController controller : controllers) { PlaybackState state = controller.getPlaybackState(); if (state != null && state.isActive()) { String newPackageName = controller.getPackageName(); String newClassName = getClassName(controller); if (!matchPrimaryMediaSource(newPackageName, newClassName, MEDIA_SOURCE_MODE_PLAYBACK, userId)) { ComponentName mediaSource = getMediaSource(newPackageName, newClassName, userId); if (Slogf.isLoggable(TAG, Log.INFO)) { if (mediaSource != null) { Slogf.i(TAG, "MediaController changed, updating media source for user %d " + "to: %s", userId, mediaSource.flattenToString()); } else { // Some apps, like Chrome, have a MediaSession but no // MediaBrowseService. Media Center doesn't consider such apps as // valid media sources. Slogf.i(TAG, "MediaController changed, but no media browse service for user" + " %d found in package: %s", userId, newPackageName); } } setPrimaryMediaSource(mediaSource, MEDIA_SOURCE_MODE_PLAYBACK, userId); } return; } } } private boolean matchPrimaryMediaSource(String newPackageName, String newClassName, @CarMediaManager.MediaSourceMode int mode, @UserIdInt int userId) { synchronized (mLock) { ComponentName mediaComponent = getPrimaryMediaComponentsForUserLocked(userId)[mode]; if (mediaComponent != null && mediaComponent.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(mediaComponent.getClassName()); } } } return false; } /** * Returns {@code true} if the provided component has a valid {@link MediaBrowserService}. */ @VisibleForTesting public boolean isMediaService(ComponentName componentName, @UserIdInt int userId) { return getMediaService(componentName, userId) != null; } /* * Gets the media service that matches the componentName for the specified user. */ private ComponentName getMediaService(ComponentName componentName, @UserIdInt int userId) { 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, UserHandle.of(userId)); 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 (DEBUG) { Slogf.d(TAG, "No MediaBrowseService for user %d with ComponentName: %s", userId, componentName.flattenToString()); } return null; } /* * Gets the component name of the media service. */ @Nullable private ComponentName getMediaSource(String packageName, String className, @UserIdInt int userId) { return getMediaService(new ComponentName(packageName, className), userId); } private void saveLastMediaSource(ComponentName component, int mode, @UserIdInt int userId) { if (!sharedPrefsInitialized()) { return; } String componentName = component.flattenToString(); String key = getMediaSourceKey(mode, userId); String serialized = mSharedPrefs.getString(key, null); String modeName = null; if (DEBUG) { modeName = mediaModeToString(mode); } if (serialized == null) { if (DEBUG) { Slogf.d(TAG, "saveLastMediaSource(%s, %s, %d): no value for key %s", componentName, modeName, userId, key); } getSharedPrefsForWriting(userId).putString(key, componentName).apply(); } else { Deque componentNames = new ArrayDeque<>(getComponentNameList(serialized)); componentNames.remove(componentName); componentNames.addFirst(componentName); String newSerialized = serializeComponentNameList(componentNames); if (DEBUG) { Slogf.d(TAG, "saveLastMediaSource(%s, %s, %d): updating %s from %s to %s", componentName, modeName, userId, key, serialized, newSerialized); } getSharedPrefsForWriting(userId).putString(key, newSerialized).apply(); } } private @NonNull ComponentName getLastMediaSource(int mode, @UserIdInt int userId) { if (sharedPrefsInitialized()) { String key = getMediaSourceKey(mode, userId); String serialized = mSharedPrefs.getString(key, ""); if (!TextUtils.isEmpty(serialized)) { for (String name : getComponentNameList(serialized)) { ComponentName componentName = ComponentName.unflattenFromString(name); if (isMediaService(componentName, userId)) { return componentName; } } } } return getDefaultMediaSource(userId); } private ComponentName getDefaultMediaSource(@UserIdInt int userId) { String defaultMediaSource = mContext.getString(R.string.config_defaultMediaSource); ComponentName defaultComponent = ComponentName.unflattenFromString(defaultMediaSource); if (isMediaService(defaultComponent, userId)) { return defaultComponent; } Slogf.e(TAG, "No media service for user %d in the default component: %s", userId, defaultComponent); return null; } private String serializeComponentNameList(Deque componentNames) { return String.join(COMPONENT_NAME_SEPARATOR, componentNames); } private List getComponentNameList(String serialized) { // Note that for an empty string, String#split returns an array of size 1 with an empty // string as its entry. Instead, we just return an empty list. if (TextUtils.isEmpty(serialized)) { return Collections.emptyList(); } String[] componentNames = serialized.split(COMPONENT_NAME_SEPARATOR); return (Arrays.asList(componentNames)); } private void savePlaybackState(PlaybackState playbackState, @UserIdInt int userId) { if (!sharedPrefsInitialized()) { return; } if (isUserEphemeral(UserHandle.of(userId))) { return; } int state = playbackState != null ? playbackState.getState() : PlaybackState.STATE_NONE; synchronized (mLock) { getOrCreateUserMediaPlayContextLocked(userId).mCurrentPlaybackState = state; } String key = getPlaybackStateKey(userId); if (DEBUG) { Slogf.d(TAG, "savePlaybackState() for user %d: %s = %d)", userId, key, state); } getSharedPrefsForWriting(userId).putInt(key, state).apply(); } /** * Builds a string key for saving the playback state for a specific media source (and user) */ private String getPlaybackStateKey(@UserIdInt int userId) { ComponentName mediaComponent; synchronized (mLock) { mediaComponent = getPrimaryMediaComponentsForUserLocked(userId)[MEDIA_SOURCE_MODE_PLAYBACK]; } StringBuilder builder = new StringBuilder().append(PLAYBACK_STATE_KEY).append(userId); if (mediaComponent != null) { builder.append(mediaComponent.flattenToString()); } return builder.toString(); } private String getMediaSourceKey(int mode, @UserIdInt int userId) { return SOURCE_KEY + mode + SOURCE_KEY_SEPARATOR + userId; } private String getLastUpdateKey(@UserIdInt int userId) { return LAST_UPDATE_KEY + userId; } /** * 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. */ @GuardedBy("mLock") private void updateActiveMediaControllerLocked(List mediaControllers, @UserIdInt int userId) { UserMediaPlayContext userMediaPlayContext = getOrCreateUserMediaPlayContextLocked(userId); if (userMediaPlayContext.mPrimaryMediaComponents[MEDIA_SOURCE_MODE_PLAYBACK] == null) { return; } if (userMediaPlayContext.mActiveMediaController != null) { userMediaPlayContext.mActiveMediaController.unregisterCallback( userMediaPlayContext.mMediaControllerCallback); userMediaPlayContext.mActiveMediaController = null; } for (MediaController controller : mediaControllers) { if (matchPrimaryMediaSource(controller.getPackageName(), getClassName(controller), MEDIA_SOURCE_MODE_PLAYBACK, userId)) { userMediaPlayContext.mActiveMediaController = controller; PlaybackState state = controller.getPlaybackState(); savePlaybackState(state, userId); // 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. controller.registerCallback(userMediaPlayContext.mMediaControllerCallback, mHandler); return; } } } /** * Returns whether we should autoplay the current media source */ private boolean shouldStartPlayback(int config, @UserIdInt int userId) { switch (config) { case AUTOPLAY_CONFIG_NEVER: return false; case AUTOPLAY_CONFIG_ALWAYS: return true; case AUTOPLAY_CONFIG_RETAIN_PER_SOURCE: if (!sharedPrefsInitialized()) { return false; } int savedState = mSharedPrefs.getInt(getPlaybackStateKey(userId), PlaybackState.STATE_NONE); if (DEBUG) { Slogf.d(TAG, "Getting saved playback state %d for user %d. Last saved on %d", savedState, userId, mSharedPrefs.getLong(getLastUpdateKey(userId), -1)); } return savedState == PlaybackState.STATE_PLAYING; case AUTOPLAY_CONFIG_RETAIN_PREVIOUS: int currentPlaybackState; synchronized (mLock) { currentPlaybackState = getOrCreateUserMediaPlayContextLocked(userId).mCurrentPlaybackState; } return currentPlaybackState == PlaybackState.STATE_PLAYING; default: Slogf.e(TAG, "Unsupported playback configuration: " + config); return false; } } /** * Gets the editor used to update shared preferences. */ private SharedPreferences.Editor getSharedPrefsForWriting(@UserIdInt int userId) { long now = System.currentTimeMillis(); String lastUpdateKey = getLastUpdateKey(userId); Slogf.i(TAG, "Updating %s to %d", lastUpdateKey, now); return mSharedPrefs.edit().putLong(lastUpdateKey, now); } @NonNull private static String getClassName(MediaController controller) { Bundle sessionExtras = controller.getExtras(); String value = sessionExtras == null ? "" : sessionExtras.getString( Car.CAR_EXTRA_BROWSE_SERVICE_FOR_SESSION); return value != null ? value : ""; } private static String mediaModeToString(@CarMediaManager.MediaSourceMode int mode) { return DebugUtils.constantToString(CarMediaManager.class, "MEDIA_SOURCE_", mode); } private final class MediaKeyEventListener implements KeyEventListener { /** * Handles a media key event from {@link CarInputService}. * *

When there are multiple active media sessions, stop after first successful delivery. */ @Override public void onKeyEvent(KeyEvent event, int displayType, int seat) { if (DEBUG) { Slogf.d(TAG, "onKeyEvent(%s, %d, %d)", event, displayType, seat); } int occupantZoneId = mOccupantZoneService.getOccupantZoneIdForSeat(seat); if (occupantZoneId == INVALID_ZONE_ID) { Slogf.w(TAG, "Failed to find a valid occupant zone for seat %d." + " Ignoring key event %s", seat, event); return; } int userId = mOccupantZoneService.getUserForOccupant(occupantZoneId); if (userId == INVALID_USER_ID) { Slogf.w(TAG, "Failed to find a valid user for occupant zone %d." + " Ignoring key event %s", occupantZoneId, event); return; } List mediaControllers = mMediaSessionManager.getActiveSessionsForUser( /* notificationListeners= */ null, UserHandle.of(userId)); // Send the key event until it is successfully sent to any of the active sessions. boolean sent = false; for (int i = 0; !sent && i < mediaControllers.size(); i++) { sent = mediaControllers.get(i).dispatchMediaButtonEvent(event); } if (DEBUG) { if (sent) { Slogf.d(TAG, "Successfully sent the key event %s to user %d", event, userId); } else { Slogf.d(TAG, "No active media session can receive the key event %s for user %d", event, userId); } } } } }