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