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

Commit 38ff0001 authored by Jaewan Kim's avatar Jaewan Kim
Browse files

MediaSession2: Add a way to notify errors between session and player

This is proposed during the offline meeting

Test: Run all MediaComponents tests once
Change-Id: I2cbd980275bf88af840eb9f1933363c3ad8ff2e3
parent 9f303ea2
Loading
Loading
Loading
Loading
+67 −32
Original line number Diff line number Diff line
@@ -38,7 +38,7 @@ import android.media.MediaItem2;
import android.media.MediaLibraryService2;
import android.media.MediaMetadata2;
import android.media.MediaPlayerInterface;
import android.media.MediaPlayerInterface.PlaybackListener;
import android.media.MediaPlayerInterface.EventCallback;
import android.media.MediaSession2;
import android.media.MediaSession2.Builder;
import android.media.MediaSession2.Command;
@@ -62,6 +62,7 @@ import android.os.Process;
import android.os.ResultReceiver;
import android.support.annotation.GuardedBy;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;

@@ -84,7 +85,7 @@ public class MediaSession2Impl implements MediaSession2Provider {
    private final MediaSession2Stub mSessionStub;
    private final SessionToken2 mSessionToken;
    private final AudioManager mAudioManager;
    private final List<PlaybackListenerHolder> mListeners = new ArrayList<>();
    private final ArrayMap<EventCallback, Executor> mCallbacks = new ArrayMap<>();
    private final PendingIntent mSessionActivity;

    // mPlayer is set to null when the session is closed, and we shouldn't throw an exception
@@ -110,7 +111,7 @@ public class MediaSession2Impl implements MediaSession2Provider {
    @GuardedBy("mLock")
    private PlaybackInfo mPlaybackInfo;
    @GuardedBy("mLock")
    private MyPlaybackListener mListener;
    private MyEventCallback mEventCallback;

    /**
     * Can be only called by the {@link Builder#build()}.
@@ -223,19 +224,20 @@ public class MediaSession2Impl implements MediaSession2Provider {
    }

    private void setPlayer(MediaPlayerInterface player, VolumeProvider2 volumeProvider) {
        PlaybackInfo info = createPlaybackInfo(volumeProvider, player.getAudioAttributes());
        final PlaybackInfo info = createPlaybackInfo(volumeProvider, player.getAudioAttributes());
        synchronized (mLock) {
            if (mPlayer != null && mListener != null) {
            if (mPlayer != null && mEventCallback != null) {
                // This might not work for a poorly implemented player.
                mPlayer.removePlaybackListener(mListener);
                mPlayer.unregisterEventCallback(mEventCallback);
            }
            mPlayer = player;
            mListener = new MyPlaybackListener(this, player);
            player.addPlaybackListener(mCallbackExecutor, mListener);
            mEventCallback = new MyEventCallback(this, player);
            player.registerEventCallback(mCallbackExecutor, mEventCallback);
            mVolumeProvider = volumeProvider;
            mPlaybackInfo = info;
        }
        mSessionStub.notifyPlaybackInfoChanged(info);
        notifyPlaybackStateChangedNotLocked(mInstance.getPlaybackState());
    }

    private PlaybackInfo createPlaybackInfo(VolumeProvider2 volumeProvider, AudioAttributes attrs) {
@@ -291,7 +293,7 @@ public class MediaSession2Impl implements MediaSession2Provider {
        synchronized (mLock) {
            if (mPlayer != null) {
                // close can be called multiple times
                mPlayer.removePlaybackListener(mListener);
                mPlayer.unregisterEventCallback(mEventCallback);
                mPlayer = null;
            }
        }
@@ -520,32 +522,31 @@ public class MediaSession2Impl implements MediaSession2Provider {
    }

    @Override
    public void addPlaybackListener_impl(Executor executor, PlaybackListener listener) {
    public void registerPlayerEventCallback_impl(Executor executor, EventCallback callback) {
        if (executor == null) {
            throw new IllegalArgumentException("executor shouldn't be null");
        }
        if (listener == null) {
            throw new IllegalArgumentException("listener shouldn't be null");
        if (callback == null) {
            throw new IllegalArgumentException("callback shouldn't be null");
        }
        ensureCallingThread();
        if (PlaybackListenerHolder.contains(mListeners, listener)) {
            Log.w(TAG, "listener is already added. Ignoring.");
        if (mCallbacks.get(callback) != null) {
            Log.w(TAG, "callback is already added. Ignoring.");
            return;
        }
        mListeners.add(new PlaybackListenerHolder(executor, listener));
        executor.execute(() -> listener.onPlaybackChanged(getInstance().getPlaybackState()));
        mCallbacks.put(callback, executor);
        // TODO(jaewan): Double check if we need this.
        final PlaybackState2 state = getInstance().getPlaybackState();
        executor.execute(() -> callback.onPlaybackStateChanged(state));
    }

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

    @Override
@@ -586,19 +587,35 @@ public class MediaSession2Impl implements MediaSession2Provider {
        }*/
    }

    private void notifyPlaybackStateChangedNotLocked(PlaybackState2 state) {
        List<PlaybackListenerHolder> listeners = new ArrayList<>();
    private void notifyPlaybackStateChangedNotLocked(final PlaybackState2 state) {
        ArrayMap<EventCallback, Executor> callbacks = new ArrayMap<>();
        synchronized (mLock) {
            listeners.addAll(mListeners);
            callbacks.putAll(mCallbacks);
        }
        // Notify to listeners added directly to this session
        for (int i = 0; i < listeners.size(); i++) {
            listeners.get(i).postPlaybackChange(state);
        // Notify to callbacks added directly to this session
        for (int i = 0; i < callbacks.size(); i++) {
            final EventCallback callback = callbacks.keyAt(i);
            final Executor executor = callbacks.valueAt(i);
            executor.execute(() -> callback.onPlaybackStateChanged(state));
        }
        // Notify to controllers as well.
        mSessionStub.notifyPlaybackStateChangedNotLocked(state);
    }

    private void notifyErrorNotLocked(String mediaId, int what, int extra) {
        ArrayMap<EventCallback, Executor> callbacks = new ArrayMap<>();
        synchronized (mLock) {
            callbacks.putAll(mCallbacks);
        }
        // Notify to callbacks added directly to this session
        for (int i = 0; i < callbacks.size(); i++) {
            final EventCallback callback = callbacks.keyAt(i);
            final Executor executor = callbacks.valueAt(i);
            executor.execute(() -> callback.onError(mediaId, what, extra));
        }
        // TODO(jaewan): Notify to controllers as well.
    }

    Context getContext() {
        return mContext;
    }
@@ -637,25 +654,43 @@ public class MediaSession2Impl implements MediaSession2Provider {
        return mSessionActivity;
    }

    private static class MyPlaybackListener implements MediaPlayerInterface.PlaybackListener {
    private static class MyEventCallback implements EventCallback {
        private final WeakReference<MediaSession2Impl> mSession;
        private final MediaPlayerInterface mPlayer;

        private MyPlaybackListener(MediaSession2Impl session, MediaPlayerInterface player) {
        private MyEventCallback(MediaSession2Impl session, MediaPlayerInterface player) {
            mSession = new WeakReference<>(session);
            mPlayer = player;
        }

        @Override
        public void onPlaybackChanged(PlaybackState2 state) {
        public void onPlaybackStateChanged(PlaybackState2 state) {
            MediaSession2Impl session = mSession.get();
            if (mPlayer != session.mInstance.getPlayer()) {
                Log.w(TAG, "Unexpected playback state change notifications. Ignoring.",
                        new IllegalStateException());
                return;
            }
            if (DEBUG) {
                Log.d(TAG, "onPlaybackStateChanged from player, state=" + state);
            }
            session.notifyPlaybackStateChangedNotLocked(state);
        }

        @Override
        public void onError(String mediaId, int what, int extra) {
            MediaSession2Impl session = mSession.get();
            if (mPlayer != session.mInstance.getPlayer()) {
                Log.w(TAG, "Unexpected playback state change notifications. Ignoring.",
                        new IllegalStateException());
                return;
            }
            if (DEBUG) {
                Log.d(TAG, "onError from player, mediaId=" + mediaId + ", what=" + what
                        + ", extra=" + extra);
            }
            session.notifyErrorNotLocked(mediaId, what, extra);
        }
    }

    public static final class CommandImpl implements CommandProvider {
+5 −5
Original line number Diff line number Diff line
@@ -22,7 +22,7 @@ import android.app.Notification;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.media.MediaPlayerInterface.PlaybackListener;
import android.media.MediaPlayerInterface.EventCallback;
import android.media.MediaSession2;
import android.media.MediaSessionService2;
import android.media.MediaSessionService2.MediaNotification;
@@ -42,7 +42,7 @@ public class MediaSessionService2Impl implements MediaSessionService2Provider {
    private static final boolean DEBUG = true; // TODO(jaewan): Change this.

    private final MediaSessionService2 mInstance;
    private final PlaybackListener mListener = new SessionServicePlaybackListener();
    private final EventCallback mCallback = new SessionServiceEventCallback();

    private final Object mLock = new Object();
    @GuardedBy("mLock")
@@ -94,7 +94,7 @@ public class MediaSessionService2Impl implements MediaSessionService2Provider {
                    + ", but got " + mSession);
        }
        // TODO(jaewan): Uncomment here.
        // mSession.addPlaybackListener(mListener, mSession.getExecutor());
        // mSession.registerPlayerEventCallback(mCallback, mSession.getExecutor());
    }

    @TokenType int getSessionType() {
@@ -135,9 +135,9 @@ public class MediaSessionService2Impl implements MediaSessionService2Provider {
                mediaNotification.getNotification());
    }

    private class SessionServicePlaybackListener implements PlaybackListener {
    private class SessionServiceEventCallback implements EventCallback {
        @Override
        public void onPlaybackChanged(PlaybackState2 state) {
        public void onPlaybackStateChanged(PlaybackState2 state) {
            if (state == null) {
                Log.w(TAG, "Ignoring null playback state");
                return;
+0 −72
Original line number Diff line number Diff line
/*
 * Copyright 2018 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.media;

import android.media.MediaPlayerInterface.PlaybackListener;
import android.media.PlaybackState2;
import android.os.Handler;
import android.support.annotation.NonNull;

import java.util.List;
import java.util.concurrent.Executor;

/**
 * Holds {@link PlaybackListener} with the {@link Handler}.
 */
public class PlaybackListenerHolder {
    public final Executor executor;
    public final PlaybackListener listener;

    public PlaybackListenerHolder(Executor executor, @NonNull PlaybackListener listener) {
        this.executor = executor;
        this.listener = listener;
    }

    public void postPlaybackChange(final PlaybackState2 state) {
        executor.execute(() -> listener.onPlaybackChanged(state));
    }

    /**
     * Returns {@code true} if the given list contains a {@link PlaybackListenerHolder} that holds
     * the given listener.
     *
     * @param list list to check
     * @param listener listener to check
     * @return {@code true} if the given list contains listener. {@code false} otherwise.
     */
    public static <Holder extends PlaybackListenerHolder> boolean contains(
            @NonNull List<Holder> list, PlaybackListener listener) {
        return indexOf(list, listener) >= 0;
    }

    /**
     * Returns the index of the {@link PlaybackListenerHolder} that contains the given listener.
     *
     * @param list list to check
     * @param listener listener to check
     * @return {@code index} of item if the given list contains listener. {@code -1} otherwise.
     */
    public static <Holder extends PlaybackListenerHolder> int indexOf(
            @NonNull List<Holder> list, PlaybackListener listener) {
        for (int i = 0; i < list.size(); i++) {
            if (list.get(i).listener == listener) {
                return i;
            }
        }
        return -1;
    }
}
+3 −17
Original line number Diff line number Diff line
@@ -30,7 +30,6 @@ public final class PlaybackState2Impl implements PlaybackState2Provider {
    private static final String KEY_BUFFERED_POSITION =
            "android.media.playbackstate2.buffered_position";
    private static final String KEY_SPEED = "android.media.playbackstate2.speed";
    private static final String KEY_ERROR_MESSAGE = "android.media.playbackstate2.error_message";
    private static final String KEY_UPDATE_TIME = "android.media.playbackstate2.update_time";
    private static final String KEY_ACTIVE_ITEM_ID = "android.media.playbackstate2.active_item_id";

@@ -42,11 +41,9 @@ public final class PlaybackState2Impl implements PlaybackState2Provider {
    private final float mSpeed;
    private final long mBufferedPosition;
    private final long mActiveItemId;
    private final CharSequence mErrorMessage;

    public PlaybackState2Impl(Context context, PlaybackState2 instance, int state, long position,
            long updateTime, float speed, long bufferedPosition, long activeItemId,
            CharSequence error) {
            long updateTime, float speed, long bufferedPosition, long activeItemId) {
        mContext = context;
        mInstance = instance;
        mState = state;
@@ -55,7 +52,6 @@ public final class PlaybackState2Impl implements PlaybackState2Provider {
        mUpdateTime = updateTime;
        mBufferedPosition = bufferedPosition;
        mActiveItemId = activeItemId;
        mErrorMessage = error;
    }

    @Override
@@ -67,7 +63,6 @@ public final class PlaybackState2Impl implements PlaybackState2Provider {
        bob.append(", speed=").append(mSpeed);
        bob.append(", updated=").append(mUpdateTime);
        bob.append(", active item id=").append(mActiveItemId);
        bob.append(", error=").append(mErrorMessage);
        bob.append("}");
        return bob.toString();
    }
@@ -92,11 +87,6 @@ public final class PlaybackState2Impl implements PlaybackState2Provider {
        return mSpeed;
    }

    @Override
    public CharSequence getErrorMessage_impl() {
        return mErrorMessage;
    }

    @Override
    public long getLastPositionUpdateTime_impl() {
        return mUpdateTime;
@@ -116,7 +106,6 @@ public final class PlaybackState2Impl implements PlaybackState2Provider {
        bundle.putFloat(KEY_SPEED, mSpeed);
        bundle.putLong(KEY_BUFFERED_POSITION, mBufferedPosition);
        bundle.putLong(KEY_ACTIVE_ITEM_ID, mActiveItemId);
        bundle.putCharSequence(KEY_ERROR_MESSAGE, mErrorMessage);
        return bundle;
    }

@@ -129,18 +118,15 @@ public final class PlaybackState2Impl implements PlaybackState2Provider {
                || !bundle.containsKey(KEY_UPDATE_TIME)
                || !bundle.containsKey(KEY_SPEED)
                || !bundle.containsKey(KEY_BUFFERED_POSITION)
                || !bundle.containsKey(KEY_ACTIVE_ITEM_ID)
                || !bundle.containsKey(KEY_ERROR_MESSAGE)) {
                || !bundle.containsKey(KEY_ACTIVE_ITEM_ID)) {
            return null;
        }

        return new PlaybackState2(context,
                bundle.getInt(KEY_STATE),
                bundle.getLong(KEY_POSITION),
                bundle.getLong(KEY_UPDATE_TIME),
                bundle.getFloat(KEY_SPEED),
                bundle.getLong(KEY_BUFFERED_POSITION),
                bundle.getLong(KEY_ACTIVE_ITEM_ID),
                bundle.getCharSequence(KEY_ERROR_MESSAGE));
                bundle.getLong(KEY_ACTIVE_ITEM_ID));
    }
}
 No newline at end of file
+2 −2
Original line number Diff line number Diff line
@@ -294,9 +294,9 @@ public class ApiFactory implements StaticProvider {
    @Override
    public PlaybackState2Provider createPlaybackState2(Context context, PlaybackState2 instance,
            int state, long position, long updateTime, float speed, long bufferedPosition,
            long activeItemId, CharSequence error) {
            long activeItemId) {
        return new PlaybackState2Impl(context, instance, state, position, updateTime, speed,
                bufferedPosition, activeItemId, error);
                bufferedPosition, activeItemId);
    }

    @Override
Loading