Simplify the media button handling logic

Here's the new media handling logic.

1. If the foreground app consumes the media key event
  (Activity.onKeyDown/Up() or Activity.setMediaController()),
  we stop here.
2. Find the media app whose AudioPlaybackConfiguration becomes
  PLAYER_STATE_STARTED lastly across the boot. The media app is the app
  with the media session.
  - If its session is still alive, send the media key event
    to the media session.
  - If its session has been released but the session set the media
    button receiver, send the media key event through the media button
    receiver.
  - If it has multiple media sessions, pick the media session whose
    PlaybackState matches with the AudioPlaybackConfiguration's player
    state.
3. Stop here. Don't try revive any other app.

This approach has two limitations.
1. If an app has multiple AudioPlaybackConfigurations and/or
   media sessions, the MediaSessionService may pick the wrong media
   session to send media key events.
2. If an app with a media session plays sound effects differently from
   the SoundPool class, the MediaSessionService would consider the app
   as the lastly played media app.

Bug: 33032080
Test: Manual tests as follows
  1. Checked that the lastly played app receives the media key events
     although the app's media session doesn't report its playback state.
  2. Checked that the lastly played app receives the media key events
     although the app's media session is released.
  3. Checked that the lastly played app doesn't receive the media key
     events if its playback is remote.
Change-Id: I1fd6f9eee0750da4dea9fcc7401fdb1c3f249a72
This commit is contained in:
Jaewan Kim
2017-02-02 11:52:08 +09:00
committed by Jae Seo
parent 7c665f783f
commit 92dea33bfe
9 changed files with 503 additions and 225 deletions

View File

