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