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

Commit 79a8e531 authored by Ajay Panicker's avatar Ajay Panicker
Browse files

Remove browsing requirement from MediaPlayerWrapper for queue support

An incorrect asumption was made that browsing was required in order to
have a now playing list. For AVRCP this is true, but in the Media
Framework a player can support browsing but have no queue and vice
versa.

Bug: 68854188
Test: runtest bluetooth -c com.android.bluetooth.avrcp.MediaPlayerWrapperTest
Change-Id: Ic4fa856e2576712f196316efe1b66f85fa1570bf
(cherry picked from commit 567a31d99c8da7bfcbf2eaad3d4f3276064932f4)
Merged-In: I11b1f4c2629476308939cdc424904b2aa2814b37
parent b2f24d86
Loading
Loading
Loading
Loading
+0 −6
Original line number Diff line number Diff line
@@ -31,12 +31,6 @@ class GPMWrapper extends MediaPlayerWrapper {

    private static final String GPM_KEY = "com.google.android.music.mediasession.music_metadata";

    // Google Play Music should always be browsable.
    @Override
    boolean isBrowsable() {
        return true;
    }

    @Override
    boolean isMetadataSynced() {
        // Check if currentPlayingQueueId is in the queue
+78 −55
Original line number Diff line number Diff line
/*
 * Copyright 2017 The Android Open Source Project
 * 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.
@@ -29,11 +29,13 @@ import android.support.annotation.VisibleForTesting;
import android.util.Log;

import java.util.List;
import java.util.Objects;

/*
 * A class to synchronize Media Controller Callbacks and only pass through
 * an update once all the relevant information is current.
 *
 * TODO (apanicke): Once MediaPlayer2 is supported better, replace this class
 * with that.
 */
class MediaPlayerWrapper {
    private static final String TAG = "NewAvrcpMediaPlayerWrapper";
@@ -44,7 +46,6 @@ class MediaPlayerWrapper {
    private String mPackageName;
    private Looper mLooper;

    private boolean mIsBrowsable = false;
    private MediaData mCurrentData;

    @GuardedBy("mCallbackLock")
@@ -57,45 +58,26 @@ class MediaPlayerWrapper {
        mCurrentData = new MediaData(null, null, null);
    }

    interface Callback {
    public interface Callback {
        void mediaUpdatedCallback(MediaData data);
    }

    class MediaData {
        public List<MediaSession.QueueItem> queue;
        public PlaybackState state;
        public MediaMetadata metadata;

        MediaData(MediaMetadata m, PlaybackState s, List<MediaSession.QueueItem> q) {
            metadata = m;
            state = s;
            queue = q;
        }

        @Override
        public boolean equals(Object o) {
            if (o == null) return false;
            if (!(o instanceof MediaData)) return false;

            final MediaData u = (MediaData) o;

            if (!Objects.equals(metadata, u.metadata)) {
                return false;
            }

            if (!Objects.equals(queue, u.queue)) {
    boolean isReady() {
        if (getPlaybackState() == null) {
            d("isReady(): PlaybackState is null");
            return false;
        }

            if (!playstateEquals(state, u.state)) {
        if (getMetadata() == null) {
            d("isReady(): Metadata is null");
            return false;
        }

        return true;
    }
    }

    static MediaPlayerWrapper wrap(MediaController controller, Looper looper, boolean browsable) {
    // TODO (apanicke): Implement a factory to make testing and creating interop wrappers easier
    static MediaPlayerWrapper wrap(MediaController controller, Looper looper) {
        if (controller == null || looper == null) {
            e("MediaPlayerWrapper.wrap(): Null parameter - Controller: " + controller
                    + " | Looper: " + looper);
@@ -113,12 +95,10 @@ class MediaPlayerWrapper {
        newWrapper.mMediaController = controller;
        newWrapper.mPackageName = controller.getPackageName();
        newWrapper.mLooper = looper;
        newWrapper.mIsBrowsable = browsable;

        newWrapper.mCurrentData.queue = newWrapper.getQueue();
        newWrapper.mCurrentData.metadata = newWrapper.getMetadata();
        newWrapper.mCurrentData.state = newWrapper.getPlaybackState();

        return newWrapper;
    }

@@ -129,17 +109,11 @@ class MediaPlayerWrapper {
        mLooper = null;
    }

    boolean isBrowsable() {
        return mIsBrowsable;
    }

    String getPackageName() {
        return mPackageName;
    }

    List<MediaSession.QueueItem> getQueue() {
        if (!isBrowsable()) return null;

        return mMediaController.getQueue();
    }

@@ -156,7 +130,23 @@ class MediaPlayerWrapper {
        return mMediaController.getPlaybackState();
    }

    // TODO: Implement shuffle and repeat support. Right now these use custom actions
    MediaData getCurrentMediaData() {
        return mCurrentData;
    }

    void playItemFromQueue(long qid) {
        // Return immediately if no queue exists.
        if (getQueue() == null) {
            Log.w(TAG, "playItemFromQueue: Trying to play item for player that has no queue: "
                    + mPackageName);
            return;
        }

        MediaController.TransportControls controller = mMediaController.getTransportControls();
        controller.skipToQueueItem(qid);
    }

    // TODO (apanicke): Implement shuffle and repeat support. Right now these use custom actions
    // and it may only be possible to do this with Google Play Music
    boolean isShuffleSupported() {
        return false;
@@ -175,12 +165,10 @@ class MediaPlayerWrapper {
    }

    /**
     * Return whether the queue, metadata, and queueID are all in sync. If
     * browsing isn't supported we don't have to worry about the queue as
     * the queue doesn't exist
     * Return whether the queue, metadata, and queueID are all in sync.
     */
    boolean isMetadataSynced() {
        if (isBrowsable()) {
        if (getQueue() != null) {
            // Check if currentPlayingQueueId is in the current Queue
            MediaSession.QueueItem currItem = null;

@@ -237,6 +225,21 @@ class MediaPlayerWrapper {
        mControllerCallbacks = null;
    }

    void updateMediaController(MediaController newController) {
        if (newController == mMediaController) return;

        synchronized (mCallbackLock) {
            if (mRegisteredCallback == null || mControllerCallbacks == null) {
                return;
            }
        }

        mControllerCallbacks.cleanup();
        mMediaController = newController;
        mControllerCallbacks = new MediaControllerListener(mLooper);
        d("Controller for " + mPackageName + " was updated.");
    }

    class TimeoutHandler extends Handler {
        private static final int MSG_TIMEOUT = 0;
        private static final long CALLBACK_TIMEOUT_MS = 1000;
@@ -255,7 +258,7 @@ class MediaPlayerWrapper {
            Log.e(TAG, "Timeout while waiting for metadata to sync for " + mPackageName);
            Log.e(TAG, "  └ Current Metadata: " + getMetadata().getDescription());
            Log.e(TAG, "  └ Current Playstate: " + getPlaybackState());
            for (int i = 0; i < getQueue().size(); i++) {
            for (int i = 0; getQueue() != null && i < getQueue().size(); i++) {
                Log.e(TAG, "  └ QueueItem(" + i + "): " + getQueue().get(i));
            }

@@ -293,10 +296,7 @@ class MediaPlayerWrapper {
                mTimeoutHandler.removeMessages(TimeoutHandler.MSG_TIMEOUT);

                if (!isMetadataSynced()) {
                    if (DEBUG) {
                        Log.d(TAG, "trySendMediaUpdate(): " + mPackageName
                                + ": Starting media update timeout");
                    }
                    d("trySendMediaUpdate(): Starting media update timeout");
                    mTimeoutHandler.sendEmptyMessageDelayed(TimeoutHandler.MSG_TIMEOUT,
                            TimeoutHandler.CALLBACK_TIMEOUT_MS);
                    return;
@@ -328,6 +328,11 @@ class MediaPlayerWrapper {

        @Override
        public void onMetadataChanged(MediaMetadata metadata) {
            if (!isReady()) {
                Log.v(TAG, mPackageName + " tried to update with incomplete metadata");
                return;
            }

            Log.v(TAG, "onMetadataChanged(): " + mPackageName + " : " + metadata.getDescription());

            if (!metadata.equals(getMetadata())) {
@@ -353,6 +358,11 @@ class MediaPlayerWrapper {

        @Override
        public void onPlaybackStateChanged(PlaybackState state) {
            if (!isReady()) {
                Log.v(TAG, mPackageName + " tried to update with no state");
                return;
            }

            Log.v(TAG, "onPlaybackStateChanged(): " + mPackageName + " : " + state.toString());

            if (!playstateEquals(state, getPlaybackState())) {
@@ -377,10 +387,12 @@ class MediaPlayerWrapper {
        @Override
        public void onQueueChanged(List<MediaSession.QueueItem> queue) {
            Log.v(TAG, "onQueueChanged(): " + mPackageName);
            if (!isBrowsable()) {
                e("Queue changed for non-browsable player " + mPackageName);

            if (!isReady()) {
                Log.v(TAG, mPackageName + " tried to updated with no queue");
                return;
            }

            if (!queue.equals(getQueue())) {
                e("The callback queue isn't the current queue");
            }
@@ -400,7 +412,9 @@ class MediaPlayerWrapper {
        }

        @Override
        public void onSessionDestroyed() {}
        public void onSessionDestroyed() {
            Log.w(TAG, "The session was destroyed " + mPackageName);
        }

        @VisibleForTesting
        Handler getTimeoutHandler() {
@@ -430,6 +444,7 @@ class MediaPlayerWrapper {
        return false;
    }

    // TODO: Use this function when returning the now playing list
    /**
     * Extracts different pieces of metadata from a MediaSession.QueueItem
     * and builds a MediaMetadata Object out of it.
@@ -475,9 +490,17 @@ class MediaPlayerWrapper {
        }
    }

    private void d(String message) {
        if (DEBUG) Log.d(TAG, mPackageName + ": " + message);
    }

    @VisibleForTesting
    Handler getTimeoutHandler() {
        if (mControllerCallbacks == null) return null;
        return mControllerCallbacks.getTimeoutHandler();
    }

    public void dump(StringBuilder sb) {
        sb.append(mMediaController.toString() + "\n");
    }
}
+61 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 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.bluetooth.avrcp;

import android.media.MediaMetadata;
import android.media.session.MediaSession;
import android.media.session.PlaybackState;

import java.util.List;
import java.util.Objects;

/*
 * Helper class to transport metadata around AVRCP
 */
class MediaData {
    public List<MediaSession.QueueItem> queue;
    public PlaybackState state;
    public MediaMetadata metadata;

    MediaData(MediaMetadata m, PlaybackState s, List<MediaSession.QueueItem> q) {
        metadata = m;
        state = s;
        queue = q;
    }

    @Override
    public boolean equals(Object o) {
        if (o == null) return false;
        if (!(o instanceof MediaData)) return false;

        final MediaData u = (MediaData) o;

        if (!MediaPlayerWrapper.playstateEquals(state, u.state)) {
            return false;
        }

        if (!Objects.equals(metadata, u.metadata)) {
            return false;
        }

        if (!Objects.equals(queue, u.queue)) {
            return false;
        }

        return true;
    }
}
+58 −61
Original line number Diff line number Diff line
@@ -54,7 +54,7 @@ public class MediaPlayerWrapperTest {
    private PlaybackState.Builder mTestState;

    @Captor ArgumentCaptor<MediaController.Callback> mControllerCbs;
    @Captor ArgumentCaptor<MediaPlayerWrapper.MediaData> mMediaUpdateData;
    @Captor ArgumentCaptor<MediaData> mMediaUpdateData;
    @Mock Log.TerribleFailureHandler mFailHandler;
    @Mock MediaController mMockController;
    @Mock MediaPlayerWrapper.Callback mTestCbs;
@@ -76,7 +76,7 @@ public class MediaPlayerWrapperTest {
    public void setUp() {
        MockitoAnnotations.initMocks(this);

        // Set failure handler to caputer Log.wtf messages
        // Set failure handler to capture Log.wtf messages
        Log.setWtfHandler(mFailHandler);

        // Set up Looper thread for the timeout handler
@@ -127,7 +127,7 @@ public class MediaPlayerWrapperTest {
        doReturn(getQueueFromDescriptions(mTestQueue)).when(mMockController).getQueue();

        // Enable testing flag which enables Log.wtf statements. Some tests test against improper
        // behaviour and the TerribleFailureListener is a good way to ensure that the error occured
        // behaviour and the TerribleFailureListener is a good way to ensure that the error occurred
        MediaPlayerWrapper.sTesting = true;
    }

@@ -137,26 +137,55 @@ public class MediaPlayerWrapperTest {
     */
    @Test
    public void testNullControllerLooper() {
        MediaPlayerWrapper wrapper = MediaPlayerWrapper.wrap(null, mThread.getLooper(), false);
        MediaPlayerWrapper wrapper = MediaPlayerWrapper.wrap(null, mThread.getLooper());
        Assert.assertNull(wrapper);

        wrapper = MediaPlayerWrapper.wrap(mMockController, null, false);
        wrapper = MediaPlayerWrapper.wrap(mMockController, null);
        Assert.assertNull(wrapper);
    }

    /*
     * Test to make sure that isReady() returns false if there is no playback state,
     * there is no metadata, or if the metadata has no title.
     */
    @Test
    public void testIsReady() {
        MediaPlayerWrapper wrapper = MediaPlayerWrapper.wrap(mMockController, mThread.getLooper());
        Assert.assertTrue(wrapper.isReady());

        // Test isReady() is false when the playback state is null
        doReturn(null).when(mMockController).getPlaybackState();
        Assert.assertFalse(wrapper.isReady());

        // Restore the old playback state
        doReturn(mTestState.build()).when(mMockController).getPlaybackState();
        Assert.assertTrue(wrapper.isReady());

        // Test isReady() is false when the metadata is null
        doReturn(null).when(mMockController).getMetadata();
        Assert.assertFalse(wrapper.isReady());

        // Restore the old metadata
        doReturn(mTestMetadata.build()).when(mMockController).getMetadata();
        Assert.assertTrue(wrapper.isReady());
    }

    /*
     * Test to make sure that a media player update gets sent whenever a Media metadata or playback
     * state change occurs instead of waiting for all data to be synced if the player doesn't
     * support browsing and queues.
     * support queues.
     */
    @Test
    public void testNoBrowsingMediaUpdates() {
    public void testNoQueueMediaUpdates() {
        // Create the wrapper object and register the looper with the timeout handler
        TestLooperManager looperManager = new TestLooperManager(mThread.getLooper());
        MediaPlayerWrapper wrapper =
                MediaPlayerWrapper.wrap(mMockController, mThread.getLooper(), false);
                MediaPlayerWrapper.wrap(mMockController, mThread.getLooper());
        wrapper.registerCallback(mTestCbs);

        // Return null when getting the queue
        doReturn(null).when(mMockController).getQueue();

        // Grab the callbacks the wrapper registered with the controller
        verify(mMockController).registerCallback(mControllerCbs.capture(), any());
        MediaController.Callback controllerCallbacks = mControllerCbs.getValue();
@@ -168,7 +197,7 @@ public class MediaPlayerWrapperTest {

        // Assert that the metadata was updated and playback state wasn't
        verify(mTestCbs, times(1)).mediaUpdatedCallback(mMediaUpdateData.capture());
        MediaPlayerWrapper.MediaData data = mMediaUpdateData.getValue();
        MediaData data = mMediaUpdateData.getValue();
        Assert.assertEquals(
                "Returned Metadata isn't equal to given Metadata",
                data.metadata.getDescription(),
@@ -202,58 +231,23 @@ public class MediaPlayerWrapperTest {
        verify(mFailHandler, never()).onTerribleFailure(any(), any(), anyBoolean());
    }

    /*
     * Test that trying to get the queue on a player that doesn't support
     * browsing returns false.
     */
    @Test
    public void testNoBrowsingNullQueue() {
        // Create the wrapper object and register the looper with the timeout handler
        TestLooperManager looperManager = new TestLooperManager(mThread.getLooper());
        MediaPlayerWrapper wrapper =
                MediaPlayerWrapper.wrap(mMockController, mThread.getLooper(), false);
        wrapper.registerCallback(mTestCbs);

        // Grab the callbacks the wrapper registered with the controller
        verify(mMockController).registerCallback(mControllerCbs.capture(), any());
        MediaController.Callback controllerCallbacks = mControllerCbs.getValue();

        // Update Queue returned by controller
        mTestQueue.add(
                new MediaDescription.Builder()
                        .setTitle("New Title")
                        .setSubtitle("BT Test Artist")
                        .setDescription("BT Test Album")
                        .setMediaId("103"));
        doReturn(getQueueFromDescriptions(mTestQueue)).when(mMockController).getQueue();
        controllerCallbacks.onQueueChanged(getQueueFromDescriptions(mTestQueue));

        // Verify no updates happened
        verify(mTestCbs, never()).mediaUpdatedCallback(any());

        // Verify that getQueue() returns null
        Assert.assertNull(wrapper.getQueue());

        // Verify that there was an error message pending and there were no timeouts
        Assert.assertFalse(wrapper.getTimeoutHandler().hasMessages(MSG_TIMEOUT));
        verify(mFailHandler, times(1)).onTerribleFailure(any(), any(), anyBoolean());
    }

    /*
     * This test updates the metadata and playback state returned by the
     * controller then sends an update. This is to make sure that all relevant
     * information is sent with every update. In the case without browsing,
     * information is sent with every update. In the case without a queue,
     * metadata and playback state are updated.
     */

    @Test
    public void testAllDataOnUpdate() {
    public void testDataOnUpdateNoQueue() {
        // Create the wrapper object and register the looper with the timeout handler
        TestLooperManager looperManager = new TestLooperManager(mThread.getLooper());
        MediaPlayerWrapper wrapper =
                MediaPlayerWrapper.wrap(mMockController, mThread.getLooper(), false);
                MediaPlayerWrapper.wrap(mMockController, mThread.getLooper());
        wrapper.registerCallback(mTestCbs);

        // Return null when getting the queue
        doReturn(null).when(mMockController).getQueue();

        // Grab the callbacks the wrapper registered with the controller
        verify(mMockController).registerCallback(mControllerCbs.capture(), any());
        MediaController.Callback controllerCallbacks = mControllerCbs.getValue();
@@ -271,7 +265,7 @@ public class MediaPlayerWrapperTest {

        // Assert that both metadata and playback state are there.
        verify(mTestCbs, times(1)).mediaUpdatedCallback(mMediaUpdateData.capture());
        MediaPlayerWrapper.MediaData data = mMediaUpdateData.getValue();
        MediaData data = mMediaUpdateData.getValue();
        Assert.assertEquals(
                "Returned PlaybackState isn't equal to given PlaybackState",
                data.state.toString(),
@@ -288,7 +282,7 @@ public class MediaPlayerWrapperTest {
    }

    /*
     * This test sends repeted Playback State updates that only have a short
     * This test sends repeated Playback State updates that only have a short
     * position update change to see if they get debounced.
     */
    @Test
@@ -296,9 +290,12 @@ public class MediaPlayerWrapperTest {
        // Create the wrapper object and register the looper with the timeout handler
        TestLooperManager looperManager = new TestLooperManager(mThread.getLooper());
        MediaPlayerWrapper wrapper =
                MediaPlayerWrapper.wrap(mMockController, mThread.getLooper(), false);
                MediaPlayerWrapper.wrap(mMockController, mThread.getLooper());
        wrapper.registerCallback(mTestCbs);

        // Return null when getting the queue
        doReturn(null).when(mMockController).getQueue();

        // Grab the callbacks the wrapper registered with the controller
        verify(mMockController).registerCallback(mControllerCbs.capture(), any());
        MediaController.Callback controllerCallbacks = mControllerCbs.getValue();
@@ -310,7 +307,7 @@ public class MediaPlayerWrapperTest {

        // Assert that both metadata and only the first playback state is there.
        verify(mTestCbs, times(1)).mediaUpdatedCallback(mMediaUpdateData.capture());
        MediaPlayerWrapper.MediaData data = mMediaUpdateData.getValue();
        MediaData data = mMediaUpdateData.getValue();
        Assert.assertEquals(
                "Returned PlaybackState isn't equal to given PlaybackState",
                data.state.toString(),
@@ -350,7 +347,7 @@ public class MediaPlayerWrapperTest {
        // Create the wrapper object and register the looper with the timeout handler
        TestLooperManager looperManager = new TestLooperManager(mThread.getLooper());
        MediaPlayerWrapper wrapper =
                MediaPlayerWrapper.wrap(mMockController, mThread.getLooper(), false);
                MediaPlayerWrapper.wrap(mMockController, mThread.getLooper());
        wrapper.registerCallback(mTestCbs);

        // Cleanup the wrapper
@@ -370,7 +367,7 @@ public class MediaPlayerWrapperTest {
        // Create the wrapper object and register the looper with the timeout handler
        TestLooperManager looperManager = new TestLooperManager(mThread.getLooper());
        MediaPlayerWrapper wrapper =
                MediaPlayerWrapper.wrap(mMockController, mThread.getLooper(), false);
                MediaPlayerWrapper.wrap(mMockController, mThread.getLooper());
        wrapper.registerCallback(mTestCbs);

        // Grab the callbacks the wrapper registered with the controller
@@ -400,7 +397,7 @@ public class MediaPlayerWrapperTest {
        // Create the wrapper object and register the looper with the timeout handler
        TestLooperManager looperManager = new TestLooperManager(mThread.getLooper());
        MediaPlayerWrapper wrapper =
                MediaPlayerWrapper.wrap(mMockController, mThread.getLooper(), true);
                MediaPlayerWrapper.wrap(mMockController, mThread.getLooper());
        wrapper.registerCallback(mTestCbs);

        // Grab the callbacks the wrapper registered with the controller
@@ -430,7 +427,7 @@ public class MediaPlayerWrapperTest {
        // Assert that the callback was called with the updated data
        verify(mTestCbs, times(1)).mediaUpdatedCallback(mMediaUpdateData.capture());
        verify(mFailHandler, never()).onTerribleFailure(any(), any(), anyBoolean());
        MediaPlayerWrapper.MediaData data = mMediaUpdateData.getValue();
        MediaData data = mMediaUpdateData.getValue();
        Assert.assertEquals(
                "Returned Metadata isn't equal to given Metadata",
                data.metadata.getDescription(),
@@ -460,7 +457,7 @@ public class MediaPlayerWrapperTest {
                InstrumentationRegistry.getInstrumentation()
                        .acquireLooperManager(mThread.getLooper());
        MediaPlayerWrapper wrapper =
                MediaPlayerWrapper.wrap(mMockController, mThread.getLooper(), true);
                MediaPlayerWrapper.wrap(mMockController, mThread.getLooper());
        wrapper.registerCallback(mTestCbs);

        // Grab the callbacks the wrapper registered with the controller
@@ -494,7 +491,7 @@ public class MediaPlayerWrapperTest {
                InstrumentationRegistry.getInstrumentation()
                        .acquireLooperManager(mThread.getLooper());
        MediaPlayerWrapper wrapper =
                MediaPlayerWrapper.wrap(mMockController, mThread.getLooper(), true);
                MediaPlayerWrapper.wrap(mMockController, mThread.getLooper());
        wrapper.registerCallback(mTestCbs);

        // Grab the callbacks the wrapper registered with the controller
@@ -545,7 +542,7 @@ public class MediaPlayerWrapperTest {
            // Check that the callback was called a certain number of times and
            // that all the Media info matches what was given
            verify(mTestCbs, times(i)).mediaUpdatedCallback(mMediaUpdateData.capture());
            MediaPlayerWrapper.MediaData data = mMediaUpdateData.getValue();
            MediaData data = mMediaUpdateData.getValue();
            Assert.assertEquals(
                    "Returned Metadata isn't equal to given Metadata",
                    data.metadata.getDescription(),