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

Commit c7f1f24f authored by Marie Janssen's avatar Marie Janssen
Browse files

AVRCP: Track players without active media sessions

Players can be selected as addressed by MediaSessionService, then
destroy and renew their MediaSession. We want them to continue to
be addressed.

Don't remove players that destroy their MediaSession, and just forget
the controller instead.

Remove players when their package is removed though.

Report "(unknown)" when we are asked for media info for an addressed
player that has no controller.

Fix a bug where we didn't send updated metadata when it changed.
Fix an ordering bug updating the media session.

Make logging more clear when we're adding / updating / removing players.
Add dumpsys log to confirm current mMediaController is correct.

Use @Nullable and @NonNull to help with distinguishing when
MediaController can be null in AddressedMediaPlayer.

Test: send pause / play from headset, look at bugreport to confirm
Change-Id: I083745f8988e67776716db2bec5e67b697e8ee67
Bug: 36357185
Fixes: 37865298
parent e1839072
Loading
Loading
Loading
Loading
+46 −31
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.bluetooth.avrcp;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.bluetooth.BluetoothAvrcp;
import android.media.session.MediaSession;
import android.media.session.PlaybackState;
@@ -42,15 +44,23 @@ public class AddressedMediaPlayer {
    static private final String TAG = "AddressedMediaPlayer";
    static private final Boolean DEBUG = false;

    static private final long SINGLE_QID = 1;
    static private final String UNKNOWN_TITLE = "(unknown)";

    private AvrcpMediaRspInterface mMediaInterface;
    private List<MediaSession.QueueItem> mNowPlayingList;

    private final List<MediaSession.QueueItem> mUnknownNowPlayingList;

    private long mLastTrackIdSent;

    public AddressedMediaPlayer(AvrcpMediaRspInterface mediaInterface) {
        mNowPlayingList = null;
        mMediaInterface = mediaInterface;
        mLastTrackIdSent = MediaSession.QueueItem.UNKNOWN_ID;
        List<MediaSession.QueueItem> unknown = new ArrayList<MediaSession.QueueItem>();
        unknown.add(getCurrentQueueItem(null, SINGLE_QID));
        mUnknownNowPlayingList = unknown;
    }

    void cleanup() {
@@ -62,7 +72,7 @@ public class AddressedMediaPlayer {

    /* get now playing list from addressed player */
    void getFolderItemsNowPlaying(byte[] bdaddr, AvrcpCmd.FolderItemsCmd reqObj,
            MediaController mediaController) {
            @Nullable MediaController mediaController) {
        if (DEBUG) Log.v(TAG, "getFolderItemsNowPlaying");
        if (mediaController == null) {
            // No players (if a player exists, we would have selected it)
@@ -77,7 +87,7 @@ public class AddressedMediaPlayer {

    /* get item attributes for item in now playing list */
    void getItemAttr(byte[] bdaddr, AvrcpCmd.ItemAttrCmd itemAttr,
            MediaController mediaController) {
            @Nullable MediaController mediaController) {
        int status = AvrcpConstants.RSP_NO_ERROR;
        long mediaId = ByteBuffer.wrap(itemAttr.mUid).getLong();
        List<MediaSession.QueueItem> items = getNowPlayingList(mediaController);
@@ -87,12 +97,6 @@ public class AddressedMediaPlayer {
        if (Arrays.equals(itemAttr.mUid, AvrcpConstants.TRACK_IS_SELECTED)) {
            if (DEBUG) Log.d(TAG, "getItemAttr: Remote requests for now playing contents:");

            if (mediaController == null) {
                Log.e(TAG, "mediaController = null, sending no available players response");
                mMediaInterface.getItemAttrRsp(bdaddr, AvrcpConstants.RSP_NO_AVBL_PLAY, null);
                return;
            }

            // get the current playing metadata and send.
            getItemAttrFilterAttr(bdaddr, itemAttr, getCurrentQueueItem(mediaController, mediaId),
                    mediaController);
@@ -113,10 +117,10 @@ public class AddressedMediaPlayer {

    /* Refresh and get the queue of now playing.
     */
    private List<MediaSession.QueueItem> getNowPlayingList(MediaController mediaController) {
        if (mediaController == null) return null;
    private List<MediaSession.QueueItem> getNowPlayingList(
            @Nullable MediaController mediaController) {
        if (mediaController == null) return mUnknownNowPlayingList;
        if (mNowPlayingList != null) return mNowPlayingList;

        List<MediaSession.QueueItem> items = mediaController.getQueue();
        if (items == null) {
            Log.i(TAG, "null queue from " + mediaController.getPackageName()
@@ -124,7 +128,7 @@ public class AddressedMediaPlayer {
            MediaMetadata metadata = mediaController.getMetadata();
            // Because we are database-unaware, we can just number the item here whatever we want
            // because they have to re-poll it every time.
            MediaSession.QueueItem current = getCurrentQueueItem(mediaController, 1);
            MediaSession.QueueItem current = getCurrentQueueItem(mediaController, SINGLE_QID);
            items = new ArrayList<MediaSession.QueueItem>();
            items.add(current);
        }
@@ -135,7 +139,14 @@ public class AddressedMediaPlayer {
    /* Constructs a queue item representing the current playing metadata from an
     * active controller with queue id |qid|.
     */
    private MediaSession.QueueItem getCurrentQueueItem(MediaController controller, long qid) {
    private MediaSession.QueueItem getCurrentQueueItem(
            @Nullable MediaController controller, long qid) {
        if (controller == null) {
            MediaDescription.Builder bob = new MediaDescription.Builder();
            bob.setTitle(UNKNOWN_TITLE);
            return new QueueItem(bob.build(), qid);
        }

        MediaMetadata metadata = controller.getMetadata();
        if (metadata == null) {
            Log.w(TAG, "Controller has no metadata!? Making an empty one");
@@ -186,12 +197,12 @@ public class AddressedMediaPlayer {
    }

    /* Instructs media player to play particular media item */
    void playItem(byte[] bdaddr, byte[] uid, MediaController mediaController) {
    void playItem(byte[] bdaddr, byte[] uid, @Nullable MediaController mediaController) {
        long qid = ByteBuffer.wrap(uid).getLong();
        List<MediaSession.QueueItem> items = mNowPlayingList;

        if (mediaController == null) {
            Log.e(TAG, "mediaController is null");
            Log.e(TAG, "No mediaController when PlayItem " + qid + " requested");
            mMediaInterface.playItemRsp(bdaddr, AvrcpConstants.RSP_INTERNAL_ERR);
            return;
        }
@@ -218,19 +229,19 @@ public class AddressedMediaPlayer {
        mMediaInterface.playItemRsp(bdaddr, AvrcpConstants.RSP_INV_ITEM);
    }

    void getTotalNumOfItems(byte[] bdaddr, MediaController mediaController) {
    void getTotalNumOfItems(byte[] bdaddr, @Nullable MediaController mediaController) {
        if (DEBUG) Log.d(TAG, "getTotalNumOfItems");
        List<MediaSession.QueueItem> items = mNowPlayingList;
        if (items != null) {
            // We already have the cached list sending the response to remote
            // We already have the cached list, send the response to remote
            mMediaInterface.getTotalNumOfItemsRsp(
                    bdaddr, AvrcpConstants.RSP_NO_ERROR, 0, items.size());
            return;
        }

        if (mediaController == null) {
            Log.e(TAG, "mediaController = null, sending no available players response");
            mMediaInterface.getItemAttrRsp(bdaddr, AvrcpConstants.RSP_NO_AVBL_PLAY, null);
            Log.e(TAG, "getTotalNumOfItems with no mediaController, sending no items");
            mMediaInterface.getTotalNumOfItemsRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR, 0, 0);
            return;
        }

@@ -246,8 +257,9 @@ public class AddressedMediaPlayer {
        mMediaInterface.getTotalNumOfItemsRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR, 0, items.size());
    }

    boolean sendTrackChangeWithId(boolean requesting, MediaController mediaController) {
        if (DEBUG) Log.d(TAG, "sendTrackChangeWithId");
    boolean sendTrackChangeWithId(boolean requesting, @Nullable MediaController mediaController) {
        if (DEBUG)
            Log.d(TAG, "sendTrackChangeWithId (" + requesting + "): controller " + mediaController);
        byte[] track;
        long qid = MediaSession.QueueItem.UNKNOWN_ID;
        if (mediaController != null) {
@@ -299,7 +311,7 @@ public class AddressedMediaPlayer {
     */
    private void getFolderItemsFilterAttr(byte[] bdaddr, AvrcpCmd.FolderItemsCmd folderItemsReqObj,
            List<MediaSession.QueueItem> items, byte scope, long startItem, long endItem,
            MediaController mediaController) {
            @NonNull MediaController mediaController) {
        if (DEBUG) Log.d(TAG, "getFolderItemsFilterAttr: startItem =" + startItem + ", endItem = "
                + endItem);

@@ -397,7 +409,7 @@ public class AddressedMediaPlayer {
    }

    private String getAttrValue(
            int attr, MediaSession.QueueItem item, MediaController mediaController) {
            int attr, MediaSession.QueueItem item, @Nullable MediaController mediaController) {
        String attrValue = null;
        if (item == null) {
            if (DEBUG) Log.d(TAG, "getAttrValue received null item");
@@ -405,12 +417,14 @@ public class AddressedMediaPlayer {
        }
        try {
            MediaDescription desc = item.getDescription();
            PlaybackState state = mediaController.getPlaybackState();
            Bundle extras = desc.getExtras();
            if (mediaController != null) {
                PlaybackState state = mediaController.getPlaybackState();
                if (state != null && (item.getQueueId() == state.getActiveQueueItemId())) {
                    if (DEBUG) Log.d(TAG, "getAttrValue: item is active, filling extra data");
                    extras = fillBundle(mediaController.getMetadata(), extras);
                }
            }
            if (DEBUG) Log.d(TAG, "getAttrValue: item " + item + " : " + desc);
            switch (attr) {
                case AvrcpConstants.ATTRID_TITLE:
@@ -466,7 +480,7 @@ public class AddressedMediaPlayer {
    }

    private void getItemAttrFilterAttr(byte[] bdaddr, AvrcpCmd.ItemAttrCmd mItemAttrReqObj,
            MediaSession.QueueItem mediaItem, MediaController mediaController) {
            MediaSession.QueueItem mediaItem, @Nullable MediaController mediaController) {
        /* Response parameters */
        int[] attrIds = null; /* array of attr ids */
        String[] attrValues = null; /* array of attr values */
@@ -486,14 +500,15 @@ public class AddressedMediaPlayer {
            } else {
                /* get only the requested attribute ids from the request */
                for (int idx = 0; idx < mItemAttrReqObj.mNumAttr; idx++) {
                    if (DEBUG) Log.d(TAG, "getAttrValue: attr id[" + idx + "] :" +
                        mItemAttrReqObj.mAttrIDs[idx]);
                    if (DEBUG)
                        Log.d(TAG, "getItemAttrFilterAttr: attr id[" + idx + "] :"
                                        + mItemAttrReqObj.mAttrIDs[idx]);
                    attrTempId.add(mItemAttrReqObj.mAttrIDs[idx]);
                }
            }
        }

        if (DEBUG) Log.d(TAG, "getAttrValue: attr id list size:" + attrTempId.size());
        if (DEBUG) Log.d(TAG, "getItemAttrFilterAttr: attr id list size:" + attrTempId.size());
        /* lookup and copy values of attributes for ids requested above */
        for (int idx = 0; idx < attrTempId.size(); idx++) {
            /* check if media player provided requested attributes */
+58 −32
Original line number Diff line number Diff line
@@ -112,6 +112,8 @@ public final class Avrcp {
    private int mAbsVolRetryTimes;
    private int mSkipAmount;

    private static final int NO_PLAYER_ID = 0;

    private int mCurrAddrPlayerID;
    private int mCurrBrowsePlayerID;
    private int mLastUsedPlayerID;
@@ -256,7 +258,7 @@ public final class Avrcp {
        mAbsVolThreshold = 0;
        mVolumeMapping = new HashMap<Integer, Integer>();
        sUIDCounter = AvrcpConstants.DEFAULT_UID_COUNTER;
        mCurrAddrPlayerID = 0;
        mCurrAddrPlayerID = NO_PLAYER_ID;
        mCurrBrowsePlayerID = 0;
        mContext = context;
        mLastUsedPlayerID = 0;
@@ -374,6 +376,11 @@ public final class Avrcp {
        @Override
        public void onSessionDestroyed() {
            Log.v(TAG, "MediaController session destroyed");
            if (mMediaController != null) {
                removeMediaController(mMediaController.getWrappedInstance());
                mMediaController.unregisterCallback(mMediaControllerCb);
                mMediaController = null;
            }
        }

        @Override
@@ -933,12 +940,14 @@ public final class Avrcp {
    }

    private void updateCurrentMediaState() {
        MediaAttributes currentAttributes = mMediaAttributes;
        PlaybackState newState = mCurrentPlayState;
        if (mMediaController == null) {
            // Use A2DP state if we don't have a MediaControlller
            boolean isPlaying = (mA2dpState == BluetoothA2dp.STATE_PLAYING);
            boolean isPlaying =
                    (mA2dpState == BluetoothA2dp.STATE_PLAYING) && mAudioManager.isMusicActive();
            if (isPlaying != isPlayingState(mCurrentPlayState)) {
                /* if a2dp is streaming, check to make sure music is active */
                if (isPlaying && !mAudioManager.isMusicActive()) return;
                PlaybackState.Builder builder = new PlaybackState.Builder();
                if (isPlaying) {
                    builder.setState(PlaybackState.STATE_PLAYING,
@@ -947,26 +956,21 @@ public final class Avrcp {
                    builder.setState(PlaybackState.STATE_PAUSED,
                            PlaybackState.PLAYBACK_POSITION_UNKNOWN, 0.0f);
                }
                updatePlaybackState(builder.build());
            }
            // Can't get metadata from A2dp so we're done.
            return;
                newState = builder.build();
            }

        MediaAttributes currentAttributes = mMediaAttributes;

        PlaybackState newState = mMediaController.getPlaybackState();

        // Metadata
            mMediaAttributes = new MediaAttributes(null);
        } else {
            newState = mMediaController.getPlaybackState();
            mMediaAttributes = new MediaAttributes(mMediaController.getMetadata());
        }

        if (currentAttributes.equals(mMediaAttributes)) {
        if (!currentAttributes.equals(mMediaAttributes)) {
            Log.v(TAG, "MediaAttributes Changed to " + mMediaAttributes.toString());
            mTracksPlayed++;
            sendTrackChangedRsp(false);
        }

        updatePlaybackState(mMediaController.getPlaybackState());
        updatePlaybackState(newState);
    }

    private void getRcFeaturesRequestFromNative(byte[] address, int features) {
@@ -1409,6 +1413,7 @@ public final class Avrcp {
        if (DEBUG) Log.d(TAG, "packageName: " + packageName + " removed: " + removed);

        if (removed) {
            removeMediaPlayerInfo(packageName);
            // old package is removed, updating local browsable player's list
            if (isBrowseSupported(packageName)) {
                removePackageFromBrowseList(packageName);
@@ -1562,14 +1567,18 @@ public final class Avrcp {
                            getMediaControllers();
                    for (android.media.session.MediaController controller : currentControllers) {
                        if (!newControllers.contains(controller)) {
                            removeMediaPlayerInfo(controller.getPackageName());
                            playersChanged = true;
                            removeMediaController(controller);
                            if (mMediaController != null && mMediaController.equals(controller)) {
                                if (DEBUG) Log.v(TAG, "Active Controller is gone!");
                                mMediaController.unregisterCallback(mMediaControllerCb);
                                mMediaController = null;
                            }
                        }
                    }

                    if (playersChanged) {
                        mHandler.sendEmptyMessage(MSG_AVAILABLE_PLAYERS_CHANGED_RSP);
                        if (newControllers.size() > 0 && (mMediaController == null)) {
                        if (newControllers.size() > 0 && getAddressedPlayerInfo() == null) {
                            if (DEBUG)
                                Log.v(TAG,
                                        "No addressed player but active sessions, taking first.");
@@ -1794,7 +1803,6 @@ public final class Avrcp {
     *  @return true if an item was updated, false if it was added instead
     */
    private boolean addMediaPlayerInfo(MediaPlayerInfo info) {
        if (DEBUG) Log.d(TAG, "add " + info.toString());
        int updateId = -1;
        boolean updated = false;
        synchronized (mMediaPlayerInfoList) {
@@ -1809,10 +1817,13 @@ public final class Avrcp {
                // New player
                mLastUsedPlayerID++;
                updateId = mLastUsedPlayerID;
            } else if (updateId == mCurrAddrPlayerID) {
                updateCurrentController(mCurrAddrPlayerID, mCurrBrowsePlayerID);
            }
            mMediaPlayerInfoList.put(updateId, info);
            if (DEBUG)
                Log.d(TAG, (updated ? "update #" : "add #") + updateId + ":" + info.toString());
            if (updateId == mCurrAddrPlayerID) {
                updateCurrentController(mCurrAddrPlayerID, mCurrBrowsePlayerID);
            }
        }
        return updated;
    }
@@ -1828,6 +1839,8 @@ public final class Avrcp {
                }
            }
            if (removeKey != -1) {
                if (DEBUG)
                    Log.d(TAG, "remove #" + removeKey + ":" + mMediaPlayerInfoList.get(removeKey));
                return mMediaPlayerInfoList.remove(removeKey);
            }

@@ -1835,6 +1848,15 @@ public final class Avrcp {
        }
    }

    /** Remove the controller referenced by |controller| from any player in the list */
    private void removeMediaController(android.media.session.MediaController controller) {
        synchronized (mMediaPlayerInfoList) {
            for (MediaPlayerInfo info : mMediaPlayerInfoList.values()) {
                if (info.getMediaController().equals(controller)) info.setMediaController(null);
            }
        }
    }

    /*
     * utility function to get the playback state of any media player through
     * media controller APIs.
@@ -2096,11 +2118,10 @@ public final class Avrcp {

        if (DEBUG)
            Log.d(TAG, "updateCurrentController: " + mMediaController + " to " + newController);
        if (mMediaController == null || newController == null
                || (mMediaController.getWrappedInstance() != newController.getWrappedInstance())) {
        if (mMediaController == null || (!mMediaController.equals(newController))) {
            if (mMediaController != null) mMediaController.unregisterCallback(mMediaControllerCb);
            if (newController != null) {
            mMediaController = newController;
            if (mMediaController != null) {
                mMediaController.registerCallback(mMediaControllerCb, mHandler);
                mAddressedMediaPlayer.updateNowPlayingList(mMediaController.getQueue());
            } else {
@@ -2184,12 +2205,16 @@ public final class Avrcp {

    private void handleGetItemAttr(AvrcpCmd.ItemAttrCmd itemAttr) {
        if (itemAttr.mScope == AvrcpConstants.BTRC_SCOPE_NOW_PLAYING) {
            if (mCurrAddrPlayerID == NO_PLAYER_ID) {
                getItemAttrRspNative(
                        itemAttr.mAddress, AvrcpConstants.RSP_NO_AVBL_PLAY, (byte) 0, null, null);
            } else {
                mAddressedMediaPlayer.getItemAttr(itemAttr.mAddress, itemAttr, mMediaController);
            }
        else {
            if (mAvrcpBrowseManager.getBrowsedMediaPlayer(itemAttr.mAddress) != null)
        } else {
            if (mAvrcpBrowseManager.getBrowsedMediaPlayer(itemAttr.mAddress) != null) {
                mAvrcpBrowseManager.getBrowsedMediaPlayer(itemAttr.mAddress).getItemAttr(itemAttr);
            else {
            } else {
                Log.e(TAG, "Could not get attributes. mBrowsedMediaPlayer is null");
                getItemAttrRspNative(itemAttr.mAddress, AvrcpConstants.RSP_INTERNAL_ERR,
                        (byte) 0, null, null);
@@ -2279,7 +2304,8 @@ public final class Avrcp {
        ProfileService.println(sb, "mSkipAmount: " + mSkipAmount);
        ProfileService.println(sb, "mVolumeMapping: " + mVolumeMapping.toString());
        if (mMediaController != null)
            ProfileService.println(sb, "mMediaSession pkg: " + mMediaController.getPackageName());
            ProfileService.println(sb, "mMediaController: " + mMediaController.getWrappedInstance()
                            + " pkg " + mMediaController.getPackageName());

        ProfileService.println(sb, "\nMedia Players:");
        synchronized (mMediaPlayerInfoList) {
+16 −0
Original line number Diff line number Diff line
@@ -139,6 +139,22 @@ public class MediaController {
        return mDelegate.controlsSameSession(other);
    }

    @Override
    public boolean equals(Object o) {
        if (o instanceof android.media.session.MediaController) {
            return mDelegate.equals(o);
        } else if (o instanceof MediaController) {
            MediaController other = (MediaController) o;
            return mDelegate.equals(other.mDelegate);
        }
        return false;
    }

    @Override
    public String toString() {
        return super.toString() + "(wraps " + mDelegate + ")";
    }

    public static abstract class Callback extends android.media.session.MediaController.Callback { }

    public class TransportControls {