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