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 
parseImageHandle(String handle)235     private static String parseImageHandle(String handle) {
236         return AvrcpCoverArtManager.isValidImageHandle(handle) ? handle : null;
237     }
238 
239     @Override
toString()240     public String toString() {
241         return "AvrcpItem{mUuid=" + mUuid + ", mUid=" + mUid + ", mItemType=" + mItemType
242                 + ", mType=" + mType + ", mDisplayableName=" + mDisplayableName
243                 + ", mTitle=" + mTitle + " mPlayingTime=" + mPlayingTime + " mTrack="
244                 + mTrackNumber + "/" + mTotalNumberOfTracks + ", mPlayable=" + mPlayable
245                 + ", mBrowsable=" + mBrowsable + ", mCoverArtHandle=" + getCoverArtHandle()
246                 + ", mImageUuid=" + mImageUuid + ", mImageUri" + mImageUri + "}";
247     }
248 
249     @Override
equals(Object o)250     public boolean equals(Object o) {
251         if (this == o) {
252             return true;
253         }
254 
255         if (!(o instanceof AvrcpItem)) {
256             return false;
257         }
258 
259         AvrcpItem other = ((AvrcpItem) o);
260         return Objects.equals(mUuid, other.getUuid())
261                 && Objects.equals(mDevice, other.getDevice())
262                 && Objects.equals(mUid, other.getUid())
263                 && Objects.equals(mItemType, other.getItemType())
264                 && Objects.equals(mType, other.getType())
265                 && Objects.equals(mTitle, other.getTitle())
266                 && Objects.equals(mDisplayableName, other.getDisplayableName())
267                 && Objects.equals(mArtistName, other.getArtistName())
268                 && Objects.equals(mAlbumName, other.getAlbumName())
269                 && Objects.equals(mTrackNumber, other.getTrackNumber())
270                 && Objects.equals(mTotalNumberOfTracks, other.getTotalNumberOfTracks())
271                 && Objects.equals(mGenre, other.getGenre())
272                 && Objects.equals(mPlayingTime, other.getPlayingTime())
273                 && Objects.equals(mCoverArtHandle, other.getCoverArtHandle())
274                 && Objects.equals(mPlayable, other.isPlayable())
275                 && Objects.equals(mBrowsable, other.isBrowsable())
276                 && Objects.equals(mImageUri, other.getCoverArtLocation());
277     }
278 
279     /**
280      * Builder for an AvrcpItem
281      */
282     public static class Builder {
283         private static final String TAG = "AvrcpItem.Builder";
284         private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
285 
286         // Attribute ID Values from AVRCP Specification
287         private static final int MEDIA_ATTRIBUTE_TITLE = 0x01;
288         private static final int MEDIA_ATTRIBUTE_ARTIST_NAME = 0x02;
289         private static final int MEDIA_ATTRIBUTE_ALBUM_NAME = 0x03;
290         private static final int MEDIA_ATTRIBUTE_TRACK_NUMBER = 0x04;
291         private static final int MEDIA_ATTRIBUTE_TOTAL_TRACK_NUMBER = 0x05;
292         private static final int MEDIA_ATTRIBUTE_GENRE = 0x06;
293         private static final int MEDIA_ATTRIBUTE_PLAYING_TIME = 0x07;
294         private static final int MEDIA_ATTRIBUTE_COVER_ART_HANDLE = 0x08;
295 
296         private AvrcpItem mAvrcpItem = new AvrcpItem();
297 
298         /**
299          * Initialize all relevant AvrcpItem internals from the AVRCP specification defined set of
300          * item attributes
301          *
302          * @param attrIds The array of AVRCP specification defined IDs in the order they match to
303          *                the value string attrMap
304          * @param attrMap The mapped values for each ID
305          * @return This object so you can continue building
306          */
fromAvrcpAttributeArray(int[] attrIds, String[] attrMap)307         public Builder fromAvrcpAttributeArray(int[] attrIds, String[] attrMap) {
308             int attributeCount = Math.max(attrIds.length, attrMap.length);
309             for (int i = 0; i < attributeCount; i++) {
310                 if (DBG) Log.d(TAG, attrIds[i] + " = " + attrMap[i]);
311                 switch (attrIds[i]) {
312                     case MEDIA_ATTRIBUTE_TITLE:
313                         mAvrcpItem.mTitle = attrMap[i];
314                         break;
315                     case MEDIA_ATTRIBUTE_ARTIST_NAME:
316                         mAvrcpItem.mArtistName = attrMap[i];
317                         break;
318                     case MEDIA_ATTRIBUTE_ALBUM_NAME:
319                         mAvrcpItem.mAlbumName = attrMap[i];
320                         break;
321                     case MEDIA_ATTRIBUTE_TRACK_NUMBER:
322                         try {
323                             mAvrcpItem.mTrackNumber = Long.valueOf(attrMap[i]);
324                         } catch (java.lang.NumberFormatException e) {
325                             // If Track Number doesn't parse, leave it unset
326                         }
327                         break;
328                     case MEDIA_ATTRIBUTE_TOTAL_TRACK_NUMBER:
329                         try {
330                             mAvrcpItem.mTotalNumberOfTracks = Long.valueOf(attrMap[i]);
331                         } catch (java.lang.NumberFormatException e) {
332                             // If Total Track Number doesn't parse, leave it unset
333                         }
334                         break;
335                     case MEDIA_ATTRIBUTE_GENRE:
336                         mAvrcpItem.mGenre = attrMap[i];
337                         break;
338                     case MEDIA_ATTRIBUTE_PLAYING_TIME:
339                         try {
340                             mAvrcpItem.mPlayingTime = Long.valueOf(attrMap[i]);
341                         } catch (java.lang.NumberFormatException e) {
342                             // If Playing Time doesn't parse, leave it unset
343                         }
344                         break;
345                     case MEDIA_ATTRIBUTE_COVER_ART_HANDLE:
346                         mAvrcpItem.mCoverArtHandle = parseImageHandle(attrMap[i]);
347                         break;
348                 }
349             }
350             return this;
351         }
352 
353         /**
354          * Set the item type for the AvrcpItem you are building
355          *
356          * Type can be one of PLAYER, FOLDER, or MEDIA
357          *
358          * @param itemType The item type as an AvrcpItem.* type value
359          * @return This object, so you can continue building
360          */
setItemType(int itemType)361         public Builder setItemType(int itemType) {
362             mAvrcpItem.mItemType = itemType;
363             return this;
364         }
365 
366         /**
367          * Set the type for the AvrcpItem you are building
368          *
369          * This is the type of the PLAYER, FOLDER, or MEDIA item.
370          *
371          * @param type The type as one of the AvrcpItem.MEDIA_* or FOLDER_* types
372          * @return This object, so you can continue building
373          */
setType(int type)374         public Builder setType(int type) {
375             mAvrcpItem.mType = type;
376             return this;
377         }
378 
379         /**
380          * Set the device for the AvrcpItem you are building
381          *
382          * @param device The BluetoothDevice object that this item came from
383          * @return This object, so you can continue building
384          */
setDevice(BluetoothDevice device)385         public Builder setDevice(BluetoothDevice device) {
386             mAvrcpItem.mDevice = device;
387             return this;
388         }
389 
390         /**
391          * Note that the AvrcpItem you are building is playable
392          *
393          * @param playable True if playable, false otherwise
394          * @return This object, so you can continue building
395          */
setPlayable(boolean playable)396         public Builder setPlayable(boolean playable) {
397             mAvrcpItem.mPlayable = playable;
398             return this;
399         }
400 
401         /**
402          * Note that the AvrcpItem you are building is browsable
403          *
404          * @param browsable True if browsable, false otherwise
405          * @return This object, so you can continue building
406          */
setBrowsable(boolean browsable)407         public Builder setBrowsable(boolean browsable) {
408             mAvrcpItem.mBrowsable = browsable;
409             return this;
410         }
411 
412         /**
413          * Set the AVRCP defined UID assigned to the AvrcpItem you are building
414          *
415          * @param uid The UID given to this item by the remote device
416          * @return This object, so you can continue building
417          */
setUid(long uid)418         public Builder setUid(long uid) {
419             mAvrcpItem.mUid = uid;
420             return this;
421         }
422 
423         /**
424          * Set the UUID you wish to associate with the AvrcpItem you are building
425          *
426          * @param uuid A string UUID value
427          * @return This object, so you can continue building
428          */
setUuid(String uuid)429         public Builder setUuid(String uuid) {
430             mAvrcpItem.mUuid = uuid;
431             return this;
432         }
433 
434         /**
435          * Set the displayable name for the AvrcpItem you are building
436          *
437          * @param displayableName A string representing a friendly, displayable name
438          * @return This object, so you can continue building
439          */
setDisplayableName(String displayableName)440         public Builder setDisplayableName(String displayableName) {
441             mAvrcpItem.mDisplayableName = displayableName;
442             return this;
443         }
444 
445         /**
446          * Set the title for the AvrcpItem you are building
447          *
448          * @param title The title as a string
449          * @return This object, so you can continue building
450          */
setTitle(String title)451         public Builder setTitle(String title) {
452             mAvrcpItem.mTitle = title;
453             return this;
454         }
455 
456         /**
457          * Set the artist name for the AvrcpItem you are building
458          *
459          * @param artistName The artist name as a string
460          * @return This object, so you can continue building
461          */
setArtistName(String artistName)462         public Builder setArtistName(String artistName) {
463             mAvrcpItem.mArtistName = artistName;
464             return this;
465         }
466 
467         /**
468          * Set the album name for the AvrcpItem you are building
469          *
470          * @param albumName The album name as a string
471          * @return This object, so you can continue building
472          */
setAlbumName(String albumName)473         public Builder setAlbumName(String albumName) {
474             mAvrcpItem.mAlbumName = albumName;
475             return this;
476         }
477 
478         /**
479          * Set the track number for the AvrcpItem you are building
480          *
481          * @param trackNumber The track number
482          * @return This object, so you can continue building
483          */
setTrackNumber(long trackNumber)484         public Builder setTrackNumber(long trackNumber) {
485             mAvrcpItem.mTrackNumber = trackNumber;
486             return this;
487         }
488 
489         /**
490          * Set the total number of tracks on the playlist or album that this AvrcpItem is on
491          *
492          * @param totalNumberOfTracks The total number of tracks along side this item
493          * @return This object, so you can continue building
494          */
setTotalNumberOfTracks(long totalNumberOfTracks)495         public Builder setTotalNumberOfTracks(long totalNumberOfTracks) {
496             mAvrcpItem.mTotalNumberOfTracks = totalNumberOfTracks;
497             return this;
498         }
499 
500         /**
501          * Set the genre name for the AvrcpItem you are building
502          *
503          * @param genre The genre as a string
504          * @return This object, so you can continue building
505          */
setGenre(String genre)506         public Builder setGenre(String genre) {
507             mAvrcpItem.mGenre = genre;
508             return this;
509         }
510 
511         /**
512          * Set the total playing time for the AvrcpItem you are building
513          *
514          * @param playingTime The playing time in seconds
515          * @return This object, so you can continue building
516          */
setPlayingTime(long playingTime)517         public Builder setPlayingTime(long playingTime) {
518             mAvrcpItem.mPlayingTime = playingTime;
519             return this;
520         }
521 
522         /**
523          * Set the cover art handle for the AvrcpItem you are building.
524          *
525          * @param coverArtHandle The cover art image handle provided by a remote device
526          * @return This object, so you can continue building
527          */
setCoverArtHandle(String coverArtHandle)528         public Builder setCoverArtHandle(String coverArtHandle) {
529             mAvrcpItem.mCoverArtHandle = parseImageHandle(coverArtHandle);
530             return this;
531         }
532 
533         /**
534          * Set the location of the downloaded cover art for the AvrcpItem you are building
535          *
536          * @param uri The URI where our storage has placed the image associated with this item
537          * @return This object, so you can continue building
538          */
setCoverArtLocation(Uri uri)539         public Builder setCoverArtLocation(Uri uri) {
540             mAvrcpItem.setCoverArtLocation(uri);
541             return this;
542         }
543 
544         /**
545          * Build the AvrcpItem
546          *
547          * @return An AvrcpItem object
548          */
build()549         public AvrcpItem build() {
550             return mAvrcpItem;
551         }
552     }
553 }
554