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