diff --git a/api/current.txt b/api/current.txt index 369e571225232..b58f706b374ea 100644 --- a/api/current.txt +++ b/api/current.txt @@ -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 { diff --git a/api/system-current.txt b/api/system-current.txt index 95d6897aaa598..9e1c123ea4140 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -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 { diff --git a/api/test-current.txt b/api/test-current.txt index a436d1f42d78d..67d458651cfaf 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -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 { diff --git a/media/java/android/media/session/MediaSession.java b/media/java/android/media/session/MediaSession.java index bee3f52c9b900..f10f4427d6f0a 100644 --- a/media/java/android/media/session/MediaSession.java +++ b/media/java/android/media/session/MediaSession.java @@ -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; /** diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 333d27bd53608..c139fa85bf3b1 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 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 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 0000000000000..c6dc11c53393f --- /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 mActiveAudioPlaybackPlayerInterfaceIds = new HashSet<>(); + private Set 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. + *

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 configs) { + final long token = Binder.clearCallingIdentity(); + try { + Set newActiveAudioPlaybackPlayerInterfaceIds = new HashSet<>(); + List 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}. + *

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 20663a09699d9..7f75c83f8d51b 100644 --- a/services/core/java/com/android/server/media/MediaSessionRecord.java +++ b/services/core/java/com/android/server/media/MediaSessionRecord.java @@ -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); } diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java index ea9128fd6e2f3..4bf9d8f5ff538 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.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 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 tokens = new ArrayList(); 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.

*

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 sessions = mPriorityStack.getPriorityList(false, 0, userId); + List 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 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(); } } } diff --git a/services/core/java/com/android/server/media/MediaSessionStack.java b/services/core/java/com/android/server/media/MediaSessionStack.java index 8b8073454f8c6..b0d8adcc122e2 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 @@ import java.util.List; *

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 mSessions = new ArrayList(); + /** + * 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 mSessions = new ArrayList(); + + 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 mCachedActiveList; - private ArrayList 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 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. + *

The media button session is the session that will receive the media button events. + *

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. + *

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 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 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 records = getPriorityList(true, 0, UserHandle.USER_ALL); + ArrayList 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 records = getPriorityList(true, 0, userId); + ArrayList 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 sortedSessions = getPriorityList(false, 0, + ArrayList 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. + *

Here's the priority order. + *

  • System priority session (session with FLAG_EXCLUSIVE_GLOBAL_PRIORITY)
  • + *
  • Active sessions whose PlaybackState is active
  • + *
  • Active sessions whose PlaybackState is inactive
  • + *
  • Inactive sessions
  • * * @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 getPriorityList(boolean activeOnly, int withFlags, - int userId) { + public ArrayList getPriorityList(boolean activeOnly, int userId) { ArrayList result = new ArrayList(); - 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; } }