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