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