Merge "Simplify the media button handling logic"
diff --git a/api/current.txt b/api/current.txt
index 369e571..b58f706 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -24337,9 +24337,9 @@
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 {
diff --git a/api/system-current.txt b/api/system-current.txt
index 95d6897..9e1c123 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -26207,9 +26207,9 @@
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 {
diff --git a/api/test-current.txt b/api/test-current.txt
index a436d1f..67d4586 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -24448,9 +24448,9 @@
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 {
diff --git a/media/java/android/media/session/MediaSession.java b/media/java/android/media/session/MediaSession.java
index bee3f52..f10f442 100644
--- a/media/java/android/media/session/MediaSession.java
+++ b/media/java/android/media/session/MediaSession.java
@@ -77,13 +77,19 @@
/**
* 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;
/**
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 333d27b..c139fa8 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -6555,7 +6555,7 @@
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 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);
}
diff --git a/services/core/java/com/android/server/media/AudioPlaybackMonitor.java b/services/core/java/com/android/server/media/AudioPlaybackMonitor.java
new file mode 100644
index 0000000..c6dc11c
--- /dev/null
+++ b/services/core/java/com/android/server/media/AudioPlaybackMonitor.java
@@ -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();
+ }
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java
index 20663a0..7f75c83 100644
--- a/services/core/java/com/android/server/media/MediaSessionRecord.java
+++ b/services/core/java/com/android/server/media/MediaSessionRecord.java
@@ -66,12 +66,6 @@
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 @@
private int mRatingType;
private int mRepeatMode;
private boolean mShuffleModeEnabled;
- private long mLastActiveTime;
// End TransportPerformer fields
// Volume handling fields
@@ -130,7 +123,7 @@
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 @@
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();
@@ -211,6 +204,15 @@
}
/**
+ * 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.
*
* @return The user id for this session.
@@ -244,7 +246,7 @@
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 @@
}
/**
- * 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 @@
@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 @@
@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 @@
@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);
}
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index ea9128f..4bf9d8f 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -57,11 +57,13 @@
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 @@
*/
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 @@
// 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 @@
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 @@
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 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 @@
}
if (mGlobalPrioritySession == session) {
mGlobalPrioritySession = null;
+ if (session.isActive() && user != null) {
+ user.pushAddressedPlayerChangedLocked();
+ }
}
try {
@@ -340,8 +356,7 @@
// 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 @@
}
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 @@
}
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 @@
}
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 @@
}
}
+ /**
+ * 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 @@
* 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 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 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 @@
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 @@
}
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 @@
for (int i = 0; i < count; i++) {
mUserRecords.valueAt(i).dumpLocked(pw, "");
}
+ mAudioPlaybackMonitor.dump(pw, "");
}
}
@@ -1390,7 +1431,7 @@
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 @@
}
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 @@
} 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 @@
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 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();
}
}
}
diff --git a/services/core/java/com/android/server/media/MediaSessionStack.java b/services/core/java/com/android/server/media/MediaSessionStack.java
index 8b80734..b0d8adc 100644
--- a/services/core/java/com/android/server/media/MediaSessionStack.java
+++ b/services/core/java/com/android/server/media/MediaSessionStack.java
@@ -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 @@
* <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 @@
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>();
- // The last record that either entered one of the playing states or was
- // added.
- private MediaSessionRecord mLastInterestingRecord;
- private MediaSessionRecord mCachedButtonReceiver;
+ 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;
+
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 @@
*/
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 @@
}
/**
- * 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.
*/
@@ -158,6 +167,81 @@
}
/**
+ * 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 @@
*/
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 @@
}
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 @@
}
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 @@
/**
* 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 @@
// 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 @@
// 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++;
- 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 if (session.isPlaybackActive()) {
+ result.add(lastPlaybackActiveIndex++, session);
+ lastActiveIndex++;
} 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 @@
private void clearCache() {
mCachedDefault = null;
mCachedVolumeDefault = null;
- mCachedButtonReceiver = null;
mCachedActiveList = null;
- mCachedTransportControlList = null;
}
}