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

Commit 4a842111 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge changes from topics "sessionplayer2", "mediasession2_audio_api",...

Merge changes from topics "sessionplayer2", "mediasession2_audio_api", "session2_playbacklistener", "session2_timing"

* changes:
  MediaSession2: Initial commit of SessionPlayer2
  MediaSession2 API set for audio focus handling
  MediaSession2: Add/remove playback listeners
  MediaSession2: Fix timing issue
parents 222535c3 2bf325a5
Loading
Loading
Loading
Loading
+31 −40
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.
@@ -92,7 +85,6 @@ public class MediaController2Impl implements MediaController2Provider {
    public MediaController2Impl(Context context, MediaController2 instance, SessionToken2 token,
            Executor executor, ControllerCallback callback) {
        mInstance = instance;

        if (context == null) {
            throw new IllegalArgumentException("context shouldn't be null");
        }
@@ -115,21 +107,28 @@ public class MediaController2Impl implements MediaController2Provider {
        };

        mSessionBinder = null;
    }

        if (token.getSessionBinder() == null) {
    @Override
    public void initialize() {
        SessionToken2Impl impl = SessionToken2Impl.from(mToken);
        // TODO(jaewan): More sanity checks.
        if (impl.getSessionBinder() == null) {
            // Session service
            mServiceConnection = new SessionServiceConnection();
            connectToService();
        } else {
            // Session
            mServiceConnection = null;
            connectToSession(token.getSessionBinder());
            connectToSession(impl.getSessionBinder());
        }
    }

    // Should be only called by constructor.
    private void connectToService() {
        // Service. Needs to get fresh binder whenever connection is needed.
        SessionToken2Impl impl = SessionToken2Impl.from(mToken);
        final Intent intent = new Intent(MediaSessionService2.SERVICE_INTERFACE);
        intent.setClassName(mToken.getPackageName(), mToken.getServiceName());
        intent.setClassName(mToken.getPackageName(), impl.getServiceName());

        // Use bindService() instead of startForegroundService() to start session service for three
        // reasons.
@@ -166,7 +165,7 @@ public class MediaController2Impl implements MediaController2Provider {
    @Override
    public void close_impl() {
        if (DEBUG) {
            Log.d(TAG, "relese from " + mToken);
            Log.d(TAG, "release from " + mToken);
        }
        final IMediaSession2 binder;
        synchronized (mLock) {
@@ -179,7 +178,6 @@ public class MediaController2Impl implements MediaController2Provider {
                mContext.unbindService(mServiceConnection);
                mServiceConnection = null;
            }
            mPlaybackListeners.clear();
            binder = mSessionBinder;
            mSessionBinder = null;
            mSessionCallbackStub.destroy();
@@ -367,8 +365,9 @@ public class MediaController2Impl implements MediaController2Provider {

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

    @Override
@@ -395,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);
            });
        }
    }

@@ -431,7 +421,6 @@ public class MediaController2Impl implements MediaController2Provider {
                release = true;
                return;
            }
            boolean registerCallbackForPlaybackNeeded;
            synchronized (mLock) {
                if (mIsReleased) {
                    return;
@@ -454,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
@@ -504,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));
        }

+3 −4
Original line number Diff line number Diff line
@@ -26,6 +26,7 @@ import android.media.MediaPlayerInterface;
import android.media.MediaSession2;
import android.media.MediaSession2.ControllerInfo;
import android.media.MediaSessionService2;
import android.media.SessionToken2;
import android.media.VolumeProvider;
import android.media.update.MediaLibraryService2Provider;
import android.os.Bundle;
@@ -55,10 +56,8 @@ public class MediaLibraryService2Impl extends MediaSessionService2Impl implement
    }

    @Override
    Intent createServiceIntent() {
        Intent serviceIntent = new Intent(mInstance, mInstance.getClass());
        serviceIntent.setAction(MediaLibraryService2.SERVICE_INTERFACE);
        return serviceIntent;
    int getSessionType() {
        return SessionToken2.TYPE_LIBRARY_SERVICE;
    }

    public static class MediaLibrarySessionImpl extends MediaSession2Impl
+131 −53
Original line number Diff line number Diff line
@@ -16,15 +16,23 @@

package com.android.media;

import static android.media.SessionToken2.TYPE_LIBRARY_SERVICE;
import static android.media.SessionToken2.TYPE_SESSION;
import static android.media.SessionToken2.TYPE_SESSION_SERVICE;

import android.Manifest.permission;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.media.AudioAttributes;
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;
@@ -33,15 +41,18 @@ import android.media.MediaSession2.CommandGroup;
import android.media.MediaSession2.ControllerInfo;
import android.media.MediaSession2.PlaylistParams;
import android.media.MediaSession2.SessionCallback;
import android.media.MediaSessionService2;
import android.media.PlaybackState2;
import android.media.SessionToken2;
import android.media.VolumeProvider;
import android.media.session.MediaSessionManager;
import android.media.update.MediaSession2Provider;
import android.os.Bundle;
import android.os.Process;
import android.os.IBinder;
import android.os.ResultReceiver;
import android.support.annotation.GuardedBy;
import android.text.TextUtils;
import android.util.Log;

import java.lang.ref.WeakReference;
@@ -95,25 +106,69 @@ public class MediaSession2Impl implements MediaSession2Provider {
        mCallback = callback;
        mCallbackExecutor = callbackExecutor;
        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
        //      with the same id. Let server check this.
        //      Note that 'ID is unique per package' is important for controller to distinguish

        // Infer type from the id and package name.
        String sessionService = getServiceName(context, MediaSessionService2.SERVICE_INTERFACE, id);
        String libraryService = getServiceName(context, MediaLibraryService2.SERVICE_INTERFACE, id);
        if (sessionService != null && libraryService != null) {
            throw new IllegalArgumentException("Ambiguous session type. Multiple"
                    + " session services define the same id=" + id);
        } else if (sessionService != null) {
            mSessionToken = new SessionToken2(context, Process.myUid(), TYPE_SESSION_SERVICE,
                    mContext.getPackageName(), sessionService, id, mSessionStub);
        } else if (libraryService != null) {
            mSessionToken = new SessionToken2(context, Process.myUid(), TYPE_LIBRARY_SERVICE,
                    mContext.getPackageName(), libraryService, id, mSessionStub);
        } else {
            mSessionToken = new SessionToken2(context, Process.myUid(), TYPE_SESSION,
                    mContext.getPackageName(), null, id, mSessionStub);
        }

        // Only remember player. Actual settings will be done in the initialize().
        mPlayer = player;
    }

    private static String getServiceName(Context context, String serviceAction, String id) {
        PackageManager manager = context.getPackageManager();
        Intent serviceIntent = new Intent(serviceAction);
        serviceIntent.setPackage(context.getPackageName());
        List<ResolveInfo> services = manager.queryIntentServices(serviceIntent,
                PackageManager.GET_META_DATA);
        String serviceName = null;
        if (services != null) {
            for (int i = 0; i < services.size(); i++) {
                String serviceId = SessionToken2Impl.getSessionId(services.get(i));
                if (serviceId != null && TextUtils.equals(id, serviceId)) {
                    if (services.get(i).serviceInfo == null) {
                        continue;
                    }
                    if (serviceName != null) {
                        throw new IllegalArgumentException("Ambiguous session type. Multiple"
                                + " session services define the same id=" + id);
                    }
                    serviceName = services.get(i).serviceInfo.name;
                }
            }
        }
        return serviceName;
    }

    @Override
    public void initialize() {
        synchronized (mLock) {
            setPlayerLocked(mPlayer);
        }
        // Ask server for the sanity check, and starts
        // Sanity check for making session ID unique 'per package' cannot be done in here.
        // Server can only know if the package has another process and has another session with the
        // same id. Note that 'ID is unique per package' is important for controller to distinguish
        // a session in another package.
        //   2. Easier to know the type of session.
        //      Session created here can be the session service token. In order distinguish,
        //      we need to iterate AndroidManifest.xml but it's already done by the server.
        //      Let server to create token with the type.
        MediaSessionManager manager =
                (MediaSessionManager) mContext.getSystemService(Context.MEDIA_SESSION_SERVICE);
        mSessionToken = manager.createSessionToken(mContext.getPackageName(), mId, mSessionStub);
        if (mSessionToken == null) {
        if (!manager.onSessionCreated(mSessionToken)) {
            throw new IllegalStateException("Session with the same id is already used by"
                    + " another process. Use MediaController2 instead.");
        }

        setPlayerInternal(player);
    }

    // TODO(jaewan): Add explicit release() and do not remove session object with the
@@ -126,28 +181,31 @@ public class MediaSession2Impl implements MediaSession2Provider {
        if (player == null) {
            throw new IllegalArgumentException("player shouldn't be null");
        }
        setPlayerInternal(player);
        if (player == mPlayer) {
            return;
        }

    private void setPlayerInternal(MediaPlayerInterface player) {
        synchronized (mLock) {
            if (mPlayer == player) {
                // Player didn't changed. No-op.
                return;
            setPlayerLocked(player);
        }
    }

    private void setPlayerLocked(MediaPlayerInterface player) {
        if (mPlayer != null && mListener != null) {
            // This might not work for a poorly implemented player.
            mPlayer.removePlaybackListener(mListener);
        }
        mPlayer = player;
        mListener = new MyPlaybackListener(this, player);
        player.addPlaybackListener(mCallbackExecutor, mListener);
            mPlayer = player;
        }
        notifyPlaybackStateChangedNotLocked(player.getPlaybackState());
    }

    @Override
    public void close_impl() {
        // Stop system service from listening this session first.
        MediaSessionManager manager =
                (MediaSessionManager) mContext.getSystemService(Context.MEDIA_SESSION_SERVICE);
        manager.onSessionDestroyed(mSessionToken);

        if (mSessionStub != null) {
            if (DEBUG) {
                Log.d(TAG, "session is now unavailable, id=" + mId);
@@ -160,7 +218,6 @@ public class MediaSession2Impl implements MediaSession2Provider {
                // close can be called multiple times
                mPlayer.removePlaybackListener(mListener);
                mPlayer = null;
                return;
            }
        }
    }
@@ -181,11 +238,6 @@ public class MediaSession2Impl implements MediaSession2Provider {
        return mSessionStub.getControllers();
    }

    @Override
    public void setAudioAttributes_impl(AudioAttributes attributes) {
        // implement
    }

    @Override
    public void setAudioFocusRequest_impl(int focusGain) {
        // implement
@@ -288,6 +340,12 @@ public class MediaSession2Impl implements MediaSession2Provider {
        // TODO(jaewan): Implement
    }

    @Override
    public List<MediaItem2> getPlaylist_impl() {
        // TODO(jaewan): Implement this
        return null;
    }

    @Override
    public void prepare_impl() {
        ensureCallingThread();
@@ -323,6 +381,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
    ///////////////////////////////////////////////////
@@ -347,7 +443,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).
@@ -417,11 +512,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;
@@ -505,18 +595,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 −61
Original line number Diff line number Diff line
@@ -16,9 +16,6 @@

package com.android.media;

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

import android.content.Context;
import android.media.IMediaSession2;
import android.media.IMediaSession2Callback;
import android.media.MediaLibraryService2.BrowserRoot;
@@ -28,15 +25,10 @@ import android.media.MediaSession2.Command;
import android.media.MediaSession2.CommandButton;
import android.media.MediaSession2.CommandGroup;
import android.media.MediaSession2.ControllerInfo;
import android.media.MediaSession2.SessionCallback;
import android.media.PlaybackState2;
import android.media.session.PlaybackState;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.support.annotation.GuardedBy;
import android.util.ArrayMap;
@@ -88,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(),
@@ -118,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.
                }
            }
        });
    }

@@ -245,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) {
@@ -297,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?
+11 −24

File changed.

Preview size limit exceeded, changes collapsed.

Loading