AddressedMediaPlayer.java revision 5aca05c1d79f3412b6964b3b6335ad6f2d558756
1/*
2 * Copyright (C) 2016 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.bluetooth.avrcp;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.bluetooth.BluetoothAvrcp;
22import android.media.session.MediaSession;
23import android.media.session.PlaybackState;
24import android.media.session.MediaSession.QueueItem;
25import android.media.MediaDescription;
26import android.media.MediaMetadata;
27import android.os.Bundle;
28import android.util.Log;
29
30import com.android.bluetooth.btservice.ProfileService;
31import com.android.bluetooth.Utils;
32
33import java.nio.ByteBuffer;
34import java.util.List;
35import java.util.Arrays;
36import java.util.ArrayList;
37
38/*************************************************************************************************
39 * Provides functionality required for Addressed Media Player, like Now Playing List related
40 * browsing commands, control commands to the current addressed player(playItem, play, pause, etc)
41 * Acts as an Interface to communicate with media controller APIs for NowPlayingItems.
42 ************************************************************************************************/
43
44public class AddressedMediaPlayer {
45    static private final String TAG = "AddressedMediaPlayer";
46    static private final Boolean DEBUG = false;
47
48    static private final long SINGLE_QID = 1;
49    static private final String UNKNOWN_TITLE = "(unknown)";
50
51    private AvrcpMediaRspInterface mMediaInterface;
52    private @NonNull List<MediaSession.QueueItem> mNowPlayingList;
53
54    private final List<MediaSession.QueueItem> mEmptyNowPlayingList;
55
56    private long mLastTrackIdSent;
57
58    public AddressedMediaPlayer(AvrcpMediaRspInterface mediaInterface) {
59        mEmptyNowPlayingList = new ArrayList<MediaSession.QueueItem>();
60        mNowPlayingList = mEmptyNowPlayingList;
61        mMediaInterface = mediaInterface;
62        mLastTrackIdSent = MediaSession.QueueItem.UNKNOWN_ID;
63    }
64
65    void cleanup() {
66        if (DEBUG) Log.v(TAG, "cleanup");
67        mNowPlayingList = mEmptyNowPlayingList;
68        mMediaInterface = null;
69        mLastTrackIdSent = MediaSession.QueueItem.UNKNOWN_ID;
70    }
71
72    /* get now playing list from addressed player */
73    void getFolderItemsNowPlaying(byte[] bdaddr, AvrcpCmd.FolderItemsCmd reqObj,
74            @Nullable MediaController mediaController) {
75        if (DEBUG) Log.v(TAG, "getFolderItemsNowPlaying");
76        if (mediaController == null) {
77            // No players (if a player exists, we would have selected it)
78            Log.e(TAG, "mediaController = null, sending no available players response");
79            mMediaInterface.folderItemsRsp(bdaddr, AvrcpConstants.RSP_NO_AVBL_PLAY, null);
80            return;
81        }
82        List<MediaSession.QueueItem> items = getNowPlayingList(mediaController);
83        getFolderItemsFilterAttr(bdaddr, reqObj, items, AvrcpConstants.BTRC_SCOPE_NOW_PLAYING,
84                reqObj.mStartItem, reqObj.mEndItem, mediaController);
85    }
86
87    /* get item attributes for item in now playing list */
88    void getItemAttr(byte[] bdaddr, AvrcpCmd.ItemAttrCmd itemAttr,
89            @Nullable MediaController mediaController) {
90        int status = AvrcpConstants.RSP_NO_ERROR;
91        long mediaId = ByteBuffer.wrap(itemAttr.mUid).getLong();
92        List<MediaSession.QueueItem> items = getNowPlayingList(mediaController);
93
94        // NOTE: this is out-of-spec (AVRCP 1.6.1 sec 6.10.4.3, p90) but we answer it anyway
95        // because some CTs ask for it.
96        if (Arrays.equals(itemAttr.mUid, AvrcpConstants.TRACK_IS_SELECTED)) {
97            if (DEBUG) Log.d(TAG, "getItemAttr: Remote requests for now playing contents:");
98
99            // get the current playing metadata and send.
100            getItemAttrFilterAttr(bdaddr, itemAttr, getCurrentQueueItem(mediaController, mediaId),
101                    mediaController);
102            return;
103        }
104
105        if (DEBUG) Log.d(TAG, "getItemAttr-UID: 0x" + Utils.byteArrayToString(itemAttr.mUid));
106        for (MediaSession.QueueItem item : items) {
107            if (item.getQueueId() == mediaId) {
108                getItemAttrFilterAttr(bdaddr, itemAttr, item, mediaController);
109                return;
110            }
111        }
112
113        // Couldn't find it, so the id is invalid
114        mMediaInterface.getItemAttrRsp(bdaddr, AvrcpConstants.RSP_INV_ITEM, null);
115    }
116
117    /* Refresh and get the queue of now playing.
118     */
119    private @NonNull List<MediaSession.QueueItem> getNowPlayingList(
120            @Nullable MediaController mediaController) {
121        if (mediaController == null) return mEmptyNowPlayingList;
122        List<MediaSession.QueueItem> items = mediaController.getQueue();
123        if (items == mNowPlayingList) return mNowPlayingList;
124        if (items == null) {
125            Log.i(TAG, "null queue from " + mediaController.getPackageName()
126                            + ", constructing single-item list");
127            MediaMetadata metadata = mediaController.getMetadata();
128            // Because we are database-unaware, we can just number the item here whatever we want
129            // because they have to re-poll it every time.
130            MediaSession.QueueItem current = getCurrentQueueItem(mediaController, SINGLE_QID);
131            items = new ArrayList<MediaSession.QueueItem>();
132            items.add(current);
133        }
134        mNowPlayingList = items;
135        // TODO (jamuraa): test to see if the single-item queue is the same and don't send
136        mMediaInterface.nowPlayingChangedRsp(AvrcpConstants.NOTIFICATION_TYPE_CHANGED);
137        return items;
138    }
139
140    /* Constructs a queue item representing the current playing metadata from an
141     * active controller with queue id |qid|.
142     */
143    private MediaSession.QueueItem getCurrentQueueItem(
144            @Nullable MediaController controller, long qid) {
145        if (controller == null) {
146            MediaDescription.Builder bob = new MediaDescription.Builder();
147            bob.setTitle(UNKNOWN_TITLE);
148            return new QueueItem(bob.build(), qid);
149        }
150
151        MediaMetadata metadata = controller.getMetadata();
152        if (metadata == null) {
153            Log.w(TAG, "Controller has no metadata!? Making an empty one");
154            metadata = (new MediaMetadata.Builder()).build();
155        }
156
157        MediaDescription.Builder bob = new MediaDescription.Builder();
158        MediaDescription desc = metadata.getDescription();
159
160        // set the simple ones that MediaMetadata builds for us
161        bob.setMediaId(desc.getMediaId());
162        bob.setTitle(desc.getTitle());
163        bob.setSubtitle(desc.getSubtitle());
164        bob.setDescription(desc.getDescription());
165        // fill the ones that we use later
166        bob.setExtras(fillBundle(metadata, desc.getExtras()));
167
168        // build queue item with the new metadata
169        desc = bob.build();
170        return new QueueItem(desc, qid);
171    }
172
173    private Bundle fillBundle(MediaMetadata metadata, Bundle currentExtras) {
174        if (metadata == null) {
175            return currentExtras;
176        }
177
178        Bundle bundle = currentExtras;
179        if (bundle == null) bundle = new Bundle();
180
181        String[] stringKeys = {MediaMetadata.METADATA_KEY_TITLE, MediaMetadata.METADATA_KEY_ARTIST,
182                MediaMetadata.METADATA_KEY_ALBUM, MediaMetadata.METADATA_KEY_GENRE};
183        for (String key : stringKeys) {
184            String current = bundle.getString(key);
185            if (current == null) bundle.putString(key, metadata.getString(key));
186        }
187
188        String[] longKeys = {MediaMetadata.METADATA_KEY_TRACK_NUMBER,
189                MediaMetadata.METADATA_KEY_NUM_TRACKS, MediaMetadata.METADATA_KEY_DURATION};
190        for (String key : longKeys) {
191            if (!bundle.containsKey(key)) bundle.putLong(key, metadata.getLong(key));
192        }
193        return bundle;
194    }
195
196    void updateNowPlayingList(@Nullable MediaController mediaController) {
197        getNowPlayingList(mediaController);
198    }
199
200    /* Instructs media player to play particular media item */
201    void playItem(byte[] bdaddr, byte[] uid, @Nullable MediaController mediaController) {
202        long qid = ByteBuffer.wrap(uid).getLong();
203        List<MediaSession.QueueItem> items = getNowPlayingList(mediaController);
204
205        if (mediaController == null) {
206            Log.e(TAG, "No mediaController when PlayItem " + qid + " requested");
207            mMediaInterface.playItemRsp(bdaddr, AvrcpConstants.RSP_INTERNAL_ERR);
208            return;
209        }
210
211        MediaController.TransportControls mediaControllerCntrl =
212                mediaController.getTransportControls();
213
214        if (items == null) {
215            Log.w(TAG, "nowPlayingItems is null");
216            mMediaInterface.playItemRsp(bdaddr, AvrcpConstants.RSP_INTERNAL_ERR);
217            return;
218        }
219
220        for (MediaSession.QueueItem item : items) {
221            if (qid == item.getQueueId()) {
222                if (DEBUG) Log.d(TAG, "Skipping to ID " + qid);
223                mediaControllerCntrl.skipToQueueItem(qid);
224                mMediaInterface.playItemRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR);
225                return;
226            }
227        }
228
229        Log.w(TAG, "Invalid now playing Queue ID " + qid);
230        mMediaInterface.playItemRsp(bdaddr, AvrcpConstants.RSP_INV_ITEM);
231    }
232
233    void getTotalNumOfItems(byte[] bdaddr, @Nullable MediaController mediaController) {
234        List<MediaSession.QueueItem> items = getNowPlayingList(mediaController);
235        if (DEBUG) Log.d(TAG, "getTotalNumOfItems: " + items.size() + " items.");
236        mMediaInterface.getTotalNumOfItemsRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR, 0, items.size());
237    }
238
239    void sendTrackChangeWithId(int type, @Nullable MediaController mediaController) {
240        if (DEBUG)
241            Log.d(TAG, "sendTrackChangeWithId (" + type + "): controller " + mediaController);
242        long qid = getActiveQueueItemId(mediaController);
243        byte[] track = ByteBuffer.allocate(AvrcpConstants.UID_SIZE).putLong(qid).array();
244        mMediaInterface.trackChangedRsp(type, track);
245        mLastTrackIdSent = qid;
246        // The nowPlaying might have changed.
247        updateNowPlayingList(mediaController);
248    }
249
250    /*
251     * helper method to check if startItem and endItem index is with range of
252     * MediaItem list. (Resultset containing all items in current path)
253     */
254    private @Nullable List<MediaSession.QueueItem> getQueueSubset(
255            @NonNull List<MediaSession.QueueItem> items, long startItem, long endItem) {
256        if (endItem > items.size()) endItem = items.size() - 1;
257        if (startItem > Integer.MAX_VALUE) startItem = Integer.MAX_VALUE;
258        try {
259            List<MediaSession.QueueItem> selected =
260                    items.subList((int) startItem, (int) Math.min(items.size(), endItem + 1));
261            if (selected.isEmpty()) {
262                Log.i(TAG, "itemsSubList is empty.");
263                return null;
264            }
265            return selected;
266        } catch (IndexOutOfBoundsException ex) {
267            Log.i(TAG, "Range (" + startItem + ", " + endItem + ") invalid");
268        } catch (IllegalArgumentException ex) {
269            Log.i(TAG, "Range start " + startItem + " > size (" + items.size() + ")");
270        }
271        return null;
272    }
273
274    /*
275     * helper method to filter required attibutes before sending GetFolderItems
276     * response
277     */
278    private void getFolderItemsFilterAttr(byte[] bdaddr, AvrcpCmd.FolderItemsCmd folderItemsReqObj,
279            @NonNull List<MediaSession.QueueItem> items, byte scope, long startItem, long endItem,
280            @NonNull MediaController mediaController) {
281        if (DEBUG) Log.d(TAG, "getFolderItemsFilterAttr: startItem =" + startItem + ", endItem = "
282                + endItem);
283
284        List<MediaSession.QueueItem> result_items = getQueueSubset(items, startItem, endItem);
285        /* check for index out of bound errors */
286        if (result_items == null) {
287            Log.w(TAG, "getFolderItemsFilterAttr: result_items is empty");
288            mMediaInterface.folderItemsRsp(bdaddr, AvrcpConstants.RSP_INV_RANGE, null);
289            return;
290        }
291
292        FolderItemsData folderDataNative = new FolderItemsData(result_items.size());
293
294        /* variables to accumulate attrs */
295        ArrayList<String> attrArray = new ArrayList<String>();
296        ArrayList<Integer> attrId = new ArrayList<Integer>();
297
298        for (int itemIndex = 0; itemIndex < result_items.size(); itemIndex++) {
299            MediaSession.QueueItem item = result_items.get(itemIndex);
300            // get the queue id
301            long qid = item.getQueueId();
302            byte[] uid = ByteBuffer.allocate(AvrcpConstants.UID_SIZE).putLong(qid).array();
303
304            // get the array of uid from 2d to array 1D array
305            for (int idx = 0; idx < AvrcpConstants.UID_SIZE; idx++) {
306                folderDataNative.mItemUid[itemIndex * AvrcpConstants.UID_SIZE + idx] = uid[idx];
307            }
308
309            /* Set display name for current item */
310            folderDataNative.mDisplayNames[itemIndex] =
311                    getAttrValue(AvrcpConstants.ATTRID_TITLE, item, mediaController);
312
313            int maxAttributesRequested = 0;
314            boolean isAllAttribRequested = false;
315            /* check if remote requested for attributes */
316            if (folderItemsReqObj.mNumAttr != AvrcpConstants.NUM_ATTR_NONE) {
317                int attrCnt = 0;
318
319                /* add requested attr ids to a temp array */
320                if (folderItemsReqObj.mNumAttr == AvrcpConstants.NUM_ATTR_ALL) {
321                    isAllAttribRequested = true;
322                    maxAttributesRequested = AvrcpConstants.MAX_NUM_ATTR;
323                } else {
324                    /* get only the requested attribute ids from the request */
325                    maxAttributesRequested = folderItemsReqObj.mNumAttr;
326                }
327
328                /* lookup and copy values of attributes for ids requested above */
329                for (int idx = 0; idx < maxAttributesRequested; idx++) {
330                    /* check if media player provided requested attributes */
331                    String value = null;
332
333                    int attribId =
334                            isAllAttribRequested ? (idx + 1) : folderItemsReqObj.mAttrIDs[idx];
335                    value = getAttrValue(attribId, item, mediaController);
336                    if (value != null) {
337                        attrArray.add(value);
338                        attrId.add(attribId);
339                        attrCnt++;
340                    }
341                }
342                /* add num attr actually received from media player for a particular item */
343                folderDataNative.mAttributesNum[itemIndex] = attrCnt;
344            }
345        }
346
347        /* copy filtered attr ids and attr values to response parameters */
348        if (folderItemsReqObj.mNumAttr != AvrcpConstants.NUM_ATTR_NONE) {
349            folderDataNative.mAttrIds = new int[attrId.size()];
350            for (int attrIndex = 0; attrIndex < attrId.size(); attrIndex++)
351                folderDataNative.mAttrIds[attrIndex] = attrId.get(attrIndex);
352            folderDataNative.mAttrValues = attrArray.toArray(new String[attrArray.size()]);
353        }
354        for (int attrIndex = 0; attrIndex < folderDataNative.mAttributesNum.length; attrIndex++)
355            if (DEBUG)
356                Log.d(TAG, "folderDataNative.mAttributesNum"
357                                + folderDataNative.mAttributesNum[attrIndex] + " attrIndex "
358                                + attrIndex);
359
360        /* create rsp object and send response to remote device */
361        FolderItemsRsp rspObj = new FolderItemsRsp(AvrcpConstants.RSP_NO_ERROR, Avrcp.sUIDCounter,
362                scope, folderDataNative.mNumItems, folderDataNative.mFolderTypes,
363                folderDataNative.mPlayable, folderDataNative.mItemTypes, folderDataNative.mItemUid,
364                folderDataNative.mDisplayNames, folderDataNative.mAttributesNum,
365                folderDataNative.mAttrIds, folderDataNative.mAttrValues);
366        mMediaInterface.folderItemsRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR, rspObj);
367    }
368
369    private String getAttrValue(
370            int attr, MediaSession.QueueItem item, @Nullable MediaController mediaController) {
371        String attrValue = null;
372        if (item == null) {
373            if (DEBUG) Log.d(TAG, "getAttrValue received null item");
374            return null;
375        }
376        try {
377            MediaDescription desc = item.getDescription();
378            Bundle extras = desc.getExtras();
379            boolean isCurrentTrack = item.getQueueId() == getActiveQueueItemId(mediaController);
380            if (isCurrentTrack) {
381                if (DEBUG) Log.d(TAG, "getAttrValue: item is active, using current data");
382                extras = fillBundle(mediaController.getMetadata(), extras);
383            }
384            if (DEBUG) Log.d(TAG, "getAttrValue: item " + item + " : " + desc);
385            switch (attr) {
386                case AvrcpConstants.ATTRID_TITLE:
387                    /* Title is mandatory attribute */
388                    if (isCurrentTrack) {
389                        attrValue = extras.getString(MediaMetadata.METADATA_KEY_TITLE);
390                    } else {
391                        attrValue = desc.getTitle().toString();
392                    }
393                    break;
394
395                case AvrcpConstants.ATTRID_ARTIST:
396                    attrValue = extras.getString(MediaMetadata.METADATA_KEY_ARTIST);
397                    break;
398
399                case AvrcpConstants.ATTRID_ALBUM:
400                    attrValue = extras.getString(MediaMetadata.METADATA_KEY_ALBUM);
401                    break;
402
403                case AvrcpConstants.ATTRID_TRACK_NUM:
404                    attrValue =
405                            Long.toString(extras.getLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER));
406                    break;
407
408                case AvrcpConstants.ATTRID_NUM_TRACKS:
409                    attrValue =
410                            Long.toString(extras.getLong(MediaMetadata.METADATA_KEY_NUM_TRACKS));
411                    break;
412
413                case AvrcpConstants.ATTRID_GENRE:
414                    attrValue = extras.getString(MediaMetadata.METADATA_KEY_GENRE);
415                    break;
416
417                case AvrcpConstants.ATTRID_PLAY_TIME:
418                    attrValue = Long.toString(extras.getLong(MediaMetadata.METADATA_KEY_DURATION));
419                    break;
420
421                case AvrcpConstants.ATTRID_COVER_ART:
422                    Log.e(TAG, "getAttrValue: Cover art attribute not supported");
423                    return null;
424
425                default:
426                    Log.e(TAG, "getAttrValue: Unknown attribute ID requested: " + attr);
427                    return null;
428            }
429        } catch (NullPointerException ex) {
430            Log.w(TAG, "getAttrValue: attr id not found in result");
431            /* checking if attribute is title, then it is mandatory and cannot send null */
432            if (attr == AvrcpConstants.ATTRID_TITLE) {
433                attrValue = "<Unknown Title>";
434            } else {
435                return null;
436            }
437        }
438        if (DEBUG) Log.d(TAG, "getAttrValue: attrvalue = " + attrValue + ", attr id:" + attr);
439        return attrValue;
440    }
441
442    private void getItemAttrFilterAttr(byte[] bdaddr, AvrcpCmd.ItemAttrCmd mItemAttrReqObj,
443            MediaSession.QueueItem mediaItem, @Nullable MediaController mediaController) {
444        /* Response parameters */
445        int[] attrIds = null; /* array of attr ids */
446        String[] attrValues = null; /* array of attr values */
447
448        /* variables to temperorily add attrs */
449        ArrayList<String> attrArray = new ArrayList<String>();
450        ArrayList<Integer> attrId = new ArrayList<Integer>();
451        ArrayList<Integer> attrTempId = new ArrayList<Integer>();
452
453        /* check if remote device has requested for attributes */
454        if (mItemAttrReqObj.mNumAttr != AvrcpConstants.NUM_ATTR_NONE) {
455            if (mItemAttrReqObj.mNumAttr == AvrcpConstants.NUM_ATTR_ALL) {
456                for (int idx = 1; idx < AvrcpConstants.MAX_NUM_ATTR; idx++) {
457                    attrTempId.add(idx); /* attr id 0x00 is unused */
458                }
459            } else {
460                /* get only the requested attribute ids from the request */
461                for (int idx = 0; idx < mItemAttrReqObj.mNumAttr; idx++) {
462                    if (DEBUG)
463                        Log.d(TAG, "getItemAttrFilterAttr: attr id[" + idx + "] :"
464                                        + mItemAttrReqObj.mAttrIDs[idx]);
465                    attrTempId.add(mItemAttrReqObj.mAttrIDs[idx]);
466                }
467            }
468        }
469
470        if (DEBUG) Log.d(TAG, "getItemAttrFilterAttr: attr id list size:" + attrTempId.size());
471        /* lookup and copy values of attributes for ids requested above */
472        for (int idx = 0; idx < attrTempId.size(); idx++) {
473            /* check if media player provided requested attributes */
474            String value = getAttrValue(attrTempId.get(idx), mediaItem, mediaController);
475            if (value != null) {
476                attrArray.add(value);
477                attrId.add(attrTempId.get(idx));
478            }
479        }
480
481        /* copy filtered attr ids and attr values to response parameters */
482        if (mItemAttrReqObj.mNumAttr != AvrcpConstants.NUM_ATTR_NONE) {
483            attrIds = new int[attrId.size()];
484
485            for (int attrIndex = 0; attrIndex < attrId.size(); attrIndex++)
486                attrIds[attrIndex] = attrId.get(attrIndex);
487
488            attrValues = attrArray.toArray(new String[attrId.size()]);
489
490            /* create rsp object and send response */
491            ItemAttrRsp rspObj = new ItemAttrRsp(AvrcpConstants.RSP_NO_ERROR, attrIds, attrValues);
492            mMediaInterface.getItemAttrRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR, rspObj);
493            return;
494        }
495    }
496
497    private long getActiveQueueItemId(@Nullable MediaController controller) {
498        if (controller == null) return MediaSession.QueueItem.UNKNOWN_ID;
499        PlaybackState state = controller.getPlaybackState();
500        if (state == null) return MediaSession.QueueItem.UNKNOWN_ID;
501        long qid = state.getActiveQueueItemId();
502        if (qid != MediaSession.QueueItem.UNKNOWN_ID) return qid;
503        // Check if we're presenting a "one item queue"
504        if (controller.getMetadata() != null) return SINGLE_QID;
505        return MediaSession.QueueItem.UNKNOWN_ID;
506    }
507
508    public void dump(StringBuilder sb, @Nullable MediaController mediaController) {
509        ProfileService.println(sb, "AddressedPlayer info:");
510        ProfileService.println(sb, "mLastTrackIdSent: " + mLastTrackIdSent);
511        ProfileService.println(sb, "mNowPlayingList: " + mNowPlayingList.size() + " elements");
512        long currentQueueId = getActiveQueueItemId(mediaController);
513        for (MediaSession.QueueItem item : mNowPlayingList) {
514            long itemId = item.getQueueId();
515            ProfileService.println(sb, (itemId == currentQueueId ? "*" : " ") + item.toString());
516        }
517    }
518}
519