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