diff --git a/apex/media/framework/java/android/media/MediaSession2.java b/apex/media/framework/java/android/media/MediaSession2.java index e76d61cf89653..cb6e1a0b202a6 100644 --- a/apex/media/framework/java/android/media/MediaSession2.java +++ b/apex/media/framework/java/android/media/MediaSession2.java @@ -262,7 +262,7 @@ public class MediaSession2 implements AutoCloseable { } /** - * Returns whehther the playback is active (i.e. playing something) + * Returns whether the playback is active (i.e. playing something) * * @return {@code true} if the playback active, {@code false} otherwise. */ diff --git a/apex/media/service/java/com/android/server/media/MediaCommunicationService.java b/apex/media/service/java/com/android/server/media/MediaCommunicationService.java index 09f65b5e4709a..74abf31cc84cc 100644 --- a/apex/media/service/java/com/android/server/media/MediaCommunicationService.java +++ b/apex/media/service/java/com/android/server/media/MediaCommunicationService.java @@ -54,7 +54,6 @@ import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; import java.util.concurrent.Executors; -import java.util.stream.Collectors; /** * A system service that manages {@link android.media.MediaSession2} creations @@ -281,6 +280,16 @@ public class MediaCommunicationService extends SystemService { session.close(); } + void onSessionPlaybackStateChanged(Session2Record session, boolean promotePriority) { + FullUserRecord user = session.getFullUser(); + if (user == null || !user.containsSession(session)) { + Log.d(TAG, "Unknown session changed playback state. Ignoring."); + return; + } + user.onPlaybackStateChanged(session, promotePriority); + } + + static boolean isMediaSessionKey(int keyCode) { switch (keyCode) { case KeyEvent.KEYCODE_MEDIA_PLAY: @@ -520,26 +529,20 @@ public class MediaCommunicationService extends SystemService { final class FullUserRecord { private final int mFullUserId; - private final Object mUserLock = new Object(); - @GuardedBy("mUserLock") - private final List mSessionRecords = new ArrayList<>(); + private final SessionPriorityList mSessionPriorityList = new SessionPriorityList(); FullUserRecord(int fullUserId) { mFullUserId = fullUserId; } public void addSession(Session2Record record) { - synchronized (mUserLock) { - mSessionRecords.add(record); - } + mSessionPriorityList.addSession(record); mHandler.post(() -> dispatchSession2Created(record.mSessionToken)); mHandler.post(() -> dispatchSession2Changed(mFullUserId)); } private void removeSession(Session2Record record) { - synchronized (mUserLock) { - mSessionRecords.remove(record); - } + mSessionPriorityList.removeSession(record); mHandler.post(() -> dispatchSession2Changed(mFullUserId)); //TODO: Handle if the removed session was the media button session. } @@ -549,48 +552,31 @@ public class MediaCommunicationService extends SystemService { } public List getAllSession2Tokens() { - synchronized (mUserLock) { - return mSessionRecords.stream() - .map(Session2Record::getSessionToken) - .collect(Collectors.toList()); - } + return mSessionPriorityList.getAllTokens(); } public List getSession2Tokens(int userId) { - synchronized (mUserLock) { - return mSessionRecords.stream() - .filter(record -> record.getUserId() == userId) - .map(Session2Record::getSessionToken) - .collect(Collectors.toList()); - } + return mSessionPriorityList.getTokensByUserId(userId); } public void destroyAllSessions() { - synchronized (mUserLock) { - for (Session2Record session : mSessionRecords) { - session.close(); - } - mSessionRecords.clear(); - } + mSessionPriorityList.destroyAllSessions(); mHandler.post(() -> dispatchSession2Changed(mFullUserId)); } public void destroySessionsForUser(int userId) { - boolean changed = false; - synchronized (mUserLock) { - for (int i = mSessionRecords.size() - 1; i >= 0; i--) { - Session2Record session = mSessionRecords.get(i); - if (session.getUserId() == userId) { - mSessionRecords.remove(i); - session.close(); - changed = true; - } - } - } - if (changed) { + if (mSessionPriorityList.destroySessionsByUserId(userId)) { mHandler.post(() -> dispatchSession2Changed(mFullUserId)); } } + + public boolean containsSession(Session2Record session) { + return mSessionPriorityList.contains(session); + } + + public void onPlaybackStateChanged(Session2Record session, boolean promotePriority) { + mSessionPriorityList.onPlaybackStateChanged(session, promotePriority); + } } static final class Session2Record { @@ -606,6 +592,7 @@ public class MediaCommunicationService extends SystemService { @GuardedBy("mSession2RecordLock") private boolean mIsClosed; + //TODO: introduce policy (See MediaSessionPolicyProvider) Session2Record(MediaCommunicationService service, FullUserRecord fullUser, Session2Token token, Executor controllerExecutor) { mServiceRef = new WeakReference<>(service); @@ -641,6 +628,12 @@ public class MediaCommunicationService extends SystemService { return mSessionToken; } + public boolean checkPlaybackActiveState(boolean expected) { + synchronized (mSession2RecordLock) { + return mIsConnected && mController.isPlaybackActive() == expected; + } + } + private class Controller2Callback extends MediaController2.ControllerCallback { @Override public void onConnected(MediaController2 controller, @@ -666,6 +659,20 @@ public class MediaCommunicationService extends SystemService { service.onSessionDied(Session2Record.this); } } + + @Override + public void onPlaybackActiveChanged( + @NonNull MediaController2 controller, + boolean playbackActive) { + if (DEBUG) { + Log.d(TAG, "playback active changed, " + mSessionToken + ", active=" + + playbackActive); + } + MediaCommunicationService service = mServiceRef.get(); + if (service != null) { + service.onSessionPlaybackStateChanged(Session2Record.this, playbackActive); + } + } } } } diff --git a/apex/media/service/java/com/android/server/media/SessionPriorityList.java b/apex/media/service/java/com/android/server/media/SessionPriorityList.java new file mode 100644 index 0000000000000..47b14b63ddbaa --- /dev/null +++ b/apex/media/service/java/com/android/server/media/SessionPriorityList.java @@ -0,0 +1,193 @@ +/* + * Copyright 2021 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.annotation.Nullable; +import android.media.Session2Token; +import android.util.Log; + +import com.android.internal.annotations.GuardedBy; +import com.android.server.media.MediaCommunicationService.Session2Record; + +import java.util.ArrayList; +import java.util.List; + +//TODO: Define the priority specifically. +/** + * Keeps track of media sessions and their priority for notifications, media + * button dispatch, etc. + * Higher priority session has more chance to be selected as media button session, + * which receives the media button events. + */ +class SessionPriorityList { + private static final String TAG = "SessionPriorityList"; + private final Object mLock = new Object(); + + @GuardedBy("mLock") + private final List mSessions = new ArrayList<>(); + + @Nullable + private Session2Record mMediaButtonSession; + @Nullable + private Session2Record mCachedVolumeSession; + + //TODO: integrate AudioPlayerStateMonitor + + public void addSession(Session2Record record) { + synchronized (mLock) { + mSessions.add(record); + } + } + + public void removeSession(Session2Record record) { + synchronized (mLock) { + mSessions.remove(record); + } + if (record == mMediaButtonSession) { + updateMediaButtonSession(null); + } + } + + public void destroyAllSessions() { + synchronized (mLock) { + for (Session2Record session : mSessions) { + session.close(); + } + mSessions.clear(); + } + } + + public boolean destroySessionsByUserId(int userId) { + boolean changed = false; + synchronized (mLock) { + for (int i = mSessions.size() - 1; i >= 0; i--) { + Session2Record session = mSessions.get(i); + if (session.getUserId() == userId) { + mSessions.remove(i); + session.close(); + changed = true; + } + } + } + return changed; + } + + public List getAllTokens() { + List sessions = new ArrayList<>(); + synchronized (mLock) { + for (Session2Record session : mSessions) { + sessions.add(session.getSessionToken()); + } + } + return sessions; + } + + public List getTokensByUserId(int userId) { + List sessions = new ArrayList<>(); + synchronized (mLock) { + for (Session2Record session : mSessions) { + if (session.getUserId() == userId) { + sessions.add(session.getSessionToken()); + } + } + } + return sessions; + } + + /** Gets the media button session which receives the media button events. */ + @Nullable + public Session2Record getMediaButtonSession() { + return mMediaButtonSession; + } + + /** Gets the media volume session which receives the volume key events. */ + @Nullable + public Session2Record getMediaVolumeSession() { + //TODO: if null, calculate it. + return mCachedVolumeSession; + } + + public boolean contains(Session2Record session) { + synchronized (mLock) { + return mSessions.contains(session); + } + } + + public void onPlaybackStateChanged(Session2Record session, boolean promotePriority) { + if (promotePriority) { + synchronized (mLock) { + if (mSessions.remove(session)) { + mSessions.add(0, session); + } else { + Log.w(TAG, "onPlaybackStateChanged: Ignoring unknown session"); + return; + } + } + } else if (session.checkPlaybackActiveState(false)) { + // Just clear the cached volume session when a state goes inactive + mCachedVolumeSession = null; + } + + // In most cases, playback state isn't needed for finding the media button 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.getSessionToken().getUid() + == session.getSessionToken().getUid()) { + Session2Record newMediaButtonSession = + findMediaButtonSession(mMediaButtonSession.getSessionToken().getUid()); + if (newMediaButtonSession != mMediaButtonSession) { + // Check if the policy states that this session should not be updated as a media + // button session. + updateMediaButtonSession(newMediaButtonSession); + } + } + } + + private void updateMediaButtonSession(@Nullable Session2Record newSession) { + mMediaButtonSession = newSession; + //TODO: invoke callbacks for media button session changed listeners + } + + /** + * Finds the media button session with the given {@param uid}. + * If the app has multiple media sessions, the media session whose playback state is not null + * and matches the audio playback state becomes the media button session. Otherwise the top + * priority session becomes the media button session. + * + * @return The media button session. Returns {@code null} if the app doesn't have a media + * session. + */ + @Nullable + private Session2Record findMediaButtonSession(int uid) { + Session2Record mediaButtonSession = null; + synchronized (mLock) { + for (Session2Record session : mSessions) { + if (uid != session.getSessionToken().getUid()) { + continue; + } + // TODO: check audio player state monitor + if (mediaButtonSession == null) { + // Pick the top priority session as a default. + mediaButtonSession = session; + } + } + } + return mediaButtonSession; + } +}