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

Commit 1cba11c7 authored by Ajay Panicker's avatar Ajay Panicker Committed by android-build-merger
Browse files

Merge "Remove browsing requirement from MediaPlayerWrapper for queue support"

am: 245b5984

Change-Id: I65a8265eda47a0b2655034c339565255b5b317f5
parents 06f4fbda 245b5984
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(),