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

Commit 98421b59 authored by Jaewan Kim's avatar Jaewan Kim
Browse files

MediaSession2: Add/remove playback listeners

Test: Run all MediaComponents test once
Change-Id: Ic24a67cbbead7a9d4d420fc03c8004cbd04f61b9
parent dd439786
Loading
Loading
Loading
Loading
+19 −34
Original line number Diff line number Diff line
@@ -56,13 +56,6 @@ public class MediaController2Impl implements MediaController2Provider {

    private final MediaController2 mInstance;

    /**
     * Flag used by MediaController2Record to filter playback callback.
     */
    static final int CALLBACK_FLAG_PLAYBACK = 0x1;

    static final int REQUEST_CODE_ALL = 0;

    private final Object mLock = new Object();

    private final Context mContext;
@@ -72,12 +65,12 @@ public class MediaController2Impl implements MediaController2Provider {
    private final Executor mCallbackExecutor;
    private final IBinder.DeathRecipient mDeathRecipient;

    @GuardedBy("mLock")
    private final List<PlaybackListenerHolder> mPlaybackListeners = new ArrayList<>();
    @GuardedBy("mLock")
    private SessionServiceConnection mServiceConnection;
    @GuardedBy("mLock")
    private boolean mIsReleased;
    @GuardedBy("mLock")
    private PlaybackState2 mPlaybackState;

    // Assignment should be used with the lock hold, but should be used without a lock to prevent
    // potential deadlock.
@@ -185,7 +178,6 @@ public class MediaController2Impl implements MediaController2Provider {
                mContext.unbindService(mServiceConnection);
                mServiceConnection = null;
            }
            mPlaybackListeners.clear();
            binder = mSessionBinder;
            mSessionBinder = null;
            mSessionCallbackStub.destroy();
@@ -373,8 +365,9 @@ public class MediaController2Impl implements MediaController2Provider {

    @Override
    public PlaybackState2 getPlaybackState_impl() {
        // TODO(jaewan): Implement
        return null;
        synchronized (mLock) {
            return mPlaybackState;
        }
    }

    @Override
@@ -401,24 +394,15 @@ public class MediaController2Impl implements MediaController2Provider {
    ///////////////////////////////////////////////////
    // Protected or private methods
    ///////////////////////////////////////////////////
    // Should be used without a lock to prevent potential deadlock.
    private void registerCallbackForPlaybackNotLocked() {
        final IMediaSession2 binder = mSessionBinder;
        if (binder != null) {
            try {
                binder.registerCallback(mSessionCallbackStub,
                        CALLBACK_FLAG_PLAYBACK, REQUEST_CODE_ALL);
            } catch (RemoteException e) {
                Log.e(TAG, "Cannot connect to the service or the session is gone", e);
            }
        }
    }

    private void pushPlaybackStateChanges(final PlaybackState2 state) {
        synchronized (mLock) {
            for (int i = 0; i < mPlaybackListeners.size(); i++) {
                mPlaybackListeners.get(i).postPlaybackChange(state);
            mPlaybackState = state;
            mCallbackExecutor.execute(() -> {
                if (!mInstance.isConnected()) {
                    return;
                }
                mCallback.onPlaybackStateChanged(state);
            });
        }
    }

@@ -437,7 +421,6 @@ public class MediaController2Impl implements MediaController2Provider {
                release = true;
                return;
            }
            boolean registerCallbackForPlaybackNeeded;
            synchronized (mLock) {
                if (mIsReleased) {
                    return;
@@ -460,15 +443,11 @@ public class MediaController2Impl implements MediaController2Provider {
                    release = true;
                    return;
                }
                registerCallbackForPlaybackNeeded = !mPlaybackListeners.isEmpty();
            }
            // TODO(jaewan): Keep commands to prevents illegal API calls.
            mCallbackExecutor.execute(() -> {
                mCallback.onConnected(commandGroup);
            });
            if (registerCallbackForPlaybackNeeded) {
                registerCallbackForPlaybackNotLocked();
            }
        } finally {
            if (release) {
                // Trick to call release() without holding the lock, to prevent potential deadlock
@@ -510,7 +489,13 @@ public class MediaController2Impl implements MediaController2Provider {

        @Override
        public void onPlaybackStateChanged(Bundle state) throws RuntimeException {
            final MediaController2Impl controller = getController();
            final MediaController2Impl controller;
            try {
                controller = getController();
            } catch (IllegalStateException e) {
                Log.w(TAG, "Don't fail silently here. Highly likely a bug");
                return;
            }
            controller.pushPlaybackStateChanges(PlaybackState2.fromBundle(state));
        }

+39 −18
Original line number Diff line number Diff line
@@ -32,6 +32,7 @@ import android.media.IMediaSession2Callback;
import android.media.MediaItem2;
import android.media.MediaLibraryService2;
import android.media.MediaPlayerInterface;
import android.media.MediaPlayerInterface.PlaybackListener;
import android.media.MediaSession2;
import android.media.MediaSession2.Builder;
import android.media.MediaSession2.Command;
@@ -379,6 +380,44 @@ public class MediaSession2Impl implements MediaSession2Provider {
        mPlayer.setCurrentPlaylistItem(index);
    }

    @Override
    public void addPlaybackListener_impl(Executor executor, PlaybackListener listener) {
        if (executor == null) {
            throw new IllegalArgumentException("executor shouldn't be null");
        }
        if (listener == null) {
            throw new IllegalArgumentException("listener shouldn't be null");
        }
        ensureCallingThread();
        if (PlaybackListenerHolder.contains(mListeners, listener)) {
            Log.w(TAG, "listener is already added. Ignoring.");
            return;
        }
        mListeners.add(new PlaybackListenerHolder(executor, listener));
        executor.execute(() -> listener.onPlaybackChanged(getInstance().getPlaybackState()));
    }

    @Override
    public void removePlaybackListener_impl(PlaybackListener listener) {
        if (listener == null) {
            throw new IllegalArgumentException("listener shouldn't be null");
        }
        ensureCallingThread();
        int idx = PlaybackListenerHolder.indexOf(mListeners, listener);
        if (idx >= 0) {
            mListeners.remove(idx);
        }
    }

    @Override
    public PlaybackState2 getPlaybackState_impl() {
        ensureCallingThread();
        ensurePlayer();
        // TODO(jaewan): Is it safe to be called on any thread?
        //               Otherwise we should cache the result from listener.
        return mPlayer.getPlaybackState();
    }

    ///////////////////////////////////////////////////
    // Protected or private methods
    ///////////////////////////////////////////////////
@@ -403,7 +442,6 @@ public class MediaSession2Impl implements MediaSession2Provider {
        }*/
    }


    private void ensurePlayer() {
        // TODO(jaewan): Should we pend command instead? Follow the decision from MP2.
        //               Alternatively we can add a API like setAcceptsPendingCommands(boolean).
@@ -473,11 +511,6 @@ public class MediaSession2Impl implements MediaSession2Provider {
        private final boolean mIsTrusted;
        private final IMediaSession2Callback mControllerBinder;

        // Flag to indicate which callbacks should be returned for the controller binder.
        // Either 0 or combination of {@link #CALLBACK_FLAG_PLAYBACK},
        // {@link #CALLBACK_FLAG_SESSION_ACTIVENESS}
        private int mFlag;

        public ControllerInfoImpl(Context context, ControllerInfo instance, int uid,
                int pid, String packageName, IMediaSession2Callback callback) {
            mInstance = instance;
@@ -561,18 +594,6 @@ public class MediaSession2Impl implements MediaSession2Provider {
            return mControllerBinder;
        }

        public boolean containsFlag(int flag) {
            return (mFlag & flag) != 0;
        }

        public void addFlag(int flag) {
            mFlag |= flag;
        }

        public void removeFlag(int flag) {
            mFlag &= ~flag;
        }

        public static ControllerInfoImpl from(ControllerInfo controller) {
            return (ControllerInfoImpl) controller.getProvider();
        }
+23 −55
Original line number Diff line number Diff line
@@ -16,8 +16,6 @@

package com.android.media;

import static com.android.media.MediaController2Impl.CALLBACK_FLAG_PLAYBACK;

import android.media.IMediaSession2;
import android.media.IMediaSession2Callback;
import android.media.MediaLibraryService2.BrowserRoot;
@@ -82,7 +80,7 @@ public class MediaSession2Stub extends IMediaSession2.Stub {
    }

    @Override
    public void connect(String callingPackage, IMediaSession2Callback callback)
    public void connect(String callingPackage, final IMediaSession2Callback callback)
            throws RuntimeException {
        final MediaSession2Impl sessionImpl = getSession();
        final ControllerInfo request = new ControllerInfo(sessionImpl.getContext(),
@@ -112,12 +110,29 @@ public class MediaSession2Stub extends IMediaSession2.Stub {
                        + " accept=" + accept);
            }
            try {
                impl.getControllerBinder().onConnectionChanged(
                callback.onConnectionChanged(
                        accept ? MediaSession2Stub.this : null,
                        allowedCommands == null ? null : allowedCommands.toBundle());
            } catch (RemoteException e) {
                // Controller may be died prematurely.
            }
            if (accept) {
                // If connection is accepted, notify the current state to the controller.
                // It's needed because we cannot call synchronous calls between session/controller.
                // Note: We're doing this after the onConnectionChanged(), but there's no guarantee
                //       that events here are notified after the onConnected() because
                //       IMediaSession2Callback is oneway (i.e. async call) and CallbackStub will
                //       use thread poll for incoming calls.
                // TODO(jaewan): Should we protect getting playback state?
                final PlaybackState2 state = session.getInstance().getPlaybackState();
                final Bundle bundle = state != null ? state.toBundle() : null;
                try {
                    callback.onPlaybackStateChanged(bundle);
                } catch (RemoteException e) {
                    // TODO(jaewan): Handle this.
                    // Controller may be died prematurely.
                }
            }
        });
    }

@@ -239,48 +254,13 @@ public class MediaSession2Stub extends IMediaSession2.Stub {
        });
    }

    @Deprecated
    @Override
    public Bundle getPlaybackState() throws RemoteException {
        MediaSession2Impl session = getSession();
        // TODO(jaewan): Check if mPlayer.getPlaybackState() is safe here.
        return session.getInstance().getPlayer().getPlaybackState().toBundle();
    }

    @Deprecated
    @Override
    public void registerCallback(final IMediaSession2Callback callbackBinder,
            final int callbackFlag, final int requestCode) throws RemoteException {
        // TODO(jaewan): Call onCommand() here. To do so, you should pend message.
        synchronized (mLock) {
            ControllerInfo controllerInfo = getController(callbackBinder);
            if (controllerInfo == null) {
                return;
            }
            ControllerInfoImpl.from(controllerInfo).addFlag(callbackFlag);
        }
    }

    @Deprecated
    @Override
    public void unregisterCallback(IMediaSession2Callback callbackBinder, int callbackFlag)
            throws RemoteException {
        // TODO(jaewan): Call onCommand() here. To do so, you should pend message.
        synchronized (mLock) {
            ControllerInfo controllerInfo = getController(callbackBinder);
            if (controllerInfo == null) {
                return;
            }
            ControllerInfoImpl.from(controllerInfo).removeFlag(callbackFlag);
        }
    }

    private ControllerInfo getController(IMediaSession2Callback caller) {
        synchronized (mLock) {
            return mControllers.get(caller.asBinder());
        }
    }

    // TODO(jaewan): Need a way to get controller with permissions
    public List<ControllerInfo> getControllers() {
        ArrayList<ControllerInfo> controllers = new ArrayList<>();
        synchronized (mLock) {
@@ -291,27 +271,15 @@ public class MediaSession2Stub extends IMediaSession2.Stub {
        return controllers;
    }

    public List<ControllerInfo> getControllersWithFlag(int flag) {
        ArrayList<ControllerInfo> controllers = new ArrayList<>();
        synchronized (mLock) {
            for (int i = 0; i < mControllers.size(); i++) {
                ControllerInfo controllerInfo = mControllers.valueAt(i);
                if (ControllerInfoImpl.from(controllerInfo).containsFlag(flag)) {
                    controllers.add(controllerInfo);
                }
            }
        }
        return controllers;
    }

    // Should be used without a lock to prevent potential deadlock.
    public void notifyPlaybackStateChangedNotLocked(PlaybackState2 state) {
        final List<ControllerInfo> list = getControllersWithFlag(CALLBACK_FLAG_PLAYBACK);
        final List<ControllerInfo> list = getControllers();
        for (int i = 0; i < list.size(); i++) {
            IMediaSession2Callback callbackBinder =
                    ControllerInfoImpl.from(list.get(i)).getControllerBinder();
            try {
                callbackBinder.onPlaybackStateChanged(state.toBundle());
                final Bundle bundle = state != null ? state.toBundle() : null;
                callbackBinder.onPlaybackStateChanged(bundle);
            } catch (RemoteException e) {
                Log.w(TAG, "Controller is gone", e);
                // TODO(jaewan): What to do when the controller is gone?
+1 −1
Original line number Diff line number Diff line
@@ -134,7 +134,7 @@ function runtest-MediaComponents() {
      ${adb} shell start
      ${adb} wait-for-device || break
      # Ensure package manager is loaded.
      sleep 5
      sleep 15

      # Install apks
      local install_failed="false"
+20 −1
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ import static junit.framework.Assert.assertTrue;
import android.content.Context;
import android.media.MediaBrowser2.BrowserCallback;
import android.media.MediaSession2.CommandGroup;
import android.media.MediaSession2.PlaylistParams;
import android.os.Bundle;
import android.support.annotation.CallSuper;
import android.support.annotation.NonNull;
@@ -100,10 +101,28 @@ public class MediaBrowser2Test extends MediaController2Test {
            disconnectLatch.countDown();
        }

        @Override
        public void onPlaybackStateChanged(PlaybackState2 state) {
            super.onPlaybackStateChanged(state);
            if (mCallbackProxy != null) {
                mCallbackProxy.onPlaybackStateChanged(state);
            }
        }

        @Override
        public void onPlaylistParamsChanged(PlaylistParams params) {
            super.onPlaylistParamsChanged(params);
            if (mCallbackProxy != null) {
                mCallbackProxy.onPlaylistParamsChanged(params);
            }
        }

        @Override
        public void onGetRootResult(Bundle rootHints, String rootMediaId, Bundle rootExtra) {
            if (mCallbackProxy != null) {
                mCallbackProxy.onGetRootResult(rootHints, rootMediaId, rootExtra);
            }
        }

        @Override
        public void waitForConnect(boolean expect) throws InterruptedException {
Loading