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

Commit 30c15892 authored by Kyunglyul Hyun's avatar Kyunglyul Hyun
Browse files

Introduce SessionPriorityList into MCS

It will acts like MediaSessionStack

Bug: 198120109
Test: CTS
Change-Id: I7a0da49d1b96ea5a12cf3e441059866670a3314c
parent bbd6112e
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;
    }
}