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

Commit 0ad953fc authored by Jaewan Kim's avatar Jaewan Kim
Browse files

Introduce common interfaces for both session1 and session2

This is intermediate step toward for making session2 to handle media
key event.

Bug: 147279043
Test: Build and run media cts
Change-Id: I2a5723dab9e81db2338575774567f2e7b1d6f8c1
parent 2d3c66da
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -141,6 +141,9 @@ public class MediaController2 implements AutoCloseable {
                // Note: unbindService() throws IllegalArgumentException when it's called twice.
                return;
            }
            if (DEBUG) {
                Log.d(TAG, "closing " + this);
            }
            mClosed = true;
            if (mServiceConnection != null) {
                // Note: This should be called even when the bindService() has returned false.
+164 −0
Original line number Diff line number Diff line
/*
 * Copyright 2019 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.media.MediaController2;
import android.media.Session2CommandGroup;
import android.media.Session2Token;
import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.Looper;
import android.os.ResultReceiver;
import android.os.UserHandle;
import android.util.Log;
import android.view.KeyEvent;

import com.android.internal.annotations.GuardedBy;

import java.io.PrintWriter;

/**
 * Keeps the record of {@link Session2Token} helps to send command to the corresponding session.
 */
// TODO(jaewan): Do not call service method directly -- introduce listener instead.
public class MediaSession2Record implements MediaSessionRecordImpl {
    private static final String TAG = "MediaSession2Record";
    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
    private final Object mLock = new Object();

    @GuardedBy("mLock")
    private final Session2Token mSessionToken;
    @GuardedBy("mLock")
    private final HandlerExecutor mHandlerExecutor;
    @GuardedBy("mLock")
    private final MediaController2 mController;
    @GuardedBy("mLock")
    private final MediaSessionService mService;
    @GuardedBy("mLock")
    private boolean mIsConnected;

    public MediaSession2Record(Session2Token sessionToken, MediaSessionService service,
            Looper handlerLooper) {
        mSessionToken = sessionToken;
        mService = service;
        mHandlerExecutor = new HandlerExecutor(new Handler(handlerLooper));
        mController = new MediaController2.Builder(service.getContext(), sessionToken)
                .setControllerCallback(mHandlerExecutor, new Controller2Callback())
                .build();
    }

    @Override
    public String getPackageName() {
        return mSessionToken.getPackageName();
    }

    public Session2Token getSession2Token() {
        return mSessionToken;
    }

    @Override
    public int getUid() {
        return mSessionToken.getUid();
    }

    @Override
    public int getUserId() {
        return UserHandle.getUserId(mSessionToken.getUid());
    }

    @Override
    public boolean isSystemPriority() {
        // System priority session is currently only allowed for telephony, and it's OK to stick to
        // the media1 API at this moment.
        return false;
    }

    @Override
    public void adjustVolume(String packageName, String opPackageName, int pid, int uid,
            boolean asSystemService, int direction, int flags, boolean useSuggested) {
        // TODO(jaewan): Add API to adjust volume.
    }

    @Override
    public boolean isActive() {
        synchronized (mLock) {
            return mIsConnected;
        }
    }

    @Override
    public boolean checkPlaybackActiveState(boolean expected) {
        synchronized (mLock) {
            return mIsConnected && mController.isPlaybackActive() == expected;
        }
    }

    @Override
    public boolean isPlaybackTypeLocal() {
        // TODO(jaewan): Implement -- need API to know whether the playback is remote or local.
        return true;
    }

    @Override
    public void close() {
        synchronized (mLock) {
            // Call close regardless of the mIsAvailable. This may be called when it's not yet
            // connected.
            mController.close();
        }
    }

    @Override
    public boolean sendMediaButton(String packageName, int pid, int uid, boolean asSystemService,
            KeyEvent ke, int sequenceId, ResultReceiver cb) {
        // TODO(jaewan): Implement.
        return false;
    }

    @Override
    public void dump(PrintWriter pw, String prefix) {
        pw.println(prefix + "token=" + mSessionToken);
        pw.println(prefix + "controller=" + mController);

        final String indent = prefix + "  ";
        pw.println(indent + "playbackActive=" + mController.isPlaybackActive());
    }

    private class Controller2Callback extends MediaController2.ControllerCallback {
        @Override
        public void onConnected(MediaController2 controller, Session2CommandGroup allowedCommands) {
            if (DEBUG) {
                Log.d(TAG, "connected to " + mSessionToken + ", allowed=" + allowedCommands);
            }
            synchronized (mLock) {
                mIsConnected = true;
            }
            mService.pushSession2TokensChanged(MediaSession2Record.this);
        }

        @Override
        public void onDisconnected(MediaController2 controller) {
            if (DEBUG) {
                Log.d(TAG, "disconnected from " + mSessionToken);
            }
            synchronized (mLock) {
                mIsConnected = false;
            }
            mService.sessionDied(MediaSession2Record.this);
        }
    }
}
+16 −4
Original line number Diff line number Diff line
@@ -62,7 +62,8 @@ import java.util.List;
 * This is the system implementation of a Session. Apps will interact with the
 * MediaSession wrapper class instead.
 */
public class MediaSessionRecord implements IBinder.DeathRecipient, AutoCloseable {
// TODO(jaewan): Do not call service method directly -- introduce listener instead.
public class MediaSessionRecord implements IBinder.DeathRecipient, MediaSessionRecordImpl {
    private static final String TAG = "MediaSessionRecord";
    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);

@@ -170,6 +171,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, AutoCloseable
     *
     * @return Info that identifies this session.
     */
    @Override
    public String getPackageName() {
        return mPackageName;
    }
@@ -188,6 +190,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, AutoCloseable
     *
     * @return The UID for this session.
     */
    @Override
    public int getUid() {
        return mOwnerUid;
    }
@@ -197,6 +200,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, AutoCloseable
     *
     * @return The user id for this session.
     */
    @Override
    public int getUserId() {
        return mUserId;
    }
@@ -207,6 +211,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, AutoCloseable
     *
     * @return True if this is a system priority session, false otherwise
     */
    @Override
    public boolean isSystemPriority() {
        return (mFlags & MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY) != 0;
    }
@@ -318,9 +323,13 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, AutoCloseable

    /**
     * Check if this session has been set to active by the app.
     * <p>
     * It's not used to prioritize sessions for dispatching media keys since API 26, but still used
     * to filter session list in MediaSessionManager#getActiveSessions().
     *
     * @return True if the session is active, false otherwise.
     */
    @Override
    public boolean isActive() {
        return mIsActive && !mDestroyed;
    }
@@ -333,6 +342,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, AutoCloseable
     * @param expected True if playback is expected to be active. false otherwise.
     * @return True if the session's playback matches with the expectation. false otherwise.
     */
    @Override
    public boolean checkPlaybackActiveState(boolean expected) {
        if (mPlaybackState == null) {
            return false;
@@ -345,7 +355,8 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, AutoCloseable
     *
     * @return {@code true} if the playback is local.
     */
    public boolean isPlaybackLocal() {
    @Override
    public boolean isPlaybackTypeLocal() {
        return mVolumeType == PlaybackInfo.PLAYBACK_TYPE_LOCAL;
    }

@@ -383,7 +394,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, AutoCloseable
     * @param sequenceId (optional) sequence id. Use this only when a wake lock is needed.
     * @param cb (optional) result receiver to receive callback. Use this only when a wake lock is
     *           needed.
     * @return {@code true} if the attempt to send media button was successfuly.
     * @return {@code true} if the attempt to send media button was successfully.
     *         {@code false} otherwise.
     */
    public boolean sendMediaButton(String packageName, int pid, int uid, boolean asSystemService,
@@ -392,6 +403,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, AutoCloseable
                cb);
    }

    @Override
    public void dump(PrintWriter pw, String prefix) {
        pw.println(prefix + mTag + " " + this);

@@ -712,7 +724,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, AutoCloseable
        public void destroySession() throws RemoteException {
            final long token = Binder.clearCallingIdentity();
            try {
                mService.destroySession(MediaSessionRecord.this);
                mService.sessionDied(MediaSessionRecord.this);
            } finally {
                Binder.restoreCallingIdentity(token);
            }
+143 −0
Original line number Diff line number Diff line
/*
 * Copyright 2019 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.media.AudioManager;
import android.os.ResultReceiver;
import android.view.KeyEvent;

import java.io.PrintWriter;

/**
 * Common interfaces between {@link MediaSessionRecord} and {@link MediaSession2Record}.
 */
public interface MediaSessionRecordImpl extends AutoCloseable {

    /**
     * Get the info for this session.
     *
     * @return Info that identifies this session.
     */
    String getPackageName();

    /**
     * Get the UID this session was created for.
     *
     * @return The UID for this session.
     */
    int getUid();

    /**
     * Get the user id this session was created for.
     *
     * @return The user id for this session.
     */
    int getUserId();

    /**
     * Check if this session has system priorty and should receive media buttons
     * before any other sessions.
     *
     * @return True if this is a system priority session, false otherwise
     */
    boolean isSystemPriority();

    /**
     * Send a volume adjustment to the session owner. Direction must be one of
     * {@link AudioManager#ADJUST_LOWER}, {@link AudioManager#ADJUST_RAISE},
     * {@link AudioManager#ADJUST_SAME}.
     *
     * @param packageName The package that made the original volume request.
     * @param opPackageName The op package that made the original volume request.
     * @param pid The pid that made the original volume request.
     * @param uid The uid that made the original volume request.
     * @param asSystemService {@code true} if the event sent to the session as if it was come from
     *          the system service instead of the app process. This helps sessions to distinguish
     *          between the key injection by the app and key events from the hardware devices.
     *          Should be used only when the volume key events aren't handled by foreground
     *          activity. {@code false} otherwise to tell session about the real caller.
     * @param direction The direction to adjust volume in.
     * @param flags Any of the flags from {@link AudioManager}.
     * @param useSuggested True to use adjustSuggestedStreamVolume instead of
     */
    void adjustVolume(String packageName, String opPackageName, int pid, int uid,
            boolean asSystemService, int direction, int flags, boolean useSuggested);

    /**
     * Check if this session has been set to active by the app. (i.e. ready to receive command and
     * getters are available).
     *
     * @return True if the session is active, false otherwise.
     */
    // TODO(jaewan): Find better naming, or remove this from the MediaSessionRecordImpl.
    boolean isActive();

    /**
     * Check if the session's playback active state matches with the expectation. This always return
     * {@code false} if the playback state is unknown (e.g. {@code null}), where we cannot know the
     * actual playback state associated with the session.
     *
     * @param expected True if playback is expected to be active. false otherwise.
     * @return True if the session's playback matches with the expectation. false otherwise.
     */
    boolean checkPlaybackActiveState(boolean expected);

    /**
     * Check whether the playback type is local or remote.
     * <p>
     * <ul>
     *   <li>Local: volume changes the stream volume because playback happens on this device.</li>
     *   <li>Remote: volume is sent to the apps callback because playback happens on the remote
     *     device and we cannot know how to control the volume of it.</li>
     * </ul>
     *
     * @return {@code true} if the playback is local. {@code false} if the playback is remote.
     */
    boolean isPlaybackTypeLocal();

    /**
     * Sends media button.
     *
     * @param packageName caller package name
     * @param pid caller pid
     * @param uid caller uid
     * @param asSystemService {@code true} if the event sent to the session as if it was come from
     *          the system service instead of the app process.
     * @param ke key events
     * @param sequenceId (optional) sequence id. Use this only when a wake lock is needed.
     * @param cb (optional) result receiver to receive callback. Use this only when a wake lock is
     *           needed.
     * @return {@code true} if the attempt to send media button was successfully.
     *         {@code false} otherwise.
     */
    boolean sendMediaButton(String packageName, int pid, int uid, boolean asSystemService,
            KeyEvent ke, int sequenceId, ResultReceiver cb);

    /**
     * Dumps internal state
     *
     * @param pw print writer
     * @param prefix prefix
     */
    void dump(PrintWriter pw, String prefix);

    /**
     * Override {@link AutoCloseable#close} to tell not to throw exception.
     */
    @Override
    void close();
}
+64 −84
Original line number Diff line number Diff line
@@ -40,10 +40,7 @@ import android.media.AudioManager;
import android.media.AudioManagerInternal;
import android.media.AudioPlaybackConfiguration;
import android.media.AudioSystem;
import android.media.IAudioService;
import android.media.IRemoteVolumeController;
import android.media.MediaController2;
import android.media.Session2CommandGroup;
import android.media.Session2Token;
import android.media.session.IActiveSessionsListener;
import android.media.session.IOnMediaKeyEventDispatchedListener;
@@ -61,7 +58,6 @@ import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.IBinder;
import android.os.Message;
import android.os.PowerManager;
@@ -127,7 +123,8 @@ public class MediaSessionService extends SystemService implements Monitor {
    // TODO: Keep session2 info in MediaSessionStack for prioritizing both session1 and session2 in
    //       one place.
    @GuardedBy("mLock")
    private final SparseArray<List<Session2Token>> mSession2TokensPerUser = new SparseArray<>();
    private final SparseArray<List<MediaSession2Record>> mSession2RecordsPerUser =
            new SparseArray<>();
    @GuardedBy("mLock")
    private final List<Session2TokensListenerRecord> mSession2TokensListenerRecords =
            new ArrayList<>();
@@ -189,11 +186,6 @@ public class MediaSessionService extends SystemService implements Monitor {
        updateUser();
    }

    private IAudioService getAudioService() {
        IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);
        return IAudioService.Stub.asInterface(b);
    }

    private boolean isGlobalPriorityActiveLocked() {
        return mGlobalPrioritySession != null && mGlobalPrioritySession.isActive();
    }
@@ -266,11 +258,21 @@ public class MediaSessionService extends SystemService implements Monitor {
    List<Session2Token> getSession2TokensLocked(int userId) {
        List<Session2Token> list = new ArrayList<>();
        if (userId == USER_ALL) {
            for (int i = 0; i < mSession2TokensPerUser.size(); i++) {
                list.addAll(mSession2TokensPerUser.valueAt(i));
            for (int i = 0; i < mSession2RecordsPerUser.size(); i++) {
                List<MediaSession2Record> records = mSession2RecordsPerUser.valueAt(i);
                for (MediaSession2Record record: records) {
                    if (record.isActive()) {
                        list.add(record.getSession2Token());
                    }
                }
            }
        } else {
            list.addAll(mSession2TokensPerUser.get(userId));
            List<MediaSession2Record> records = mSession2RecordsPerUser.get(userId);
            for (MediaSession2Record record: records) {
                if (record.isActive()) {
                    list.add(record.getSession2Token());
                }
            }
        }
        return list;
    }
@@ -347,7 +349,13 @@ public class MediaSessionService extends SystemService implements Monitor {
                    user.destroySessionsForUserLocked(userId);
                }
            }
            mSession2TokensPerUser.remove(userId);
            List<MediaSession2Record> list = mSession2RecordsPerUser.get(userId);
            if (list != null) {
                for (MediaSession2Record session : list) {
                    session.close();
                }
                mSession2RecordsPerUser.remove(userId);
            }
            updateUser();
        }
    }
@@ -372,9 +380,17 @@ public class MediaSessionService extends SystemService implements Monitor {
        }
    }

    void destroySession(MediaSessionRecord session) {
    void pushSession2TokensChanged(MediaSession2Record sessionRecord) {
        synchronized (mLock) {
            destroySessionLocked(session);
            pushSession2TokensChangedLocked(sessionRecord.getUserId());
        }
    }

    void sessionDied(MediaSession2Record sessionRecord) {
        synchronized (mLock) {
            int userId = sessionRecord.getUserId();
            mSession2RecordsPerUser.get(userId).remove(sessionRecord);
            pushSession2TokensChangedLocked(userId);
        }
    }

@@ -393,8 +409,8 @@ public class MediaSessionService extends SystemService implements Monitor {
                            mUserRecords.put(userInfo.id, new FullUserRecord(userInfo.id));
                        }
                    }
                    if (mSession2TokensPerUser.get(userInfo.id) == null) {
                        mSession2TokensPerUser.put(userInfo.id, new ArrayList<>());
                    if (mSession2RecordsPerUser.get(userInfo.id) == null) {
                        mSession2RecordsPerUser.put(userInfo.id, new ArrayList<>());
                    }
                }
            }
@@ -405,8 +421,8 @@ public class MediaSessionService extends SystemService implements Monitor {
                Log.w(TAG, "Cannot find FullUserInfo for the current user " + currentFullUserId);
                mCurrentFullUserRecord = new FullUserRecord(currentFullUserId);
                mUserRecords.put(currentFullUserId, mCurrentFullUserRecord);
                if (mSession2TokensPerUser.get(currentFullUserId) == null) {
                    mSession2TokensPerUser.put(currentFullUserId, new ArrayList<>());
                if (mSession2RecordsPerUser.get(currentFullUserId) == null) {
                    mSession2RecordsPerUser.put(currentFullUserId, new ArrayList<>());
                }
            }
            mFullUserIds.put(currentFullUserId, currentFullUserId);
@@ -541,15 +557,6 @@ public class MediaSessionService extends SystemService implements Monitor {
        return false;
    }

    private MediaSessionRecord createSessionInternal(int callerPid, int callerUid, int userId,
            String callerPackageName, ISessionCallback cb, String tag, Bundle sessionInfo)
            throws RemoteException {
        synchronized (mLock) {
            return createSessionLocked(callerPid, callerUid, userId, callerPackageName, cb,
                    tag, sessionInfo);
        }
    }

    /*
     * When a session is created the following things need to happen.
     * 1. Its callback binder needs a link to death
@@ -557,8 +564,9 @@ public class MediaSessionService extends SystemService implements Monitor {
     * 3. It needs to be added to the priority stack.
     * 4. It needs to be added to the relevant user record.
     */
    private MediaSessionRecord createSessionLocked(int callerPid, int callerUid, int userId,
    private MediaSessionRecord createSessionInternal(int callerPid, int callerUid, int userId,
            String callerPackageName, ISessionCallback cb, String tag, Bundle sessionInfo) {
        synchronized (mLock) {
            FullUserRecord user = getFullUserRecordLocked(userId);
            if (user == null) {
                Log.w(TAG, "Request from invalid user: " +  userId + ", pkg=" + callerPackageName);
@@ -581,6 +589,7 @@ public class MediaSessionService extends SystemService implements Monitor {
            }
            return session;
        }
    }

    private int findIndexOfSessionsListenerLocked(IActiveSessionsListener listener) {
        for (int i = mSessionsListeners.size() - 1; i >= 0; i--) {
@@ -869,13 +878,13 @@ public class MediaSessionService extends SystemService implements Monitor {
                    + mRestoredMediaButtonReceiverComponentType);
            mPriorityStack.dump(pw, indent);
            pw.println(indent + "Session2Tokens:");
            for (int i = 0; i < mSession2TokensPerUser.size(); i++) {
                List<Session2Token> list = mSession2TokensPerUser.valueAt(i);
            for (int i = 0; i < mSession2RecordsPerUser.size(); i++) {
                List<MediaSession2Record> list = mSession2RecordsPerUser.valueAt(i);
                if (list == null || list.size() == 0) {
                    continue;
                }
                for (Session2Token token : list) {
                    pw.println(indent + "  " + token);
                for (MediaSession2Record record : list) {
                    record.dump(pw, indent);
                }
            }
        }
@@ -1132,14 +1141,10 @@ public class MediaSessionService extends SystemService implements Monitor {
                    throw new SecurityException("Unexpected Session2Token's UID, expected=" + uid
                            + " but actually=" + sessionToken.getUid());
                }
                Controller2Callback callback = new Controller2Callback(sessionToken);
                // Note: It's safe not to keep controller here because it wouldn't be GC'ed until
                //       it's closed.
                // TODO: Keep controller as well for better readability
                //       because the GC behavior isn't straightforward.
                MediaController2 controller = new MediaController2.Builder(mContext, sessionToken)
                        .setControllerCallback(new HandlerExecutor(mHandler), callback)
                        .build();
                MediaSession2Record record = new MediaSession2Record(
                        sessionToken, MediaSessionService.this, mHandler.getLooper());
                mSession2RecordsPerUser.get(record.getUserId()).add(record);
                // Do not immediately notify changes -- do so when framework can dispatch command
            } finally {
                Binder.restoreCallingIdentity(token);
            }
@@ -2424,29 +2429,4 @@ public class MediaSessionService extends SystemService implements Monitor {
        }
    }

    private class Controller2Callback extends MediaController2.ControllerCallback {
        private final Session2Token mToken;

        Controller2Callback(Session2Token token) {
            mToken = token;
        }

        @Override
        public void onConnected(MediaController2 controller, Session2CommandGroup allowedCommands) {
            synchronized (mLock) {
                int userId = UserHandle.getUserId(mToken.getUid());
                mSession2TokensPerUser.get(userId).add(mToken);
                pushSession2TokensChangedLocked(userId);
            }
        }

        @Override
        public void onDisconnected(MediaController2 controller) {
            synchronized (mLock) {
                int userId = UserHandle.getUserId(mToken.getUid());
                mSession2TokensPerUser.get(userId).remove(mToken);
                pushSession2TokensChangedLocked(userId);
            }
        }
    }
}
Loading