Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 44ecb68a authored by Kyunglyul Hyun's avatar Kyunglyul Hyun Committed by Android (Google) Code Review
Browse files

Merge "Introduce SessionPriorityList into MCS"

parents d1a1b991 30c15892
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -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.
     */
+46 −39
Original line number Diff line number Diff line
@@ -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<Session2Record> 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,47 +552,30 @@ public class MediaCommunicationService extends SystemService {
        }

        public List<Session2Token> getAllSession2Tokens() {
            synchronized (mUserLock) {
                return mSessionRecords.stream()
                        .map(Session2Record::getSessionToken)
                        .collect(Collectors.toList());
            }
            return mSessionPriorityList.getAllTokens();
        }

        public List<Session2Token> 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 (mSessionPriorityList.destroySessionsByUserId(userId)) {
                mHandler.post(() -> dispatchSession2Changed(mFullUserId));
            }
        }
            if (changed) {
                mHandler.post(() -> dispatchSession2Changed(mFullUserId));

        public boolean containsSession(Session2Record session) {
            return mSessionPriorityList.contains(session);
        }

        public void onPlaybackStateChanged(Session2Record session, boolean promotePriority) {
            mSessionPriorityList.onPlaybackStateChanged(session, promotePriority);
        }
    }

@@ -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);
                }
            }
        }
    }
}
+193 −0
Original line number Diff line number Diff line
/*
 * 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<Session2Record> 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<Session2Token> getAllTokens() {
        List<Session2Token> sessions = new ArrayList<>();
        synchronized (mLock) {
            for (Session2Record session : mSessions) {
                sessions.add(session.getSessionToken());
            }
        }
        return sessions;
    }

    public List<Session2Token> getTokensByUserId(int userId) {
        List<Session2Token> 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;
    }
}