@@ -24337,9 +24337,9 @@ package android.media.session {
method public void setRepeatMode(int);
method public void setSessionActivity(android.app.PendingIntent);
method public void setShuffleModeEnabled(boolean);
field public static final int FLAG_HANDLES_MEDIA_BUTTONS = 1; // 0x1
field public static final deprecated int FLAG_HANDLES_MEDIA_BUTTONS = 1; // 0x1
field public static final int FLAG_HANDLES_QUEUE_COMMANDS = 4; // 0x4
field public static final int FLAG_HANDLES_TRANSPORT_CONTROLS = 2; // 0x2
field public static final deprecated int FLAG_HANDLES_TRANSPORT_CONTROLS = 2; // 0x2
}
public static abstract class MediaSession.Callback {

View File

@@ -26207,9 +26207,9 @@ package android.media.session {
method public void setRepeatMode(int);
method public void setSessionActivity(android.app.PendingIntent);
method public void setShuffleModeEnabled(boolean);
field public static final int FLAG_HANDLES_MEDIA_BUTTONS = 1; // 0x1
field public static final deprecated int FLAG_HANDLES_MEDIA_BUTTONS = 1; // 0x1
field public static final int FLAG_HANDLES_QUEUE_COMMANDS = 4; // 0x4
field public static final int FLAG_HANDLES_TRANSPORT_CONTROLS = 2; // 0x2
field public static final deprecated int FLAG_HANDLES_TRANSPORT_CONTROLS = 2; // 0x2
}
public static abstract class MediaSession.Callback {

View File

@@ -24448,9 +24448,9 @@ package android.media.session {
method public void setRepeatMode(int);
method public void setSessionActivity(android.app.PendingIntent);
method public void setShuffleModeEnabled(boolean);
field public static final int FLAG_HANDLES_MEDIA_BUTTONS = 1; // 0x1
field public static final deprecated int FLAG_HANDLES_MEDIA_BUTTONS = 1; // 0x1
field public static final int FLAG_HANDLES_QUEUE_COMMANDS = 4; // 0x4
field public static final int FLAG_HANDLES_TRANSPORT_CONTROLS = 2; // 0x2
field public static final deprecated int FLAG_HANDLES_TRANSPORT_CONTROLS = 2; // 0x2
}
public static abstract class MediaSession.Callback {

View File

@@ -77,13 +77,19 @@ public final class MediaSession {
/**
* Set this flag on the session to indicate that it can handle media button
* events.
* @deprecated This flag is no longer used. All media sessions are expected to handle media
* button events now.
*/
@Deprecated
public static final int FLAG_HANDLES_MEDIA_BUTTONS = 1 << 0;
/**
* Set this flag on the session to indicate that it handles transport
* control commands through its {@link Callback}.
* @deprecated This flag is no longer used. All media sessions are expected to handle transport
* controls now.
*/
@Deprecated
public static final int FLAG_HANDLES_TRANSPORT_CONTROLS = 1 << 1;
/**

View File

@@ -6555,7 +6555,7 @@ public class AudioService extends IAudioService.Stub
public void registerPlaybackCallback(IPlaybackConfigDispatcher pcdb) {
final boolean isPrivileged =
(PackageManager.PERMISSION_GRANTED == mContext.checkCallingPermission(
(PackageManager.PERMISSION_GRANTED == mContext.checkCallingOrSelfPermission(
android.Manifest.permission.MODIFY_AUDIO_ROUTING));
mPlaybackMonitor.registerPlaybackCallback(pcdb, isPrivileged);
}
@@ -6566,7 +6566,7 @@ public class AudioService extends IAudioService.Stub
public List<AudioPlaybackConfiguration> getActivePlaybackConfigurations() {
final boolean isPrivileged =
(PackageManager.PERMISSION_GRANTED == mContext.checkCallingPermission(
(PackageManager.PERMISSION_GRANTED == mContext.checkCallingOrSelfPermission(
android.Manifest.permission.MODIFY_AUDIO_ROUTING));
return mPlaybackMonitor.getActivePlaybackConfigurations(isPrivileged);
}

View File

@@ -0,0 +1,201 @@
/*
* Copyright (C) 2017 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.server.media;
import android.content.Context;
import android.media.AudioManager.AudioPlaybackCallback;
import android.media.AudioPlaybackConfiguration;
import android.media.IAudioService;
import android.media.IPlaybackConfigDispatcher;
import android.os.Binder;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.IntArray;
import android.util.Log;
import android.util.SparseArray;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Monitors changes in audio playback and notify the newly started audio playback through the
* {@link OnAudioPlaybackStartedListener}.
*/
class AudioPlaybackMonitor extends IPlaybackConfigDispatcher.Stub {
private static boolean DEBUG = MediaSessionService.DEBUG;
private static String TAG = "AudioPlaybackMonitor";
/**
* Called when audio playback is started for a given UID.
*/
interface OnAudioPlaybackStartedListener {
void onAudioPlaybackStarted(int uid);
}
private final Object mLock = new Object();
private final Context mContext;
private final OnAudioPlaybackStartedListener mListener;
private Set<Integer> mActiveAudioPlaybackPlayerInterfaceIds = new HashSet<>();
private Set<Integer> mActiveAudioPlaybackClientUids = new HashSet<>();
// Sorted array of UIDs that had active audio playback. (i.e. playing an audio/video)
// The UID whose audio playback becomes active at the last comes first.
// TODO(b/35278867): Find and use unique identifier for apps because apps may share the UID.
private final IntArray mSortedAudioPlaybackClientUids = new IntArray();
AudioPlaybackMonitor(Context context, IAudioService audioService,
OnAudioPlaybackStartedListener listener) {
mContext = context;
mListener = listener;
try {
audioService.registerPlaybackCallback(this);
} catch (RemoteException e) {
Log.wtf(TAG, "Failed to register playback callback", e);
}
}
/**
* Called when the {@link AudioPlaybackConfiguration} is updated.
* <p>If an app starts audio playback, the app's local media session will be the media button
* session. If the app has multiple media sessions, the playback active local session will be
* picked.
*
* @param configs List of the current audio playback configuration
*/
@Override
public void dispatchPlaybackConfigChange(List<AudioPlaybackConfiguration> configs) {
final long token = Binder.clearCallingIdentity();
try {
Set<Integer> newActiveAudioPlaybackPlayerInterfaceIds = new HashSet<>();
List<Integer> newActiveAudioPlaybackClientUids = new ArrayList<>();
synchronized (mLock) {
mActiveAudioPlaybackClientUids.clear();
for (AudioPlaybackConfiguration config : configs) {
// Ignore inactive (i.e. not playing) or PLAYER_TYPE_JAM_SOUNDPOOL
// (i.e. playback from the SoundPool class which is only for sound effects)
// playback.
// Note that we shouldn't ignore PLAYER_TYPE_UNKNOWN because it might be OEM
// specific audio/video players.
if (!config.isActive()
|| config.getPlayerType()
== AudioPlaybackConfiguration.PLAYER_TYPE_JAM_SOUNDPOOL) {
continue;
}
mActiveAudioPlaybackClientUids.add(config.getClientUid());
newActiveAudioPlaybackPlayerInterfaceIds.add(config.getPlayerInterfaceId());
if (!mActiveAudioPlaybackPlayerInterfaceIds.contains(
config.getPlayerInterfaceId())) {
if (DEBUG) {
Log.d(TAG, "Found a new active media playback. " +
AudioPlaybackConfiguration.toLogFriendlyString(config));
}
// New active audio playback.
newActiveAudioPlaybackClientUids.add(config.getClientUid());
int index = mSortedAudioPlaybackClientUids.indexOf(config.getClientUid());
if (index == 0) {
// It's the lastly played music app already. Skip updating.
continue;
} else if (index > 0) {
mSortedAudioPlaybackClientUids.remove(index);
}
mSortedAudioPlaybackClientUids.add(0, config.getClientUid());
}
}
mActiveAudioPlaybackPlayerInterfaceIds.clear();
mActiveAudioPlaybackPlayerInterfaceIds = newActiveAudioPlaybackPlayerInterfaceIds;
}
for (int uid : newActiveAudioPlaybackClientUids) {
mListener.onAudioPlaybackStarted(uid);
}
} finally {
Binder.restoreCallingIdentity(token);
}
}
/**
* Returns the sorted list of UIDs that have had active audio playback. (i.e. playing an
* audio/video) The UID whose audio playback becomes active at the last comes first.
*/
public IntArray getSortedAudioPlaybackClientUids() {
IntArray sortedAudioPlaybackClientUids = new IntArray();
synchronized (mLock) {
sortedAudioPlaybackClientUids.addAll(mSortedAudioPlaybackClientUids);
}
return sortedAudioPlaybackClientUids;
}
/**
* Returns if the audio playback is active for the uid.
*/
public boolean isPlaybackActive(int uid) {
synchronized (mLock) {
return mActiveAudioPlaybackClientUids.contains(uid);
}
}
/**
* Cleans up the sorted list of audio playback client UIDs with given {@param
* mediaButtonSessionUid}.
* <p>UIDs whose audio playback started after the media button session's audio playback
* cannot be the lastly played media app. So they won't needed anymore.
*
* @param mediaButtonSessionUid UID of the media button session.
*/
public void cleanUpAudioPlaybackUids(int mediaButtonSessionUid) {
synchronized (mLock) {
int userId = UserHandle.getUserId(mediaButtonSessionUid);
for (int i = mSortedAudioPlaybackClientUids.size() - 1; i >= 0; i--) {
if (mSortedAudioPlaybackClientUids.get(i) == mediaButtonSessionUid) {
break;
}
if (userId == UserHandle.getUserId(mSortedAudioPlaybackClientUids.get(i))) {
// Clean up unnecessary UIDs.
// It doesn't need to be managed profile aware because it's just to prevent
// the list from increasing indefinitely. The media button session updating
// shouldn't be affected by cleaning up.
mSortedAudioPlaybackClientUids.remove(i);
}
}
}
}
/**
* Dumps {@link AudioPlaybackMonitor}.
*/
public void dump(PrintWriter pw, String prefix) {
synchronized (mLock) {
pw.println(prefix + "Audio playback (lastly played comes first)");
String indent = prefix + " ";
for (int i = 0; i < mSortedAudioPlaybackClientUids.size(); i++) {
int uid = mSortedAudioPlaybackClientUids.get(i);
pw.print(indent + "uid=" + uid + " packages=");
String[] packages = mContext.getPackageManager().getPackagesForUid(uid);
if (packages != null && packages.length > 0) {
for (int j = 0; j < packages.length; j++) {
pw.print(packages[j] + " ");
}
}
pw.println();
}
}
}
}

View File

@@ -65,12 +65,6 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
private static final String TAG = "MediaSessionRecord";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
/**
* The length of time a session will still be considered active after
* pausing in ms.
*/
private static final int ACTIVE_BUFFER = 30000;
/**
* The amount of time we'll send an assumed volume after the last volume
* command before reverting to the last reported volume.
@@ -109,7 +103,6 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
private int mRatingType;
private int mRepeatMode;
private boolean mShuffleModeEnabled;
private long mLastActiveTime;
// End TransportPerformer fields
// Volume handling fields
@@ -130,7 +123,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
private String mCallingPackage;
public MediaSessionRecord(int ownerPid, int ownerUid, int userId, String ownerPackageName,
ISessionCallback cb, String tag, MediaSessionService service, Handler handler) {
ISessionCallback cb, String tag, MediaSessionService service, Looper handlerLooper) {
mOwnerPid = ownerPid;
mOwnerUid = ownerUid;
mUserId = userId;
@@ -140,7 +133,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
mSession = new SessionStub();
mSessionCb = new SessionCb(cb);
mService = service;
mHandler = new MessageHandler(handler.getLooper());
mHandler = new MessageHandler(handlerLooper);
mAudioManager = (AudioManager) service.getContext().getSystemService(Context.AUDIO_SERVICE);
mAudioManagerInternal = LocalServices.getService(AudioManagerInternal.class);
mAudioAttrs = new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build();
@@ -210,6 +203,15 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
return (mFlags & flag) != 0;
}
/**
* Get the UID this session was created for.
*
* @return The UID for this session.
*/
public int getUid() {
return mOwnerUid;
}
/**
* Get the user id this session was created for.
*
@@ -244,7 +246,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
public void adjustVolume(int direction, int flags, String packageName, int uid,
boolean useSuggested) {
int previousFlagPlaySound = flags & AudioManager.FLAG_PLAY_SOUND;
if (isPlaybackActive(false) || hasFlag(MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY)) {
if (isPlaybackActive() || hasFlag(MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY)) {
flags &= ~AudioManager.FLAG_PLAY_SOUND;
}
if (mVolumeType == PlaybackInfo.PLAYBACK_TYPE_LOCAL) {
@@ -320,25 +322,13 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
}
/**
* Check if the session is currently performing playback. This will also
* return true if the session was recently paused.
* Check if the session is currently performing playback.
*
* @param includeRecentlyActive True if playback that was recently paused
* should count, false if it shouldn't.
* @return True if the session is performing playback, false otherwise.
*/
public boolean isPlaybackActive(boolean includeRecentlyActive) {
int state = mPlaybackState == null ? 0 : mPlaybackState.getState();
if (MediaSession.isActiveState(state)) {
return true;
}
if (includeRecentlyActive && state == mPlaybackState.STATE_PAUSED) {
long inactiveTime = SystemClock.uptimeMillis() - mLastActiveTime;
if (inactiveTime < ACTIVE_BUFFER) {
return true;
}
}
return false;
public boolean isPlaybackActive() {
int state = mPlaybackState == null ? PlaybackState.STATE_NONE : mPlaybackState.getState();
return MediaSession.isActiveState(state);
}
/**
@@ -456,7 +446,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
@Override
public String toString() {
return mPackageName + "/" + mTag + " (uid=" + mUserId + ")";
return mPackageName + "/" + mTag + " (userId=" + mUserId + ")";
}
private void postAdjustLocalVolume(final int stream, final int direction, final int flags,
@@ -817,6 +807,12 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
@Override
public void setMediaButtonReceiver(PendingIntent pi) {
mMediaButtonReceiver = pi;
final long token = Binder.clearCallingIdentity();
try {
mService.onMediaButtonReceiverChanged(MediaSessionRecord.this);
} finally {
Binder.restoreCallingIdentity(token);
}
}
@Override
@@ -842,15 +838,19 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
@Override
public void setPlaybackState(PlaybackState state) {
int oldState = mPlaybackState == null ? 0 : mPlaybackState.getState();
int newState = state == null ? 0 : state.getState();
if (MediaSession.isActiveState(oldState) && newState == PlaybackState.STATE_PAUSED) {
mLastActiveTime = SystemClock.elapsedRealtime();
}
int oldState = mPlaybackState == null
? PlaybackState.STATE_NONE : mPlaybackState.getState();
int newState = state == null
? PlaybackState.STATE_NONE : state.getState();
synchronized (mLock) {
mPlaybackState = state;
}
mService.onSessionPlaystateChange(MediaSessionRecord.this, oldState, newState);
final long token = Binder.clearCallingIdentity();
try {
mService.onSessionPlaystateChanged(MediaSessionRecord.this, oldState, newState);
} finally {
Binder.restoreCallingIdentity(token);
}
mHandler.post(MessageHandler.MSG_UPDATE_PLAYBACK_STATE);
}

View File

@@ -57,11 +57,13 @@ import android.os.Process;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ServiceManager;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
import android.speech.RecognizerIntent;
import android.text.TextUtils;
import android.util.IntArray;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;
@@ -85,7 +87,7 @@ import java.util.List;
*/
public class MediaSessionService extends SystemService implements Monitor {
private static final String TAG = "MediaSessionService";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
// Leave log for key event always.
private static final boolean DEBUG_KEY_EVENT = true;
@@ -114,6 +116,7 @@ public class MediaSessionService extends SystemService implements Monitor {
// It's always not null after the MediaSessionService is started.
private FullUserRecord mCurrentFullUserRecord;
private MediaSessionRecord mGlobalPrioritySession;
private AudioPlaybackMonitor mAudioPlaybackMonitor;
// Used to notify system UI when remote volume was changed. TODO find a
// better way to handle this.
@@ -134,6 +137,19 @@ public class MediaSessionService extends SystemService implements Monitor {
mKeyguardManager =
(KeyguardManager) getContext().getSystemService(Context.KEYGUARD_SERVICE);
mAudioService = getAudioService();
mAudioPlaybackMonitor = new AudioPlaybackMonitor(getContext(), mAudioService,
new AudioPlaybackMonitor.OnAudioPlaybackStartedListener() {
@Override
public void onAudioPlaybackStarted(int uid) {
synchronized (mLock) {
FullUserRecord user =
getFullUserRecordLocked(UserHandle.getUserId(uid));
if (user != null) {
user.mPriorityStack.updateMediaButtonSessionIfNeeded();
}
}
}
});
mAudioManagerInternal = LocalServices.getService(AudioManagerInternal.class);
mContentResolver = getContext().getContentResolver();
mSettingsObserver = new SettingsObserver();
@@ -161,9 +177,10 @@ public class MediaSessionService extends SystemService implements Monitor {
user.mPriorityStack.onSessionStateChange(record);
if ((record.getFlags() & MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY) != 0) {
mGlobalPrioritySession = record;
user.pushAddressedPlayerChangedLocked();
}
mHandler.postSessionsChanged(record.getUserId());
}
mHandler.post(MessageHandler.MSG_SESSIONS_CHANGED, record.getUserId(), 0);
}
/**
@@ -180,18 +197,14 @@ public class MediaSessionService extends SystemService implements Monitor {
}
}
public void onSessionPlaystateChange(MediaSessionRecord record, int oldState, int newState) {
boolean updateSessions = false;
public void onSessionPlaystateChanged(MediaSessionRecord record, int oldState, int newState) {
synchronized (mLock) {
FullUserRecord user = getFullUserRecordLocked(record.getUserId());
if (user == null || !user.mPriorityStack.contains(record)) {
Log.d(TAG, "Unknown session changed playback state. Ignoring.");
return;
}
updateSessions = user.mPriorityStack.onPlaystateChange(record, oldState, newState);
}
if (updateSessions) {
mHandler.post(MessageHandler.MSG_SESSIONS_CHANGED, record.getUserId(), 0);
user.mPriorityStack.onPlaystateChanged(record, oldState, newState);
}
}
@@ -332,6 +345,9 @@ public class MediaSessionService extends SystemService implements Monitor {
}
if (mGlobalPrioritySession == session) {
mGlobalPrioritySession = null;
if (session.isActive() && user != null) {
user.pushAddressedPlayerChangedLocked();
}
}
try {
@@ -340,8 +356,7 @@ public class MediaSessionService extends SystemService implements Monitor {
// ignore exceptions while destroying a session.
}
session.onDestroy();
mHandler.post(MessageHandler.MSG_SESSIONS_CHANGED, session.getUserId(), 0);
mHandler.postSessionsChanged(session.getUserId());
}
private void enforcePackageName(String packageName, int uid) {
@@ -461,7 +476,7 @@ public class MediaSessionService extends SystemService implements Monitor {
}
final MediaSessionRecord session = new MediaSessionRecord(callerPid, callerUid, userId,
callerPackageName, cb, tag, this, mHandler);
callerPackageName, cb, tag, this, mHandler.getLooper());
try {
cb.asBinder().linkToDeath(session, 0);
} catch (RemoteException e) {
@@ -469,8 +484,7 @@ public class MediaSessionService extends SystemService implements Monitor {
}
user.addSessionLocked(session);
mHandler.post(MessageHandler.MSG_SESSIONS_CHANGED, userId, 0);
mHandler.postSessionsChanged(userId);
if (DEBUG) {
Log.d(TAG, "Created session for " + callerPackageName + " with tag " + tag);
@@ -496,10 +510,6 @@ public class MediaSessionService extends SystemService implements Monitor {
}
List<MediaSessionRecord> records = user.mPriorityStack.getActiveSessions(userId);
int size = records.size();
if (size > 0 && records.get(0).isPlaybackActive(false)) {
user.rememberMediaButtonReceiverLocked(records.get(0));
}
user.pushAddressedPlayerChangedLocked();
ArrayList<MediaSession.Token> tokens = new ArrayList<MediaSession.Token>();
for (int i = 0; i < size; i++) {
tokens.add(new MediaSession.Token(records.get(i).getControllerBinder()));
@@ -536,6 +546,22 @@ public class MediaSessionService extends SystemService implements Monitor {
}
}
/**
* Called when the media button receiver for the {@param record} is changed.
*
* @param record the media session whose media button receiver is updated.
*/
public void onMediaButtonReceiverChanged(MediaSessionRecord record) {
synchronized (mLock) {
FullUserRecord user = getFullUserRecordLocked(record.getUserId());
MediaSessionRecord mediaButtonSession =
user.mPriorityStack.getMediaButtonSession();
if (record == mediaButtonSession) {
user.rememberMediaButtonReceiverLocked(mediaButtonSession);
}
}
}
private String getCallingPackageName(int uid) {
String[] packages = getContext().getPackageManager().getPackagesForUid(uid);
if (packages != null && packages.length > 0) {
@@ -568,10 +594,10 @@ public class MediaSessionService extends SystemService implements Monitor {
* place makes more sense and increases the readability.</p>
* <p>The contents of this object is guarded by {@link #mLock}.
*/
final class FullUserRecord {
final class FullUserRecord implements MediaSessionStack.OnMediaButtonSessionChangedListener {
private static final String COMPONENT_NAME_USER_ID_DELIM = ",";
private final int mFullUserId;
private final MediaSessionStack mPriorityStack = new MediaSessionStack();
private final MediaSessionStack mPriorityStack;
private PendingIntent mLastMediaButtonReceiver;
private ComponentName mRestoredMediaButtonReceiver;
private int mRestoredMediaButtonReceiverUserId;
@@ -588,6 +614,7 @@ public class MediaSessionService extends SystemService implements Monitor {
public FullUserRecord(int fullUserId) {
mFullUserId = fullUserId;
mPriorityStack = new MediaSessionStack(mAudioPlaybackMonitor, this);
// Restore the remembered media button receiver before the boot.
String mediaButtonReceiver = Settings.Secure.getStringForUser(mContentResolver,
Settings.System.MEDIA_BUTTON_RECEIVER, mFullUserId);
@@ -603,15 +630,14 @@ public class MediaSessionService extends SystemService implements Monitor {
}
public void destroySessionsForUserLocked(int userId) {
List<MediaSessionRecord> sessions = mPriorityStack.getPriorityList(false, 0, userId);
List<MediaSessionRecord> sessions = mPriorityStack.getPriorityList(false, userId);
for (MediaSessionRecord session : sessions) {
MediaSessionService.this.destroySessionLocked(session);
}
}
public void addSessionLocked(MediaSessionRecord session) {
mPriorityStack.addSession(session,
mFullUserId == mFullUserIds.get(session.getUserId()));
mPriorityStack.addSession(session);
}
public void removeSessionLocked(MediaSessionRecord session) {
@@ -642,21 +668,41 @@ public class MediaSessionService extends SystemService implements Monitor {
mPriorityStack.dump(pw, indent);
}
// Remember the media button receiver and keep it in the persistent storage.
private void rememberMediaButtonReceiverLocked(MediaSessionRecord record) {
@Override
public void onMediaButtonSessionChanged(MediaSessionRecord oldMediaButtonSession,
MediaSessionRecord newMediaButtonSession) {
if (DEBUG_KEY_EVENT) {
Log.d(TAG, "Media button session will be changed to " + newMediaButtonSession);
}
synchronized (mLock) {
if (oldMediaButtonSession != null) {
mHandler.postSessionsChanged(oldMediaButtonSession.getUserId());
}
if (newMediaButtonSession != null) {
rememberMediaButtonReceiverLocked(newMediaButtonSession);
mHandler.postSessionsChanged(newMediaButtonSession.getUserId());
}
pushAddressedPlayerChangedLocked();
}
}
// Remember media button receiver and keep it in the persistent storage.
public void rememberMediaButtonReceiverLocked(MediaSessionRecord record) {
PendingIntent receiver = record.getMediaButtonReceiver();
if (receiver == null) {
return;
}
mLastMediaButtonReceiver = receiver;
ComponentName component = receiver.getIntent().getComponent();
if (component != null && record.getPackageName().equals(component.getPackageName())) {
String componentName = component.flattenToString();
Settings.Secure.putStringForUser(mContentResolver,
Settings.System.MEDIA_BUTTON_RECEIVER,
componentName + COMPONENT_NAME_USER_ID_DELIM + record.getUserId(),
mFullUserId);
mRestoredMediaButtonReceiver = null;
String componentName = "";
if (receiver != null) {
ComponentName component = receiver.getIntent().getComponent();
if (component != null
&& record.getPackageName().equals(component.getPackageName())) {
componentName = component.flattenToString();
}
}
Settings.Secure.putStringForUser(mContentResolver,
Settings.System.MEDIA_BUTTON_RECEIVER,
componentName + COMPONENT_NAME_USER_ID_DELIM + record.getUserId(),
mFullUserId);
}
private void pushAddressedPlayerChangedLocked() {
@@ -682,14 +728,8 @@ public class MediaSessionService extends SystemService implements Monitor {
}
private MediaSessionRecord getMediaButtonSessionLocked() {
if (isGlobalPriorityActiveLocked()) {
return mGlobalPrioritySession;
}
// If we don't have a media button receiver to fall back on
// include non-playing sessions for dispatching.
boolean useNotPlayingSessions = (mLastMediaButtonReceiver == null
&& mRestoredMediaButtonReceiver == null);
return mPriorityStack.getDefaultMediaButtonSession(useNotPlayingSessions);
return isGlobalPriorityActiveLocked()
? mGlobalPrioritySession : mPriorityStack.getMediaButtonSession();
}
}
@@ -1262,6 +1302,7 @@ public class MediaSessionService extends SystemService implements Monitor {
for (int i = 0; i < count; i++) {
mUserRecords.valueAt(i).dumpLocked(pw, "");
}
mAudioPlaybackMonitor.dump(pw, "");
}
}
@@ -1390,7 +1431,7 @@ public class MediaSessionService extends SystemService implements Monitor {
PendingIntent receiver = mCurrentFullUserRecord.mLastMediaButtonReceiver;
if (DEBUG_KEY_EVENT) {
Log.d(TAG, "Sending " + keyEvent
+ " to the last known pendingIntent " + receiver);
+ " to the last known PendingIntent " + receiver);
}
receiver.send(getContext(),
needWakeLock ? mKeyEventReceiver.mLastTimeoutId : -1,
@@ -1413,7 +1454,8 @@ public class MediaSessionService extends SystemService implements Monitor {
}
mediaButtonIntent.setComponent(receiver);
getContext().sendBroadcastAsUser(mediaButtonIntent,
UserHandle.of(mCurrentFullUserRecord.mRestoredMediaButtonReceiverUserId));
UserHandle.of(mCurrentFullUserRecord
.mRestoredMediaButtonReceiverUserId));
if (mCurrentFullUserRecord.mCallback != null) {
mCurrentFullUserRecord.mCallback
.onMediaKeyEventDispatchedToMediaButtonReceiver(
@@ -1426,23 +1468,6 @@ public class MediaSessionService extends SystemService implements Monitor {
} catch (RemoteException e) {
Log.w(TAG, "Failed to send callback", e);
}
} else {
if (DEBUG) {
Log.d(TAG, "Sending media key ordered broadcast");
}
if (needWakeLock) {
mMediaEventWakeLock.acquire();
}
// Fallback to legacy behavior
Intent keyIntent = new Intent(Intent.ACTION_MEDIA_BUTTON, null);
keyIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
keyIntent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
if (needWakeLock) {
keyIntent.putExtra(EXTRA_WAKELOCK_ACQUIRED, WAKELOCK_RELEASE_ON_FINISHED);
}
// Send broadcast only to the full user.
getContext().sendOrderedBroadcastAsUser(keyIntent, UserHandle.CURRENT,
null, mKeyEventDone, mHandler, Activity.RESULT_OK, null, null);
}
}
@@ -1637,12 +1662,13 @@ public class MediaSessionService extends SystemService implements Monitor {
final class MessageHandler extends Handler {
private static final int MSG_SESSIONS_CHANGED = 1;
private static final int MSG_VOLUME_INITIAL_DOWN = 2;
private final SparseArray<Integer> mIntegerCache = new SparseArray<>();
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_SESSIONS_CHANGED:
pushSessionsChanged(msg.arg1);
pushSessionsChanged((int) msg.obj);
break;
case MSG_VOLUME_INITIAL_DOWN:
synchronized (mLock) {
@@ -1657,8 +1683,15 @@ public class MediaSessionService extends SystemService implements Monitor {
}
}
public void post(int what, int arg1, int arg2) {
obtainMessage(what, arg1, arg2).sendToTarget();
public void postSessionsChanged(int userId) {
// Use object instead of the arguments when posting message to remove pending requests.
Integer userIdInteger = mIntegerCache.get(userId);
if (userIdInteger == null) {
userIdInteger = Integer.valueOf(userId);
mIntegerCache.put(userId, userIdInteger);
}
removeMessages(MSG_SESSIONS_CHANGED, userIdInteger);
obtainMessage(MSG_SESSIONS_CHANGED, userIdInteger).sendToTarget();
}
}
}

View File

@@ -16,12 +16,13 @@
package com.android.server.media;
import android.app.ActivityManager;
import android.media.session.MediaController.PlaybackInfo;
import android.media.session.PlaybackState;
import android.media.session.MediaSession;
import android.os.RemoteException;
import android.media.session.PlaybackState;
import android.os.Debug;
import android.os.UserHandle;
import android.util.IntArray;
import android.util.Log;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -33,6 +34,20 @@ import java.util.List;
* <p>This class isn't thread-safe. The caller should take care of the synchronization.
*/
class MediaSessionStack {
private static final boolean DEBUG = MediaSessionService.DEBUG;
private static final String TAG = "MediaSessionStack";
/**
* Listens the change in the media button session.
*/
interface OnMediaButtonSessionChangedListener {
/**
* Called when the media button session is changed.
*/
void onMediaButtonSessionChanged(MediaSessionRecord oldMediaButtonSession,
MediaSessionRecord newMediaButtonSession);
}
/**
* These are states that usually indicate the user took an action and should
* bump priority regardless of the old state.
@@ -51,57 +66,45 @@ class MediaSessionStack {
PlaybackState.STATE_CONNECTING,
PlaybackState.STATE_PLAYING };
private final ArrayList<MediaSessionRecord> mSessions = new ArrayList<MediaSessionRecord>();
/**
* Sorted list of the media sessions.
* The session of which PlaybackState is changed to ALWAYS_PRIORITY_STATES or
* TRANSITION_PRIORITY_STATES comes first.
* @see #shouldUpdatePriority
*/
private final List<MediaSessionRecord> mSessions = new ArrayList<MediaSessionRecord>();
private final AudioPlaybackMonitor mAudioPlaybackMonitor;
private final OnMediaButtonSessionChangedListener mOnMediaButtonSessionChangedListener;
/**
* The media button session which receives media key events.
* It could be null if the previous media buttion session is released.
*/
private MediaSessionRecord mMediaButtonSession;
// The last record that either entered one of the playing states or was
// added.
private MediaSessionRecord mLastInterestingRecord;
private MediaSessionRecord mCachedButtonReceiver;
private MediaSessionRecord mCachedDefault;
private MediaSessionRecord mCachedVolumeDefault;
private ArrayList<MediaSessionRecord> mCachedActiveList;
private ArrayList<MediaSessionRecord> mCachedTransportControlList;
/**
* Checks if a media session is created from the most recent app.
*
* @param record A media session record to be examined.
* @return {@code true} if the media session's package name equals to the most recent app, false
* otherwise.
*/
private static boolean isFromMostRecentApp(MediaSessionRecord record) {
try {
List<ActivityManager.RecentTaskInfo> tasks =
ActivityManager.getService().getRecentTasks(1,
ActivityManager.RECENT_IGNORE_HOME_AND_RECENTS_STACK_TASKS |
ActivityManager.RECENT_IGNORE_UNAVAILABLE |
ActivityManager.RECENT_INCLUDE_PROFILES |
ActivityManager.RECENT_WITH_EXCLUDED, record.getUserId()).getList();
if (tasks != null && !tasks.isEmpty()) {
ActivityManager.RecentTaskInfo recentTask = tasks.get(0);
if (recentTask.userId == record.getUserId() && recentTask.baseIntent != null) {
return recentTask.baseIntent.getComponent().getPackageName()
.equals(record.getPackageName());
}
}
} catch (RemoteException e) {
return false;
}
return false;
MediaSessionStack(AudioPlaybackMonitor monitor, OnMediaButtonSessionChangedListener listener) {
mAudioPlaybackMonitor = monitor;
mOnMediaButtonSessionChangedListener = listener;
}
/**
* Add a record to the priority tracker.
*
* @param record The record to add.
* @param fromForegroundUser {@code true} if the session is created by the foreground user.
*/
public void addSession(MediaSessionRecord record, boolean fromForegroundUser) {
public void addSession(MediaSessionRecord record) {
mSessions.add(record);
clearCache();
if (fromForegroundUser && isFromMostRecentApp(record)) {
mLastInterestingRecord = record;
}
// Update the media button session.
// The added session could be the session from the package with the audio playback.
// This can happen if an app starts audio playback before creating media session.
updateMediaButtonSessionIfNeeded();
}
/**
@@ -111,6 +114,11 @@ class MediaSessionStack {
*/
public void removeSession(MediaSessionRecord record) {
mSessions.remove(record);
if (mMediaButtonSession == record) {
// When the media button session is gone, try to find the alternative media session
// in the media button session app.
onMediaSessionChangeInMediaButtonSessionApp();
}
clearCache();
}
@@ -122,32 +130,33 @@ class MediaSessionStack {
}
/**
* Notify the priority tracker that a session's state changed.
* Notify the priority tracker that a session's playback state changed.
*
* @param record The record that changed.
* @param oldState Its old playback state.
* @param newState Its new playback state.
* @return true if the priority order was updated, false otherwise.
*/
public boolean onPlaystateChange(MediaSessionRecord record, int oldState, int newState) {
public void onPlaystateChanged(MediaSessionRecord record, int oldState, int newState) {
if (shouldUpdatePriority(oldState, newState)) {
mSessions.remove(record);
mSessions.add(0, record);
clearCache();
// This becomes the last interesting record since it entered a
// playing state
mLastInterestingRecord = record;
return true;
} else if (!MediaSession.isActiveState(newState)) {
// Just clear the volume cache when a state goes inactive
mCachedVolumeDefault = null;
}
return false;
// In most cases, playback state isn't needed for finding media buttion session,
// but we only use it as a hint if an app has multiple local media sessions.
// In that case, we pick the media session whose PlaybackState matches
// the audio playback configuration.
if (mMediaButtonSession != null && mMediaButtonSession.getUid() == record.getUid()) {
onMediaSessionChangeInMediaButtonSessionApp();
}
}
/**
* Handle any stack changes that need to occur in response to a session
* state change. TODO add the old and new session state as params
* Handle the change in activeness for a session.
*
* @param record The record that changed.
*/
@@ -157,6 +166,81 @@ class MediaSessionStack {
clearCache();
}
/**
* Update the media button session if needed.
* <p>The media button session is the session that will receive the media button events.
* <p>We send the media button events to the lastly played app. If the app has the media
* session, the session will receive the media button events.
*/
public void updateMediaButtonSessionIfNeeded() {
if (DEBUG) {
Log.d(TAG, "updateMediaButtonSessionIfNeeded, callers=" + Debug.getCallers(2));
}
IntArray audioPlaybackUids = mAudioPlaybackMonitor.getSortedAudioPlaybackClientUids();
for (int i = 0; i < audioPlaybackUids.size(); i++) {
MediaSessionRecord mediaButtonSession =
findMediaButtonSession(audioPlaybackUids.get(i));
if (mediaButtonSession != null) {
// Found the media button session.
mAudioPlaybackMonitor.cleanUpAudioPlaybackUids(mediaButtonSession.getUid());
if (mMediaButtonSession != mediaButtonSession) {
mOnMediaButtonSessionChangedListener.onMediaButtonSessionChanged(
mMediaButtonSession, mediaButtonSession);
mMediaButtonSession = mediaButtonSession;
}
return;
}
}
}
/**
* Handle the change in a media session in the media button session app.
* <p>If the app has multiple media sessions, change in a media sesion in the app may change
* the media button session.
* @see #findMediaButtonSession
*/
private void onMediaSessionChangeInMediaButtonSessionApp() {
MediaSessionRecord newMediaButtonSession =
findMediaButtonSession(mMediaButtonSession.getUid());
if (newMediaButtonSession != mMediaButtonSession) {
mOnMediaButtonSessionChangedListener.onMediaButtonSessionChanged(mMediaButtonSession,
newMediaButtonSession);
mMediaButtonSession = newMediaButtonSession;
}
}
/**
* Find the media button session with the given {@param uid}.
* If the app has multiple media sessions, the media session matches the audio playback state
* becomes the media button session.
*
* @return The media button session. Returns {@code null} if the app doesn't have a media
* session.
*/
private MediaSessionRecord findMediaButtonSession(int uid) {
MediaSessionRecord mediaButtonSession = null;
for (MediaSessionRecord session : mSessions) {
// Since the media buttons come with the headset/speaker, users will expect changes in
// the headset/speaker when they press media buttons. So only consider local playback
// for the media button session.
if (uid == session.getUid()
&& session.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_LOCAL) {
if (session.isPlaybackActive() ==
mAudioPlaybackMonitor.isPlaybackActive(session.getUid())) {
// If there's a media session whose PlaybackState matches
// the audio playback state, return it immediately.
return session;
}
if (mediaButtonSession == null) {
// Among the media sessions whose PlaybackState doesn't match
// the audio playback state, pick the top priority.
mediaButtonSession = session;
}
}
}
return mediaButtonSession;
}
/**
* Get the current priority sorted list of active sessions. The most
* important session is at index 0 and the least important at size - 1.
@@ -166,57 +250,29 @@ class MediaSessionStack {
*/
public ArrayList<MediaSessionRecord> getActiveSessions(int userId) {
if (mCachedActiveList == null) {
mCachedActiveList = getPriorityList(true, 0, userId);
mCachedActiveList = getPriorityList(true, userId);
}
return mCachedActiveList;
}
/**
* Get the highest priority session that can handle media buttons.
* Get the media button session which receives the media button events.
*
* @param includeNotPlaying Return a non-playing session if nothing else is
* available
* @return The default media button session or null.
* @return The media button session or null.
*/
public MediaSessionRecord getDefaultMediaButtonSession(boolean includeNotPlaying) {
if (mCachedButtonReceiver != null) {
return mCachedButtonReceiver;
}
ArrayList<MediaSessionRecord> records = getPriorityList(true,
MediaSession.FLAG_HANDLES_MEDIA_BUTTONS, UserHandle.USER_ALL);
if (records.size() > 0) {
MediaSessionRecord record = records.get(0);
if (record.isPlaybackActive(false)) {
// Since we're going to send a button event to this record make
// it the last interesting one.
mLastInterestingRecord = record;
mCachedButtonReceiver = record;
} else if (mLastInterestingRecord != null) {
if (records.contains(mLastInterestingRecord)) {
mCachedButtonReceiver = mLastInterestingRecord;
} else {
// That record is no longer used. Clear its reference.
mLastInterestingRecord = null;
}
}
if (includeNotPlaying && mCachedButtonReceiver == null) {
// If we really want a record and we didn't find one yet use the
// highest priority session even if it's not playing.
mCachedButtonReceiver = record;
}
}
return mCachedButtonReceiver;
public MediaSessionRecord getMediaButtonSession() {
return mMediaButtonSession;
}
public MediaSessionRecord getDefaultVolumeSession() {
if (mCachedVolumeDefault != null) {
return mCachedVolumeDefault;
}
ArrayList<MediaSessionRecord> records = getPriorityList(true, 0, UserHandle.USER_ALL);
ArrayList<MediaSessionRecord> records = getPriorityList(true, UserHandle.USER_ALL);
int size = records.size();
for (int i = 0; i < size; i++) {
MediaSessionRecord record = records.get(i);
if (record.isPlaybackActive(false)) {
if (record.isPlaybackActive()) {
mCachedVolumeDefault = record;
return record;
}
@@ -225,7 +281,7 @@ class MediaSessionStack {
}
public MediaSessionRecord getDefaultRemoteSession(int userId) {
ArrayList<MediaSessionRecord> records = getPriorityList(true, 0, userId);
ArrayList<MediaSessionRecord> records = getPriorityList(true, userId);
int size = records.size();
for (int i = 0; i < size; i++) {
@@ -238,9 +294,10 @@ class MediaSessionStack {
}
public void dump(PrintWriter pw, String prefix) {
ArrayList<MediaSessionRecord> sortedSessions = getPriorityList(false, 0,
ArrayList<MediaSessionRecord> sortedSessions = getPriorityList(false,
UserHandle.USER_ALL);
int count = sortedSessions.size();
pw.println(prefix + "Media button session is " + mMediaButtonSession);
pw.println(prefix + "Sessions Stack - have " + count + " sessions:");
String indent = prefix + " ";
for (int i = 0; i < count; i++) {
@@ -252,22 +309,23 @@ class MediaSessionStack {
/**
* Get a priority sorted list of sessions. Can filter to only return active
* sessions or sessions with specific flags.
* sessions or sessions.
* <p>Here's the priority order.
* <li>System priority session (session with FLAG_EXCLUSIVE_GLOBAL_PRIORITY)</li>
* <li>Active sessions whose PlaybackState is active</li>
* <li>Active sessions whose PlaybackState is inactive</li>
* <li>Inactive sessions</li>
*
* @param activeOnly True to only return active sessions, false to return
* all sessions.
* @param withFlags Only return sessions with all the specified flags set. 0
* returns all sessions.
* @param userId The user to get sessions for. {@link UserHandle#USER_ALL}
* will return sessions for all users.
* @return The priority sorted list of sessions.
*/
public ArrayList<MediaSessionRecord> getPriorityList(boolean activeOnly, int withFlags,
int userId) {
public ArrayList<MediaSessionRecord> getPriorityList(boolean activeOnly, int userId) {
ArrayList<MediaSessionRecord> result = new ArrayList<MediaSessionRecord>();
int lastLocalIndex = 0;
int lastPlaybackActiveIndex = 0;
int lastActiveIndex = 0;
int lastPublishedIndex = 0;
int size = mSessions.size();
for (int i = 0; i < size; i++) {
@@ -277,10 +335,7 @@ class MediaSessionStack {
// Filter out sessions for the wrong user
continue;
}
if ((session.getFlags() & withFlags) != withFlags) {
// Filter out sessions with the wrong flags
continue;
}
if (!session.isActive()) {
if (!activeOnly) {
// If we're getting unpublished as well always put them at
@@ -294,28 +349,13 @@ class MediaSessionStack {
// System priority sessions are special and always go at the
// front. We expect there to only be one of these at a time.
result.add(0, session);
lastLocalIndex++;
lastPlaybackActiveIndex++;
lastActiveIndex++;
} else if (session.isPlaybackActive()) {
result.add(lastPlaybackActiveIndex++, session);
lastActiveIndex++;
lastPublishedIndex++;
} else if (session.isPlaybackActive(true)) {
// TODO this with real local route check
if (true) {
// Active local sessions get top priority
result.add(lastLocalIndex, session);
lastLocalIndex++;
lastActiveIndex++;
lastPublishedIndex++;
} else {
// Then active remote sessions
result.add(lastActiveIndex, session);
lastActiveIndex++;
lastPublishedIndex++;
}
} else {
// inactive sessions go at the end in order of whoever last did
// something.
result.add(lastPublishedIndex, session);
lastPublishedIndex++;
result.add(lastActiveIndex++, session);
}
}
@@ -345,8 +385,6 @@ class MediaSessionStack {
private void clearCache() {
mCachedDefault = null;
mCachedVolumeDefault = null;
mCachedButtonReceiver = null;
mCachedActiveList = null;
mCachedTransportControlList = null;
}
}