blob: bda7fbc4b1482da86aebfd87615d6e9e42c09634 [file] [log] [blame]
/*
* 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 android.app.ActivityManager;
import android.car.media.CarMediaManager;
import android.car.media.CarMediaManager.MediaSourceChangedListener;
import android.car.media.ICarMedia;
import android.car.media.ICarMediaSourceListener;
import android.content.BroadcastReceiver;
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.Handler;
import android.os.HandlerThread;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.service.media.MediaBrowserService;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.car.user.CarUserService;
import java.io.PrintWriter;
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";
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 PACKAGE_NAME_SEPARATOR = ",";
private Context mContext;
private final MediaSessionManager mMediaSessionManager;
private MediaSessionUpdater mMediaSessionUpdater;
private String mPrimaryMediaPackage;
private SharedPreferences mSharedPrefs;
// MediaController for the current active user's active media session. This controller can be
// null if playback has not been started yet.
private MediaController mActiveUserMediaController;
private SessionChangedListener mSessionsListener;
private boolean mStartPlayback;
private RemoteCallbackList<ICarMediaSourceListener> mMediaSourceListeners =
new RemoteCallbackList();
// Handler to receive PlaybackState callbacks from the active media controller.
private Handler mHandler;
private HandlerThread mHandlerThread;
/** The package name of the last media source that was removed while being primary. */
private String mRemovedMediaSourcePackage;
/**
* Listens to {@link Intent#ACTION_PACKAGE_REMOVED} and {@link Intent#ACTION_PACKAGE_REPLACED}
* so we can reset the media source to null when its application is uninstalled, and restore it
* when the application is reinstalled.
*/
private BroadcastReceiver mPackageRemovedReceiver = 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())) {
if (mPrimaryMediaPackage != null && mPrimaryMediaPackage.equals(intentPackage)) {
mRemovedMediaSourcePackage = intentPackage;
setPrimaryMediaSource(null);
}
} else if (Intent.ACTION_PACKAGE_REPLACED.equals(intent.getAction())
|| Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction())) {
if (mRemovedMediaSourcePackage != null
&& mRemovedMediaSourcePackage.equals(intentPackage)
&& isMediaService(intentPackage)) {
setPrimaryMediaSource(mRemovedMediaSourcePackage);
}
}
}
};
private BroadcastReceiver mUserSwitchReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
updateMediaSessionCallbackForCurrentUser();
}
};
public CarMediaService(Context context) {
mContext = context;
mMediaSessionManager = mContext.getSystemService(MediaSessionManager.class);
mMediaSessionUpdater = new MediaSessionUpdater();
mHandlerThread = new HandlerThread(CarLog.TAG_MEDIA);
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
filter.addAction(Intent.ACTION_PACKAGE_ADDED);
filter.addDataScheme("package");
mContext.registerReceiver(mPackageRemovedReceiver, filter);
IntentFilter userSwitchFilter = new IntentFilter();
userSwitchFilter.addAction(Intent.ACTION_USER_SWITCHED);
mContext.registerReceiver(mUserSwitchReceiver, userSwitchFilter);
updateMediaSessionCallbackForCurrentUser();
}
@Override
public void init() {
CarLocalServices.getService(CarUserService.class).runOnUser0Unlock(() -> {
mSharedPrefs = mContext.getSharedPreferences(SHARED_PREF, Context.MODE_PRIVATE);
mPrimaryMediaPackage = getLastMediaPackage();
mStartPlayback = mSharedPrefs.getInt(PLAYBACK_STATE_KEY, PlaybackState.STATE_NONE)
== PlaybackState.STATE_PLAYING;
notifyListeners();
});
}
@Override
public void release() {
mMediaSessionUpdater.unregisterCallbacks();
}
@Override
public void dump(PrintWriter writer) {
writer.println("*CarMediaService*");
writer.println("\tCurrent media package: " + mPrimaryMediaPackage);
if (mActiveUserMediaController != null) {
writer.println(
"\tCurrent media controller: " + mActiveUserMediaController.getPackageName());
}
writer.println("\tNumber of active media sessions: "
+ mMediaSessionManager.getActiveSessionsForUser(null,
ActivityManager.getCurrentUser()).size());
}
/**
* @see {@link CarMediaManager#setMediaSource(String)}
*/
@Override
public synchronized void setMediaSource(String packageName) {
ICarImpl.assertPermission(mContext, android.Manifest.permission.MEDIA_CONTENT_CONTROL);
setPrimaryMediaSource(packageName);
}
/**
* @see {@link CarMediaManager#getMediaSource()}
*/
@Override
public synchronized String getMediaSource() {
ICarImpl.assertPermission(mContext, android.Manifest.permission.MEDIA_CONTENT_CONTROL);
return mPrimaryMediaPackage;
}
/**
* @see {@link CarMediaManager#registerMediaSourceListener(MediaSourceChangedListener)}
*/
@Override
public synchronized void registerMediaSourceListener(ICarMediaSourceListener callback) {
ICarImpl.assertPermission(mContext, android.Manifest.permission.MEDIA_CONTENT_CONTROL);
mMediaSourceListeners.register(callback);
}
/**
* @see {@link CarMediaManager#unregisterMediaSourceListener(ICarMediaSourceListener)}
*/
@Override
public synchronized void unregisterMediaSourceListener(ICarMediaSourceListener callback) {
ICarImpl.assertPermission(mContext, android.Manifest.permission.MEDIA_CONTENT_CONTROL);
mMediaSourceListeners.unregister(callback);
}
private void updateMediaSessionCallbackForCurrentUser() {
if (mSessionsListener != null) {
mMediaSessionManager.removeOnActiveSessionsChangedListener(mSessionsListener);
}
mSessionsListener = new SessionChangedListener(ActivityManager.getCurrentUser());
mMediaSessionManager.addOnActiveSessionsChangedListener(mSessionsListener, null,
ActivityManager.getCurrentUser(), null);
mMediaSessionUpdater.registerCallbacks(mMediaSessionManager.getActiveSessionsForUser(
null, ActivityManager.getCurrentUser()));
}
/**
* Attempts to play the current source using MediaController.TransportControls.play()
*/
private void play() {
if (mActiveUserMediaController != null) {
TransportControls controls = mActiveUserMediaController.getTransportControls();
if (controls != null) {
controls.play();
}
}
}
/**
* Attempts to stop the current source using MediaController.TransportControls.stop()
*/
private void stop() {
if (mActiveUserMediaController != null) {
TransportControls controls = mActiveUserMediaController.getTransportControls();
if (controls != null) {
controls.stop();
}
}
}
private class SessionChangedListener implements OnActiveSessionsChangedListener {
private final int mCurrentUser;
SessionChangedListener(int currentUser) {
mCurrentUser = currentUser;
}
@Override
public void onActiveSessionsChanged(List<MediaController> controllers) {
if (ActivityManager.getCurrentUser() != mCurrentUser) {
Log.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) {
setPrimaryMediaSource(mMediaController.getPackageName());
}
mPreviousPlaybackState = state.getState();
}
}
private class MediaSessionUpdater {
private Map<Token, MediaControllerCallback> 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<MediaController> newControllers) {
List<MediaController> additions = new ArrayList<>(newControllers.size());
Map<MediaSession.Token, MediaControllerCallback> 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 new active sessions for a matching controller.
if (mActiveUserMediaController == null) {
updateActiveMediaController(additions);
}
}
/**
* Unregister all MediaController callbacks
*/
private void unregisterCallbacks() {
for (Map.Entry<Token, MediaControllerCallback> entry : mCallbacks.entrySet()) {
entry.getValue().unregister();
}
}
}
/**
* Updates the primary media source, then notifies content observers of the change
*/
private synchronized void setPrimaryMediaSource(@Nullable String packageName) {
if (mPrimaryMediaPackage != null && mPrimaryMediaPackage.equals((packageName))) {
return;
}
stop();
mStartPlayback = false;
mPrimaryMediaPackage = packageName;
updateActiveMediaController(mMediaSessionManager
.getActiveSessionsForUser(null, ActivityManager.getCurrentUser()));
if (mSharedPrefs != null) {
if (!TextUtils.isEmpty(mPrimaryMediaPackage)) {
saveLastMediaPackage(mPrimaryMediaPackage);
mRemovedMediaSourcePackage = null;
}
} else {
// Shouldn't reach this unless there is some other error in CarService
Log.e(CarLog.TAG_MEDIA, "Error trying to save last media source, prefs uninitialized");
}
notifyListeners();
}
private void notifyListeners() {
int i = mMediaSourceListeners.beginBroadcast();
while (i-- > 0) {
try {
ICarMediaSourceListener callback = mMediaSourceListeners.getBroadcastItem(i);
callback.onMediaSourceChanged(mPrimaryMediaPackage);
} catch (RemoteException e) {
Log.e(CarLog.TAG_MEDIA, "calling onMediaSourceChanged failed " + e);
}
}
mMediaSourceListeners.finishBroadcast();
}
private MediaController.Callback mMediaControllerCallback = new MediaController.Callback() {
@Override
public void onPlaybackStateChanged(PlaybackState state) {
savePlaybackState(state);
// Try to start playback if the new state allows the play action
maybeRestartPlayback(state);
}
};
/**
* Finds the currently playing media source, then updates the active source if different
*/
private synchronized void updatePrimaryMediaSourceWithCurrentlyPlaying(
List<MediaController> controllers) {
for (MediaController controller : controllers) {
if (controller.getPlaybackState() != null
&& controller.getPlaybackState().getState() == PlaybackState.STATE_PLAYING) {
if (mPrimaryMediaPackage == null || !mPrimaryMediaPackage.equals(
controller.getPackageName())) {
setPrimaryMediaSource(controller.getPackageName());
}
return;
}
}
}
private boolean isMediaService(String packageName) {
return getBrowseServiceClassName(packageName) != null;
}
private String getBrowseServiceClassName(@NonNull String packageName) {
PackageManager packageManager = mContext.getPackageManager();
Intent mediaIntent = new Intent();
mediaIntent.setPackage(packageName);
mediaIntent.setAction(MediaBrowserService.SERVICE_INTERFACE);
List<ResolveInfo> mediaServices = packageManager.queryIntentServices(mediaIntent,
PackageManager.GET_RESOLVED_FILTER);
if (mediaServices == null || mediaServices.isEmpty()) {
return null;
}
return mediaServices.get(0).serviceInfo.name;
}
private void saveLastMediaPackage(@NonNull String packageName) {
String serialized = mSharedPrefs.getString(SOURCE_KEY, null);
if (serialized == null) {
mSharedPrefs.edit().putString(SOURCE_KEY, packageName).apply();
} else {
Deque<String> packageNames = getPackageNameList(serialized);
packageNames.remove(packageName);
packageNames.addFirst(packageName);
mSharedPrefs.edit().putString(SOURCE_KEY, serializePackageNameList(packageNames))
.apply();
}
}
private String getLastMediaPackage() {
String serialized = mSharedPrefs.getString(SOURCE_KEY, null);
if (!TextUtils.isEmpty(serialized)) {
for (String packageName : getPackageNameList(serialized)) {
if (isMediaService(packageName)) {
return packageName;
}
}
}
String defaultSourcePackage = mContext.getString(R.string.default_media_application);
if (isMediaService(defaultSourcePackage)) {
return defaultSourcePackage;
}
return null;
}
private String serializePackageNameList(Deque<String> packageNames) {
return packageNames.stream().collect(Collectors.joining(PACKAGE_NAME_SEPARATOR));
}
private Deque<String> getPackageNameList(String serialized) {
String[] packageNames = serialized.split(PACKAGE_NAME_SEPARATOR);
return new ArrayDeque(Arrays.asList(packageNames));
}
private void savePlaybackState(PlaybackState playbackState) {
int state = playbackState != null ? playbackState.getState() : PlaybackState.STATE_NONE;
if (state == PlaybackState.STATE_PLAYING) {
// No longer need to request play if audio was resumed already via some other means,
// e.g. Assistant starts playback, user uses hardware button, etc.
mStartPlayback = false;
}
if (mSharedPrefs != null) {
mSharedPrefs.edit().putInt(PLAYBACK_STATE_KEY, state).apply();
}
}
private void maybeRestartPlayback(PlaybackState state) {
if (mStartPlayback && state != null
&& (state.getActions() & PlaybackState.ACTION_PLAY) != 0) {
play();
mStartPlayback = false;
}
}
/**
* Updates active media controller from the list that has the same package name as the primary
* media package. Clears callback and resets media controller to null if not found.
*/
private void updateActiveMediaController(List<MediaController> mediaControllers) {
if (mPrimaryMediaPackage == null) {
return;
}
if (mActiveUserMediaController != null) {
mActiveUserMediaController.unregisterCallback(mMediaControllerCallback);
mActiveUserMediaController = null;
}
for (MediaController controller : mediaControllers) {
if (mPrimaryMediaPackage.equals(controller.getPackageName())) {
mActiveUserMediaController = controller;
// 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.
PlaybackState state = mActiveUserMediaController.getPlaybackState();
savePlaybackState(state);
mActiveUserMediaController.registerCallback(mMediaControllerCallback, mHandler);
maybeRestartPlayback(state);
return;
}
}
}
}