1 /*
2  * Copyright (C) 2020 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 
17 package com.android.bluetooth.avrcpcontroller;
18 
19 import android.bluetooth.BluetoothDevice;
20 import android.net.Uri;
21 import android.os.Bundle;
22 import android.support.v4.media.MediaBrowserCompat.MediaItem;
23 import android.support.v4.media.MediaDescriptionCompat;
24 import android.support.v4.media.MediaMetadataCompat;
25 import android.util.Log;
26 
27 import java.util.Objects;
28 
29 /**
30  * An object representing a single item returned from an AVRCP folder listing in the VFS scope.
31  *
32  * This object knows how to turn itself into each of the Android Media Framework objects so the
33  * metadata can easily be shared with the system.
34  */
35 public class AvrcpItem {
36     private static final String TAG = "AvrcpItem";
37     private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
38 
39     // AVRCP Specification defined item types
40     public static final int TYPE_PLAYER = 0x1;
41     public static final int TYPE_FOLDER = 0x2;
42     public static final int TYPE_MEDIA = 0x3;
43 
44     // AVRCP Specification defined folder item sub types. These match with the Media Framework's
45     // definition of the constants as well.
46     public static final int FOLDER_MIXED = 0x00;
47     public static final int FOLDER_TITLES = 0x01;
48     public static final int FOLDER_ALBUMS = 0x02;
49     public static final int FOLDER_ARTISTS = 0x03;
50     public static final int FOLDER_GENRES = 0x04;
51     public static final int FOLDER_PLAYLISTS = 0x05;
52     public static final int FOLDER_YEARS = 0x06;
53 
54     // AVRCP Specification defined media item sub types
55     public static final int MEDIA_AUDIO = 0x00;
56     public static final int MEDIA_VIDEO = 0x01;
57 
58     // Keys for packaging extra data with MediaItems
59     public static final String AVRCP_ITEM_KEY_UID = "avrcp-item-key-uid";
60 
61     // Type of item, one of [TYPE_PLAYER, TYPE_FOLDER, TYPE_MEDIA]
62     private int mItemType;
63 
64     // Sub type of item, dependant on whether it's a folder or media item
65     // Folder -> FOLDER_* constants
66     // Media -> MEDIA_* constants
67     private int mType;
68 
69     // Bluetooth Device this piece of metadata came from
70     private BluetoothDevice mDevice;
71 
72     // AVRCP Specification defined metadata for browsed media items
73     private long mUid;
74     private String mDisplayableName;
75 
76     // AVRCP Specification defined set of available attributes
77     private String mTitle;
78     private String mArtistName;
79     private String mAlbumName;
80     private long mTrackNumber;
81     private long mTotalNumberOfTracks;
82     private String mGenre;
83     private long mPlayingTime;
84     private String mCoverArtHandle;
85 
86     private boolean mPlayable = false;
87     private boolean mBrowsable = false;
88 
89     // Our own book keeping value since database unaware players sometimes send repeat UIDs.
90     private String mUuid;
91 
92     // A status to indicate if the image at the URI is downloaded and cached
93     private String mImageUuid = null;
94 
95     // Our own internal Uri value that points to downloaded cover art image
96     private Uri mImageUri;
97 
AvrcpItem()98     private AvrcpItem() {
99     }
100 
getDevice()101     public BluetoothDevice getDevice() {
102         return mDevice;
103     }
104 
getUid()105     public long getUid() {
106         return mUid;
107     }
108 
getUuid()109     public String getUuid() {
110         return mUuid;
111     }
112 
getItemType()113     public int getItemType() {
114         return mItemType;
115     }
116 
getType()117     public int getType() {
118         return mType;
119     }
120 
getDisplayableName()121     public String getDisplayableName() {
122         return mDisplayableName;
123     }
124 
getTitle()125     public String getTitle() {
126         return mTitle;
127     }
128 
getArtistName()129     public String getArtistName() {
130         return mArtistName;
131     }
132 
getAlbumName()133     public String getAlbumName() {
134         return mAlbumName;
135     }
136 
getTrackNumber()137     public long getTrackNumber() {
138         return mTrackNumber;
139     }
140 
getTotalNumberOfTracks()141     public long getTotalNumberOfTracks() {
142         return mTotalNumberOfTracks;
143     }
144 
getGenre()145     public String getGenre() {
146         return mGenre;
147     }
148 
getPlayingTime()149     public long getPlayingTime() {
150         return mPlayingTime;
151     }
152 
isPlayable()153     public boolean isPlayable() {
154         return mPlayable;
155     }
156 
isBrowsable()157     public boolean isBrowsable() {
158         return mBrowsable;
159     }
160 
getCoverArtHandle()161     public String getCoverArtHandle() {
162         return mCoverArtHandle;
163     }
164 
getCoverArtUuid()165     public String getCoverArtUuid() {
166         return mImageUuid;
167     }
168 
setCoverArtUuid(String uuid)169     public void setCoverArtUuid(String uuid) {
170         mImageUuid = uuid;
171     }
172 
getCoverArtLocation()173     public synchronized Uri getCoverArtLocation() {
174         return mImageUri;
175     }
176 
setCoverArtLocation(Uri uri)177     public synchronized void setCoverArtLocation(Uri uri) {
178         mImageUri = uri;
179     }
180 
181     /**
182      * Convert this item an Android Media Framework MediaMetadata
183      */
toMediaMetadata()184     public MediaMetadataCompat toMediaMetadata() {
185         MediaMetadataCompat.Builder metaDataBuilder = new MediaMetadataCompat.Builder();
186         Uri coverArtUri = getCoverArtLocation();
187         String uriString = coverArtUri != null ? coverArtUri.toString() : null;
188         metaDataBuilder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mUuid);
189         metaDataBuilder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, mDisplayableName);
190         metaDataBuilder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, mTitle);
191         metaDataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, mArtistName);
192         metaDataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, mAlbumName);
193         metaDataBuilder.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, mTrackNumber);
194         metaDataBuilder.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, mTotalNumberOfTracks);
195         metaDataBuilder.putString(MediaMetadataCompat.METADATA_KEY_GENRE, mGenre);
196         metaDataBuilder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, mPlayingTime);
197         metaDataBuilder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, uriString);
198         metaDataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ART_URI, uriString);
199         metaDataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, uriString);
200         if (mItemType == TYPE_FOLDER) {
201             metaDataBuilder.putLong(MediaMetadataCompat.METADATA_KEY_BT_FOLDER_TYPE, mType);
202         }
203         return metaDataBuilder.build();
204     }
205 
206     /**
207      * Convert this item an Android Media Framework MediaItem
208      */
toMediaItem()209     public MediaItem toMediaItem() {
210         MediaDescriptionCompat.Builder descriptionBuilder = new MediaDescriptionCompat.Builder();
211 
212         descriptionBuilder.setMediaId(mUuid);
213 
214         String name = null;
215         if (mDisplayableName != null) {
216             name = mDisplayableName;
217         } else if (mTitle != null) {
218             name = mTitle;
219         }
220         descriptionBuilder.setTitle(name);
221 
222         descriptionBuilder.setIconUri(getCoverArtLocation());
223 
224         Bundle extras = new Bundle();
225         extras.putLong(AVRCP_ITEM_KEY_UID, mUid);
226         descriptionBuilder.setExtras(extras);
227 
228         int flags = 0x0;
229         if (mPlayable) flags |= MediaItem.FLAG_PLAYABLE;
230         if (mBrowsable) flags |= MediaItem.FLAG_BROWSABLE;
231 
232         return new MediaItem(descriptionBuilder.build(), flags);
233     }
234 
235     @Override
toString()236     public String toString() {
237         return "AvrcpItem{mUuid=" + mUuid + ", mUid=" + mUid + ", mItemType=" + mItemType
238                 + ", mType=" + mType + ", mDisplayableName=" + mDisplayableName
239                 + ", mTitle=" + mTitle + ", mPlayable=" + mPlayable + ", mBrowsable="
240                 + mBrowsable + ", mCoverArtHandle=" + getCoverArtHandle()
241                 + ", mImageUuid=" + mImageUuid + ", mImageUri" + mImageUri + "}";
242     }
243 
244     @Override
equals(Object o)245     public boolean equals(Object o) {
246         if (this == o) {
247             return true;
248         }
249 
250         if (!(o instanceof AvrcpItem)) {
251             return false;
252         }
253 
254         AvrcpItem other = ((AvrcpItem) o);
255         return Objects.equals(mUuid, other.getUuid())
256                 && Objects.equals(mDevice, other.getDevice())
257                 && Objects.equals(mUid, other.getUid())
258                 && Objects.equals(mItemType, other.getItemType())
259                 && Objects.equals(mType, other.getType())
260                 && Objects.equals(mTitle, other.getTitle())
261                 && Objects.equals(mDisplayableName, other.getDisplayableName())
262                 && Objects.equals(mArtistName, other.getArtistName())
263                 && Objects.equals(mAlbumName, other.getAlbumName())
264                 && Objects.equals(mTrackNumber, other.getTrackNumber())
265                 && Objects.equals(mTotalNumberOfTracks, other.getTotalNumberOfTracks())
266                 && Objects.equals(mGenre, other.getGenre())
267                 && Objects.equals(mPlayingTime, other.getPlayingTime())
268                 && Objects.equals(mCoverArtHandle, other.getCoverArtHandle())
269                 && Objects.equals(mPlayable, other.isPlayable())
270                 && Objects.equals(mBrowsable, other.isBrowsable())
271                 && Objects.equals(mImageUri, other.getCoverArtLocation());
272     }
273 
274     /**
275      * Builder for an AvrcpItem
276      */
277     public static class Builder {
278         private static final String TAG = "AvrcpItem.Builder";
279         private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
280 
281         // Attribute ID Values from AVRCP Specification
282         private static final int MEDIA_ATTRIBUTE_TITLE = 0x01;
283         private static final int MEDIA_ATTRIBUTE_ARTIST_NAME = 0x02;
284         private static final int MEDIA_ATTRIBUTE_ALBUM_NAME = 0x03;
285         private static final int MEDIA_ATTRIBUTE_TRACK_NUMBER = 0x04;
286         private static final int MEDIA_ATTRIBUTE_TOTAL_TRACK_NUMBER = 0x05;
287         private static final int MEDIA_ATTRIBUTE_GENRE = 0x06;
288         private static final int MEDIA_ATTRIBUTE_PLAYING_TIME = 0x07;
289         private static final int MEDIA_ATTRIBUTE_COVER_ART_HANDLE = 0x08;
290 
291         private AvrcpItem mAvrcpItem = new AvrcpItem();
292 
293         /**
294          * Initialize all relevant AvrcpItem internals from the AVRCP specification defined set of
295          * item attributes
296          *
297          * @param attrIds The array of AVRCP specification defined IDs in the order they match to
298          *                the value string attrMap
299          * @param attrMap The mapped values for each ID
300          * @return This object so you can continue building
301          */
fromAvrcpAttributeArray(int[] attrIds, String[] attrMap)302         public Builder fromAvrcpAttributeArray(int[] attrIds, String[] attrMap) {
303             int attributeCount = Math.max(attrIds.length, attrMap.length);
304             for (int i = 0; i < attributeCount; i++) {
305                 if (DBG) Log.d(TAG, attrIds[i] + " = " + attrMap[i]);
306                 switch (attrIds[i]) {
307                     case MEDIA_ATTRIBUTE_TITLE:
308                         mAvrcpItem.mTitle = attrMap[i];
309                         break;
310                     case MEDIA_ATTRIBUTE_ARTIST_NAME:
311                         mAvrcpItem.mArtistName = attrMap[i];
312                         break;
313                     case MEDIA_ATTRIBUTE_ALBUM_NAME:
314                         mAvrcpItem.mAlbumName = attrMap[i];
315                         break;
316                     case MEDIA_ATTRIBUTE_TRACK_NUMBER:
317                         try {
318                             mAvrcpItem.mTrackNumber = Long.valueOf(attrMap[i]);
319                         } catch (java.lang.NumberFormatException e) {
320                             // If Track Number doesn't parse, leave it unset
321                         }
322                         break;
323                     case MEDIA_ATTRIBUTE_TOTAL_TRACK_NUMBER:
324                         try {
325                             mAvrcpItem.mTotalNumberOfTracks = Long.valueOf(attrMap[i]);
326                         } catch (java.lang.NumberFormatException e) {
327                             // If Total Track Number doesn't parse, leave it unset
328                         }
329                         break;
330                     case MEDIA_ATTRIBUTE_GENRE:
331                         mAvrcpItem.mGenre = attrMap[i];
332                         break;
333                     case MEDIA_ATTRIBUTE_PLAYING_TIME:
334                         try {
335                             mAvrcpItem.mPlayingTime = Long.valueOf(attrMap[i]);
336                         } catch (java.lang.NumberFormatException e) {
337                             // If Playing Time doesn't parse, leave it unset
338                         }
339                         break;
340                     case MEDIA_ATTRIBUTE_COVER_ART_HANDLE:
341                         mAvrcpItem.mCoverArtHandle = attrMap[i];
342                         break;
343                 }
344             }
345             return this;
346         }
347 
348         /**
349          * Set the item type for the AvrcpItem you are building
350          *
351          * Type can be one of PLAYER, FOLDER, or MEDIA
352          *
353          * @param itemType The item type as an AvrcpItem.* type value
354          * @return This object, so you can continue building
355          */
setItemType(int itemType)356         public Builder setItemType(int itemType) {
357             mAvrcpItem.mItemType = itemType;
358             return this;
359         }
360 
361         /**
362          * Set the type for the AvrcpItem you are building
363          *
364          * This is the type of the PLAYER, FOLDER, or MEDIA item.
365          *
366          * @param type The type as one of the AvrcpItem.MEDIA_* or FOLDER_* types
367          * @return This object, so you can continue building
368          */
setType(int type)369         public Builder setType(int type) {
370             mAvrcpItem.mType = type;
371             return this;
372         }
373 
374         /**
375          * Set the device for the AvrcpItem you are building
376          *
377          * @param device The BluetoothDevice object that this item came from
378          * @return This object, so you can continue building
379          */
setDevice(BluetoothDevice device)380         public Builder setDevice(BluetoothDevice device) {
381             mAvrcpItem.mDevice = device;
382             return this;
383         }
384 
385         /**
386          * Note that the AvrcpItem you are building is playable
387          *
388          * @param playable True if playable, false otherwise
389          * @return This object, so you can continue building
390          */
setPlayable(boolean playable)391         public Builder setPlayable(boolean playable) {
392             mAvrcpItem.mPlayable = playable;
393             return this;
394         }
395 
396         /**
397          * Note that the AvrcpItem you are building is browsable
398          *
399          * @param browsable True if browsable, false otherwise
400          * @return This object, so you can continue building
401          */
setBrowsable(boolean browsable)402         public Builder setBrowsable(boolean browsable) {
403             mAvrcpItem.mBrowsable = browsable;
404             return this;
405         }
406 
407         /**
408          * Set the AVRCP defined UID assigned to the AvrcpItem you are building
409          *
410          * @param uid The UID given to this item by the remote device
411          * @return This object, so you can continue building
412          */
setUid(long uid)413         public Builder setUid(long uid) {
414             mAvrcpItem.mUid = uid;
415             return this;
416         }
417 
418         /**
419          * Set the UUID you wish to associate with the AvrcpItem you are building
420          *
421          * @param uuid A string UUID value
422          * @return This object, so you can continue building
423          */
setUuid(String uuid)424         public Builder setUuid(String uuid) {
425             mAvrcpItem.mUuid = uuid;
426             return this;
427         }
428 
429         /**
430          * Set the displayable name for the AvrcpItem you are building
431          *
432          * @param displayableName A string representing a friendly, displayable name
433          * @return This object, so you can continue building
434          */
setDisplayableName(String displayableName)435         public Builder setDisplayableName(String displayableName) {
436             mAvrcpItem.mDisplayableName = displayableName;
437             return this;
438         }
439 
440         /**
441          * Set the title for the AvrcpItem you are building
442          *
443          * @param title The title as a string
444          * @return This object, so you can continue building
445          */
setTitle(String title)446         public Builder setTitle(String title) {
447             mAvrcpItem.mTitle = title;
448             return this;
449         }
450 
451         /**
452          * Set the artist name for the AvrcpItem you are building
453          *
454          * @param artistName The artist name as a string
455          * @return This object, so you can continue building
456          */
setArtistName(String artistName)457         public Builder setArtistName(String artistName) {
458             mAvrcpItem.mArtistName = artistName;
459             return this;
460         }
461 
462         /**
463          * Set the album name for the AvrcpItem you are building
464          *
465          * @param albumName The album name as a string
466          * @return This object, so you can continue building
467          */
setAlbumName(String albumName)468         public Builder setAlbumName(String albumName) {
469             mAvrcpItem.mAlbumName = albumName;
470             return this;
471         }
472 
473         /**
474          * Set the track number for the AvrcpItem you are building
475          *
476          * @param trackNumber The track number
477          * @return This object, so you can continue building
478          */
setTrackNumber(long trackNumber)479         public Builder setTrackNumber(long trackNumber) {
480             mAvrcpItem.mTrackNumber = trackNumber;
481             return this;
482         }
483 
484         /**
485          * Set the total number of tracks on the playlist or album that this AvrcpItem is on
486          *
487          * @param totalNumberOfTracks The total number of tracks along side this item
488          * @return This object, so you can continue building
489          */
setTotalNumberOfTracks(long totalNumberOfTracks)490         public Builder setTotalNumberOfTracks(long totalNumberOfTracks) {
491             mAvrcpItem.mTotalNumberOfTracks = totalNumberOfTracks;
492             return this;
493         }
494 
495         /**
496          * Set the genre name for the AvrcpItem you are building
497          *
498          * @param genre The genre as a string
499          * @return This object, so you can continue building
500          */
setGenre(String genre)501         public Builder setGenre(String genre) {
502             mAvrcpItem.mGenre = genre;
503             return this;
504         }
505 
506         /**
507          * Set the total playing time for the AvrcpItem you are building
508          *
509          * @param playingTime The playing time in seconds
510          * @return This object, so you can continue building
511          */
setPlayingTime(long playingTime)512         public Builder setPlayingTime(long playingTime) {
513             mAvrcpItem.mPlayingTime = playingTime;
514             return this;
515         }
516 
517         /**
518          * Set the cover art handle for the AvrcpItem you are building
519          *
520          * @param coverArtHandle The cover art image handle provided by a remote device
521          * @return This object, so you can continue building
522          */
setCoverArtHandle(String coverArtHandle)523         public Builder setCoverArtHandle(String coverArtHandle) {
524             mAvrcpItem.mCoverArtHandle = coverArtHandle;
525             return this;
526         }
527 
528         /**
529          * Set the location of the downloaded cover art for the AvrcpItem you are building
530          *
531          * @param uri The URI where our storage has placed the image associated with this item
532          * @return This object, so you can continue building
533          */
setCoverArtLocation(Uri uri)534         public Builder setCoverArtLocation(Uri uri) {
535             mAvrcpItem.setCoverArtLocation(uri);
536             return this;
537         }
538 
539         /**
540          * Build the AvrcpItem
541          *
542          * @return An AvrcpItem object
543          */
build()544         public AvrcpItem build() {
545             return mAvrcpItem;
546         }
547     }
548 }
549