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

Commit f6f272f8 authored by Jaewan Kim's avatar Jaewan Kim
Browse files

MediaSession2: Use Executor for callback handling

This also simplifies future work for adding more functions
Test: Run all MediaComponents test once

Change-Id: Ib9aebd9212368d616dba99792d6ed13b24617885
parent de9a7b5c
Loading
Loading
Loading
Loading
+10 −10
Original line number Diff line number Diff line
@@ -222,38 +222,38 @@ public class MediaController2Impl implements MediaController2Provider {

    @Override
    public void play_impl() {
        sendCommand(MediaSession2.COMMAND_CODE_PLAYBACK_START);
        sendTransportControlCommand(MediaSession2.COMMAND_CODE_PLAYBACK_START);
    }

    @Override
    public void pause_impl() {
        sendCommand(MediaSession2.COMMAND_CODE_PLAYBACK_PAUSE);
        sendTransportControlCommand(MediaSession2.COMMAND_CODE_PLAYBACK_PAUSE);
    }

    @Override
    public void stop_impl() {
        sendCommand(MediaSession2.COMMAND_CODE_PLAYBACK_STOP);
        sendTransportControlCommand(MediaSession2.COMMAND_CODE_PLAYBACK_STOP);
    }

    @Override
    public void skipToPrevious_impl() {
        sendCommand(MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_PREV_ITEM);
        sendTransportControlCommand(MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_PREV_ITEM);
    }

    @Override
    public void skipToNext_impl() {
        sendCommand(MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM);
        sendTransportControlCommand(MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM);
    }

    private void sendCommand(int code) {
        // TODO(jaewan): optimization) Cache Command objects?
        Command command = new Command(code);
        // TODO(jaewan): Check if the command is in the allowed group.
    private void sendTransportControlCommand(int commandCode) {
        sendTransportControlCommand(commandCode, 0);
    }

    private void sendTransportControlCommand(int commandCode, long arg) {
        final IMediaSession2 binder = mSessionBinder;
        if (binder != null) {
            try {
                binder.sendCommand(mSessionCallbackStub, command.toBundle(), null);
                binder.sendTransportControlCommand(mSessionCallbackStub, commandCode, arg);
            } catch (RemoteException e) {
                Log.w(TAG, "Cannot connect to the service or the session is gone", e);
            }
+50 −33
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ import android.media.AudioAttributes;
import android.media.IMediaSession2Callback;
import android.media.MediaItem2;
import android.media.MediaPlayerBase;
import android.media.MediaPlayerBase.PlaybackListener;
import android.media.MediaSession2;
import android.media.MediaSession2.Builder;
import android.media.MediaSession2.Command;
@@ -39,11 +40,11 @@ import android.media.VolumeProvider;
import android.media.session.MediaSessionManager;
import android.media.update.MediaSession2Provider;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.ResultReceiver;
import android.support.annotation.GuardedBy;
import android.util.Log;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
@@ -53,20 +54,21 @@ public class MediaSession2Impl implements MediaSession2Provider {
    private static final String TAG = "MediaSession2";
    private static final boolean DEBUG = true;//Log.isLoggable(TAG, Log.DEBUG);

    private final MediaSession2 mInstance;
    private final Object mLock = new Object();

    private final MediaSession2 mInstance;
    private final Context mContext;
    private final String mId;
    private final Handler mHandler;
    private final Executor mCallbackExecutor;
    private final SessionCallback mCallback;
    private final MediaSession2Stub mSessionStub;
    private final SessionToken2 mSessionToken;
    private final List<PlaybackListenerHolder> mListeners = new ArrayList<>();

    @GuardedBy("mLock")
    private MediaPlayerBase mPlayer;

    private final List<PlaybackListenerHolder> mListeners = new ArrayList<>();
    @GuardedBy("mLock")
    private MyPlaybackListener mListener;
    private MediaSession2 instance;

    /**
     * Can be only called by the {@link Builder#build()}.
@@ -90,9 +92,9 @@ public class MediaSession2Impl implements MediaSession2Provider {
        // Initialize finals first.
        mContext = context;
        mId = id;
        mHandler = new Handler(Looper.myLooper());
        mCallback = callback;
        mCallbackExecutor = callbackExecutor;
        mSessionStub = new MediaSession2Stub(this, callback);
        mSessionStub = new MediaSession2Stub(this);
        // Ask server to create session token for following reasons.
        //   1. Make session ID unique per package.
        //      Server can only know if the package has another process and has another session
@@ -118,7 +120,8 @@ public class MediaSession2Impl implements MediaSession2Provider {
    //               setPlayer(null). Token can be available when player is null, and
    //               controller can also attach to session.
    @Override
    public void setPlayer_impl(MediaPlayerBase player, VolumeProvider volumeProvider) throws IllegalArgumentException {
    public void setPlayer_impl(MediaPlayerBase player, VolumeProvider volumeProvider)
            throws IllegalArgumentException {
        ensureCallingThread();
        if (player == null) {
            throw new IllegalArgumentException("player shouldn't be null");
@@ -127,26 +130,24 @@ public class MediaSession2Impl implements MediaSession2Provider {
    }

    private void setPlayerInternal(MediaPlayerBase player) {
        synchronized (mLock) {
            if (mPlayer == player) {
                // Player didn't changed. No-op.
                return;
            }
        // TODO(jaewan): Find equivalent for the executor
        //mHandler.removeCallbacksAndMessages(null);
            if (mPlayer != null && mListener != null) {
                // This might not work for a poorly implemented player.
                mPlayer.removePlaybackListener(mListener);
            }
            mListener = new MyPlaybackListener(this, player);
            player.addPlaybackListener(mCallbackExecutor, mListener);
        notifyPlaybackStateChanged(player.getPlaybackState());
            mPlayer = player;
        }
        notifyPlaybackStateChangedNotLocked(player.getPlaybackState());
    }

    @Override
    public void close_impl() {
        // Flush any pending messages.
        mHandler.removeCallbacksAndMessages(null);
        if (mSessionStub != null) {
            if (DEBUG) {
                Log.d(TAG, "session is now unavailable, id=" + mId);
@@ -154,6 +155,14 @@ public class MediaSession2Impl implements MediaSession2Provider {
            // Invalidate previously published session stub.
            mSessionStub.destroyNotLocked();
        }
        synchronized (mLock) {
            if (mPlayer != null) {
                // close can be called multiple times
                mPlayer.removePlaybackListener(mListener);
                mPlayer = null;
                return;
            }
        }
    }

    @Override
@@ -321,14 +330,14 @@ public class MediaSession2Impl implements MediaSession2Provider {
        }
    }

    Handler getHandler() {
        return mHandler;
    private void notifyPlaybackStateChangedNotLocked(PlaybackState2 state) {
        List<PlaybackListenerHolder> listeners = new ArrayList<>();
        synchronized (mLock) {
            listeners.addAll(mListeners);
        }

    private void notifyPlaybackStateChanged(PlaybackState2 state) {
        // Notify to listeners added directly to this session
        for (int i = 0; i < mListeners.size(); i++) {
            mListeners.get(i).postPlaybackChange(state);
        for (int i = 0; i < listeners.size(); i++) {
            listeners.get(i).postPlaybackChange(state);
        }
        // Notify to controllers as well.
        mSessionStub.notifyPlaybackStateChangedNotLocked(state);
@@ -346,6 +355,14 @@ public class MediaSession2Impl implements MediaSession2Provider {
        return mPlayer;
    }

    Executor getCallbackExecutor() {
        return mCallbackExecutor;
    }

    SessionCallback getCallback() {
        return mCallback;
    }

    private static class MyPlaybackListener implements MediaPlayerBase.PlaybackListener {
        private final WeakReference<MediaSession2Impl> mSession;
        private final MediaPlayerBase mPlayer;
@@ -363,7 +380,7 @@ public class MediaSession2Impl implements MediaSession2Provider {
                        new IllegalStateException());
                return;
            }
            session.notifyPlaybackStateChanged(state);
            session.notifyPlaybackStateChangedNotLocked(state);
        }
    }

+102 −150
Original line number Diff line number Diff line
@@ -51,30 +51,19 @@ public class MediaSession2Stub extends IMediaSession2.Stub {
    private static final boolean DEBUG = true; // TODO(jaewan): Rename.

    private final Object mLock = new Object();
    private final CommandHandler mCommandHandler;
    private final WeakReference<MediaSession2Impl> mSession;
    private final Context mContext;
    private final SessionCallback mSessionCallback;
    private final MediaLibrarySessionCallback mLibraryCallback;

    @GuardedBy("mLock")
    private final ArrayMap<IBinder, ControllerInfo> mControllers = new ArrayMap<>();

    public MediaSession2Stub(MediaSession2Impl session, SessionCallback callback) {
    public MediaSession2Stub(MediaSession2Impl session) {
        mSession = new WeakReference<>(session);
        mContext = session.getContext();
        // TODO(jaewan): Should be executor from the session builder
        mCommandHandler = new CommandHandler(session.getHandler().getLooper());
        mSessionCallback = callback;
        mLibraryCallback = (callback instanceof MediaLibrarySessionCallback)
                ? (MediaLibrarySessionCallback) callback : null;
    }

    public void destroyNotLocked() {
        final List<ControllerInfo> list;
        synchronized (mLock) {
            mSession.clear();
            mCommandHandler.removeCallbacksAndMessages(null);
            list = getControllers();
            mControllers.clear();
        }
@@ -99,14 +88,43 @@ public class MediaSession2Stub extends IMediaSession2.Stub {
    }

    @Override
    public void connect(String callingPackage, IMediaSession2Callback callback) {
        if (callback == null) {
            // Requesting connect without callback to receive result.
    public void connect(String callingPackage, IMediaSession2Callback callback)
            throws RuntimeException {
        final MediaSession2Impl sessionImpl = getSession();
        final ControllerInfo request = new ControllerInfo(sessionImpl.getContext(),
                Binder.getCallingUid(), Binder.getCallingPid(), callingPackage, callback);
        sessionImpl.getCallbackExecutor().execute(() -> {
            final MediaSession2Impl session = mSession.get();
            if (session == null) {
                return;
            }
        ControllerInfo request = new ControllerInfo(mContext,
                Binder.getCallingUid(), Binder.getCallingPid(), callingPackage, callback);
        mCommandHandler.postConnect(request);
            CommandGroup allowedCommands = session.getCallback().onConnect(request);
            // Don't reject connection for the request from trusted app.
            // Otherwise server will fail to retrieve session's information to dispatch
            // media keys to.
            boolean accept = allowedCommands != null || request.isTrusted();
            ControllerInfoImpl impl = ControllerInfoImpl.from(request);
            if (accept) {
                synchronized (mLock) {
                    mControllers.put(impl.getId(), request);
                }
                if (allowedCommands == null) {
                    // For trusted apps, send non-null allowed commands to keep connection.
                    allowedCommands = new CommandGroup();
                }
            }
            if (DEBUG) {
                Log.d(TAG, "onConnectResult, request=" + request
                        + " accept=" + accept);
            }
            try {
                impl.getControllerBinder().onConnectionChanged(
                        accept ? MediaSession2Stub.this : null,
                        allowedCommands == null ? null : allowedCommands.toBundle());
            } catch (RemoteException e) {
                // Controller may be died prematurely.
            }
        });
    }

    @Override
@@ -122,20 +140,64 @@ public class MediaSession2Stub extends IMediaSession2.Stub {
    @Override
    public void sendCommand(IMediaSession2Callback caller, Bundle command, Bundle args)
            throws RuntimeException {
        ControllerInfo controller = getController(caller);
        // TODO(jaewan): Generic command
    }

    @Override
    public void sendTransportControlCommand(IMediaSession2Callback caller,
            int commandCode, long arg) throws RuntimeException {
        final MediaSession2Impl sessionImpl = getSession();
        final ControllerInfo controller = getController(caller);
        if (controller == null) {
            if (DEBUG) {
                Log.d(TAG, "Command from a controller that hasn't connected. Ignore");
            }
            return;
        }
        mCommandHandler.postCommand(controller, Command.fromBundle(command), args);
        sessionImpl.getCallbackExecutor().execute(() -> {
            final MediaSession2Impl session = mSession.get();
            if (session == null) {
                return;
            }
            // TODO(jaewan): Sanity check.
            Command command = new Command(commandCode);
            boolean accepted = session.getCallback().onCommandRequest(controller, command);
            if (!accepted) {
                // Don't run rejected command.
                if (DEBUG) {
                    Log.d(TAG, "Command " + commandCode + " from "
                            + controller + " was rejected by " + session);
                }
                return;
            }

            switch (commandCode) {
                case MediaSession2.COMMAND_CODE_PLAYBACK_START:
                    session.getInstance().play();
                    break;
                case MediaSession2.COMMAND_CODE_PLAYBACK_PAUSE:
                    session.getInstance().pause();
                    break;
                case MediaSession2.COMMAND_CODE_PLAYBACK_STOP:
                    session.getInstance().stop();
                    break;
                case MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_PREV_ITEM:
                    session.getInstance().skipToPrevious();
                    break;
                case MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM:
                    session.getInstance().skipToNext();
                    break;
                default:
                    // TODO(jaewan): Resend unknown (new) commands through the custom command.
            }
        });
    }

    @Override
    public void getBrowserRoot(IMediaSession2Callback caller, Bundle rootHints)
            throws RuntimeException {
        if (mLibraryCallback == null) {
        final MediaSession2Impl sessionImpl = getSession();
        if (!(sessionImpl.getCallback() instanceof MediaLibrarySessionCallback)) {
            if (DEBUG) {
                Log.d(TAG, "Session cannot hand getBrowserRoot()");
            }
@@ -148,7 +210,24 @@ public class MediaSession2Stub extends IMediaSession2.Stub {
            }
            return;
        }
        mCommandHandler.postOnGetRoot(controller, rootHints);
        sessionImpl.getCallbackExecutor().execute(() -> {
            final MediaSession2Impl session = mSession.get();
            if (session == null) {
                return;
            }
            final MediaLibrarySessionCallback libraryCallback =
                    (MediaLibrarySessionCallback) session.getCallback();
            final ControllerInfoImpl controllerImpl = ControllerInfoImpl.from(controller);
            BrowserRoot root = libraryCallback.onGetRoot(controller, rootHints);
            try {
                controllerImpl.getControllerBinder().onGetRootResult(rootHints,
                        root == null ? null : root.getRootId(),
                        root == null ? null : root.getExtras());
            } catch (RemoteException e) {
                // Controller may be died prematurely.
                // TODO(jaewan): Handle this.
            }
        });
    }

    @Deprecated
@@ -250,131 +329,4 @@ public class MediaSession2Stub extends IMediaSession2.Stub {
            // TODO(jaewan): What to do when the controller is gone?
        }
    }

    // TODO(jaewan): Remove this. We should use Executor given by the session builder.
    private class CommandHandler extends Handler {
        public static final int MSG_CONNECT = 1000;
        public static final int MSG_COMMAND = 1001;
        public static final int MSG_ON_GET_ROOT = 2000;

        public CommandHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            final MediaSession2Impl session = MediaSession2Stub.this.mSession.get();
            if (session == null || session.getPlayer() == null) {
                return;
            }

            switch (msg.what) {
                case MSG_CONNECT: {
                    ControllerInfo request = (ControllerInfo) msg.obj;
                    CommandGroup allowedCommands = mSessionCallback.onConnect(request);
                    // Don't reject connection for the request from trusted app.
                    // Otherwise server will fail to retrieve session's information to dispatch
                    // media keys to.
                    boolean accept = allowedCommands != null || request.isTrusted();
                    ControllerInfoImpl impl = ControllerInfoImpl.from(request);
                    if (accept) {
                        synchronized (mLock) {
                            mControllers.put(impl.getId(), request);
                        }
                        if (allowedCommands == null) {
                            // For trusted apps, send non-null allowed commands to keep connection.
                            allowedCommands = new CommandGroup();
                        }
                    }
                    if (DEBUG) {
                        Log.d(TAG, "onConnectResult, request=" + request
                                + " accept=" + accept);
                    }
                    try {
                        impl.getControllerBinder().onConnectionChanged(
                                accept ? MediaSession2Stub.this : null,
                                allowedCommands == null ? null : allowedCommands.toBundle());
                    } catch (RemoteException e) {
                        // Controller may be died prematurely.
                    }
                    break;
                }
                case MSG_COMMAND: {
                    CommandParam param = (CommandParam) msg.obj;
                    Command command = param.command;
                    boolean accepted = mSessionCallback.onCommandRequest(
                            param.controller, command);
                    if (!accepted) {
                        // Don't run rejected command.
                        if (DEBUG) {
                            Log.d(TAG, "Command " + command + " from "
                                    + param.controller + " was rejected by " + session);
                        }
                        return;
                    }

                    switch (param.command.getCommandCode()) {
                        case MediaSession2.COMMAND_CODE_PLAYBACK_START:
                            session.getInstance().play();
                            break;
                        case MediaSession2.COMMAND_CODE_PLAYBACK_PAUSE:
                            session.getInstance().pause();
                            break;
                        case MediaSession2.COMMAND_CODE_PLAYBACK_STOP:
                            session.getInstance().stop();
                            break;
                        case MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_PREV_ITEM:
                            session.getInstance().skipToPrevious();
                            break;
                        case MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM:
                            session.getInstance().skipToNext();
                            break;
                        default:
                            // TODO(jaewan): Handle custom command.
                    }
                    break;
                }
                case MSG_ON_GET_ROOT: {
                    final CommandParam param = (CommandParam) msg.obj;
                    final ControllerInfoImpl controller = ControllerInfoImpl.from(param.controller);
                    BrowserRoot root = mLibraryCallback.onGetRoot(param.controller, param.args);
                    try {
                        controller.getControllerBinder().onGetRootResult(param.args,
                                root == null ? null : root.getRootId(),
                                root == null ? null : root.getExtras());
                    } catch (RemoteException e) {
                        // Controller may be died prematurely.
                        // TODO(jaewan): Handle this.
                    }
                    break;
                }
            }
        }

        public void postConnect(ControllerInfo request) {
            obtainMessage(MSG_CONNECT, request).sendToTarget();
        }

        public void postCommand(ControllerInfo controller, Command command, Bundle args) {
            CommandParam param = new CommandParam(controller, command, args);
            obtainMessage(MSG_COMMAND, param).sendToTarget();
        }

        public void postOnGetRoot(ControllerInfo controller, Bundle rootHints) {
            CommandParam param = new CommandParam(controller, null, rootHints);
            obtainMessage(MSG_ON_GET_ROOT, param).sendToTarget();
        }
    }

    private static class CommandParam {
        public final ControllerInfo controller;
        public final Command command;
        public final Bundle args;

        private CommandParam(ControllerInfo controller, Command command, Bundle args) {
            this.controller = controller;
            this.command = command;
            this.args = args;
        }
    }
}
+0 −5
Original line number Diff line number Diff line
@@ -152,11 +152,6 @@ public class MediaSessionService2Impl implements MediaSessionService2Provider {
                return;
            }
            MediaSession2Impl impl = (MediaSession2Impl) mSession.getProvider();
            if (impl.getHandler().getLooper() != Looper.myLooper()) {
                Log.w(TAG, "Ignoring " + state + ". Expected " + impl.getHandler().getLooper()
                        + " but " + Looper.myLooper());
                return;
            }
            updateNotification(state);
        }
    }
+12 −11
Original line number Diff line number Diff line
@@ -45,6 +45,7 @@ import static org.junit.Assert.*;
 */
// TODO(jaewan): Implement host-side test so controller and session can run in different processes.
// TODO(jaewan): Fix flaky failure -- see MediaController2Impl.getController()
// TODO(jaeawn): Revisit create/close session in the sHandler. It's no longer necessary.
@RunWith(AndroidJUnit4.class)
@SmallTest
@FlakyTest
@@ -60,11 +61,10 @@ public class MediaController2Test extends MediaSession2TestBase {
    public void setUp() throws Exception {
        super.setUp();
        // Create this test specific MediaSession2 to use our own Handler.
        sHandler.postAndSync(()->{
        mPlayer = new MockPlayer(1);
            mSession = new MediaSession2.Builder(mContext, mPlayer).setId(TAG).build();
        });

        mSession = new MediaSession2.Builder(mContext, mPlayer)
                .setSessionCallback(sHandlerExecutor, new SessionCallback())
                .setId(TAG).build();
        mController = createController(mSession.getToken());
        TestServiceRegistry.getInstance().setHandler(sHandler);
    }
@@ -73,11 +73,9 @@ public class MediaController2Test extends MediaSession2TestBase {
    @Override
    public void cleanUp() throws Exception {
        super.cleanUp();
        sHandler.postAndSync(() -> {
        if (mSession != null) {
            mSession.close();
        }
        });
        TestServiceRegistry.getInstance().cleanUp();
    }

@@ -275,6 +273,7 @@ public class MediaController2Test extends MediaSession2TestBase {
            final MockPlayer player = new MockPlayer(0);
            sessionHandler.postAndSync(() -> {
                mSession = new MediaSession2.Builder(mContext, mPlayer)
                        .setSessionCallback(sHandlerExecutor, new SessionCallback())
                        .setId("testDeadlock").build();
            });
            final MediaController2 controller = createController(mSession.getToken());
@@ -462,7 +461,9 @@ public class MediaController2Test extends MediaSession2TestBase {
        sHandler.postAndSync(() -> {
            // Recreated session has different session stub, so previously created controller
            // shouldn't be available.
            mSession = new MediaSession2.Builder(mContext, mPlayer).setId(id).build();
            mSession = new MediaSession2.Builder(mContext, mPlayer)
                    .setSessionCallback(sHandlerExecutor, new SessionCallback())
                    .setId(id).build();
        });
        testNoInteraction();
    }
Loading