1 /* 2 * Copyright 2018 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.car.media.common; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.content.Context; 22 import android.content.res.Resources; 23 import android.content.res.TypedArray; 24 import android.graphics.Bitmap; 25 import android.graphics.Color; 26 import android.graphics.drawable.BitmapDrawable; 27 import android.graphics.drawable.Drawable; 28 import android.net.Uri; 29 import android.os.Bundle; 30 import android.os.Parcel; 31 import android.os.Parcelable; 32 import android.support.v4.media.MediaBrowserCompat; 33 import android.support.v4.media.MediaDescriptionCompat; 34 import android.support.v4.media.MediaMetadataCompat; 35 import android.support.v4.media.session.MediaSessionCompat; 36 import android.text.TextUtils; 37 38 import androidx.annotation.VisibleForTesting; 39 40 import com.android.car.apps.common.BitmapUtils; 41 import com.android.car.apps.common.CommonFlags; 42 import com.android.car.apps.common.imaging.ImageBinder; 43 import com.android.car.apps.common.imaging.ImageBinder.PlaceholderType; 44 45 import java.util.ArrayList; 46 import java.util.HashMap; 47 import java.util.List; 48 import java.util.Map; 49 import java.util.Objects; 50 51 /** 52 * Abstract representation of a media item metadata. 53 * 54 * For media art, only local uris are supported so downloads can be attributed to the media app. 55 * Bitmaps are not supported because they slow down the binder. 56 */ 57 public class MediaItemMetadata implements Parcelable { 58 private static final String TAG = "MediaItemMetadata"; 59 60 static final int INVALID_MEDIA_ART_TINT_COLOR = Color.argb(200, 255, 0, 0); 61 62 @NonNull 63 private final MediaDescriptionCompat mMediaDescription; 64 @Nullable 65 private final Long mQueueId; 66 private final boolean mIsBrowsable; 67 private final boolean mIsPlayable; 68 private final String mAlbumTitle; 69 private final String mArtist; 70 private final ArtworkRef mArtworkKey = new ArtworkRef(); 71 72 73 /** Creates an instance based on a {@link MediaMetadataCompat} */ MediaItemMetadata(@onNull MediaMetadataCompat metadata)74 public MediaItemMetadata(@NonNull MediaMetadataCompat metadata) { 75 this(metadata.getDescription(), null, false, false, 76 metadata.getString(MediaMetadataCompat.METADATA_KEY_ALBUM), 77 metadata.getString(MediaMetadataCompat.METADATA_KEY_ARTIST)); 78 } 79 80 /** Creates an instance based on a {@link MediaSessionCompat.QueueItem} */ MediaItemMetadata(@onNull MediaSessionCompat.QueueItem queueItem)81 public MediaItemMetadata(@NonNull MediaSessionCompat.QueueItem queueItem) { 82 this(queueItem.getDescription(), queueItem.getQueueId(), false, true, null, null); 83 } 84 85 /** Creates an instance based on a {@link MediaBrowserCompat.MediaItem} */ MediaItemMetadata(@onNull MediaBrowserCompat.MediaItem item)86 public MediaItemMetadata(@NonNull MediaBrowserCompat.MediaItem item) { 87 this(item.getDescription(), null, item.isBrowsable(), item.isPlayable(), null, null); 88 } 89 90 /** Creates an instance based on a {@link Parcel} */ MediaItemMetadata(@onNull Parcel in)91 public MediaItemMetadata(@NonNull Parcel in) { 92 mMediaDescription = (MediaDescriptionCompat) in.readValue( 93 MediaDescriptionCompat.class.getClassLoader()); 94 mQueueId = in.readByte() == 0x00 ? null : in.readLong(); 95 mIsBrowsable = in.readByte() != 0x00; 96 mIsPlayable = in.readByte() != 0x00; 97 mAlbumTitle = in.readString(); 98 mArtist = in.readString(); 99 } 100 101 @VisibleForTesting MediaItemMetadata(MediaDescriptionCompat description, Long queueId, boolean isBrowsable, boolean isPlayable, String albumTitle, String artist)102 public MediaItemMetadata(MediaDescriptionCompat description, Long queueId, boolean isBrowsable, 103 boolean isPlayable, String albumTitle, String artist) { 104 mMediaDescription = description; 105 mQueueId = queueId; 106 mIsPlayable = isPlayable; 107 mIsBrowsable = isBrowsable; 108 mAlbumTitle = albumTitle; 109 mArtist = artist; 110 } 111 112 /** 113 * The key to access the image to display for this media item. 114 * Implemented as a class so that later we can support showing different images for the same 115 * item (eg: cover and author) by adding other keys. 116 */ 117 public class ArtworkRef implements ImageBinder.ImageRef { 118 getBitmapToFlag(Context context)119 private @Nullable Bitmap getBitmapToFlag(Context context) { 120 CommonFlags flags = CommonFlags.getInstance(context); 121 return (flags.shouldFlagImproperImageRefs() && (mMediaDescription != null)) 122 ? mMediaDescription.getIconBitmap() : null; 123 } 124 getPlaceholderHash()125 private int getPlaceholderHash() { 126 // Only the title is reliably populated in metadata, since the album/artist fields 127 // aren't set in the items retrieved from the browse service (only Title/Subtitle). 128 return (getTitle() != null) ? getTitle().hashCode() : 0; 129 } 130 131 @Override toString()132 public String toString() { 133 return "title: " + getTitle() + " uri: " + getNonEmptyArtworkUri(); 134 } 135 136 @Override getImageURI()137 public @Nullable Uri getImageURI() { 138 return getNonEmptyArtworkUri(); 139 } 140 141 @Override equals(Context context, Object o)142 public boolean equals(Context context, Object o) { 143 if (this == o) return true; 144 if (o == null || getClass() != o.getClass()) return false; 145 ArtworkRef other = (ArtworkRef) o; 146 147 Bitmap myBitmap = getBitmapToFlag(context); 148 Bitmap otherBitmap = other.getBitmapToFlag(context); 149 if ((myBitmap != null) || (otherBitmap != null)) { 150 return Objects.equals(myBitmap, otherBitmap); 151 } 152 153 Uri myUri = getImageURI(); 154 Uri otherUri = other.getImageURI(); 155 if ((myUri != null) || (otherUri != null)) { 156 return Objects.equals(myUri, otherUri); 157 } 158 159 return getPlaceholderHash() == other.getPlaceholderHash(); 160 } 161 162 @Override getImage(Context context)163 public @Nullable Drawable getImage(Context context) { 164 Bitmap bitmap = getBitmapToFlag(context); 165 if (bitmap != null) { 166 Resources res = context.getResources(); 167 return new BitmapDrawable(res, BitmapUtils.createTintedBitmap(bitmap, 168 context.getColor( 169 com.android.car.apps.common.R.color.improper_image_refs_tint_color 170 ))); 171 } 172 return null; 173 } 174 175 @Override getPlaceholder(Context context, @NonNull PlaceholderType type)176 public Drawable getPlaceholder(Context context, @NonNull PlaceholderType type) { 177 if (type == PlaceholderType.NONE) return null; 178 179 List<Drawable> placeholders = getPlaceHolders(type, context); 180 int random = Math.floorMod(getPlaceholderHash(), placeholders.size()); 181 return placeholders.get(random); 182 } 183 } 184 185 /** @return media item id */ 186 @Nullable getId()187 public String getId() { 188 return mMediaDescription.getMediaId(); 189 } 190 191 /** @return media item title */ 192 @Nullable getTitle()193 public CharSequence getTitle() { 194 return mMediaDescription.getTitle(); 195 } 196 197 /** @return media item subtitle */ 198 @Nullable getSubtitle()199 public CharSequence getSubtitle() { 200 return mMediaDescription.getSubtitle(); 201 } 202 203 /** @return the album title for the media */ 204 @Nullable getAlbumTitle()205 public String getAlbumTitle() { 206 return mAlbumTitle; 207 } 208 209 /** @return the artist of the media */ 210 @Nullable getArtist()211 public CharSequence getArtist() { 212 return mArtist; 213 } 214 215 /** 216 * @return the id of this item in the session queue, or NULL if this is not a session queue 217 * item. 218 */ 219 @Nullable getQueueId()220 public Long getQueueId() { 221 return mQueueId; 222 } 223 224 getArtworkKey()225 public ArtworkRef getArtworkKey() { 226 return mArtworkKey; 227 } 228 229 /** 230 * @return a {@link Uri} referencing the artwork's bitmap. 231 */ getNonEmptyArtworkUri()232 private @Nullable Uri getNonEmptyArtworkUri() { 233 Uri uri = mMediaDescription.getIconUri(); 234 return (uri != null && !TextUtils.isEmpty(uri.toString())) ? uri : null; 235 } 236 237 /** 238 * @return optional extras that can include extra information about the media item to be played. 239 */ getExtras()240 public Bundle getExtras() { 241 return mMediaDescription.getExtras(); 242 } 243 244 /** 245 * @return boolean that indicate if media is explicit. 246 */ isExplicit()247 public boolean isExplicit() { 248 Bundle extras = mMediaDescription.getExtras(); 249 return extras != null && extras.getLong(MediaConstants.EXTRA_IS_EXPLICIT) 250 == MediaConstants.EXTRA_METADATA_ENABLED_VALUE; 251 } 252 253 /** 254 * @return boolean that indicate if media is downloaded. 255 */ isDownloaded()256 public boolean isDownloaded() { 257 Bundle extras = mMediaDescription.getExtras(); 258 return extras != null && extras.getLong(MediaConstants.EXTRA_DOWNLOAD_STATUS) 259 == MediaDescriptionCompat.STATUS_DOWNLOADED; 260 } 261 262 private static Map<PlaceholderType, List<Drawable>> sPlaceHolders = new HashMap<>(); 263 getPlaceHolders(PlaceholderType type, Context context)264 private static List<Drawable> getPlaceHolders(PlaceholderType type, Context context) { 265 List<Drawable> placeHolders = sPlaceHolders.get(type); 266 if (placeHolders == null) { 267 TypedArray placeholderImages = context.getResources().obtainTypedArray( 268 type == PlaceholderType.FOREGROUND 269 ? R.array.placeholder_images : R.array.placeholder_backgrounds); 270 271 if (placeholderImages == null) { 272 throw new NullPointerException("No placeholders for " + type); 273 } 274 275 placeHolders = new ArrayList<>(placeholderImages.length()); 276 for (int i = 0; i < placeholderImages.length(); i++) { 277 placeHolders.add(placeholderImages.getDrawable(i)); 278 } 279 placeholderImages.recycle(); 280 sPlaceHolders.put(type, placeHolders); 281 282 if (sPlaceHolders.size() <= 0) { 283 throw new Resources.NotFoundException("Placeholders should not be empty " + type); 284 } 285 } 286 return placeHolders; 287 } 288 isBrowsable()289 public boolean isBrowsable() { 290 return mIsBrowsable; 291 } 292 293 /** 294 * @return Content style hint for browsable items, if provided as an extra, or 295 * 0 as default value if not provided. 296 */ getBrowsableContentStyleHint()297 public int getBrowsableContentStyleHint() { 298 Bundle extras = mMediaDescription.getExtras(); 299 if (extras != null) { 300 if (extras.containsKey(MediaConstants.CONTENT_STYLE_BROWSABLE_HINT)) { 301 return extras.getInt(MediaConstants.CONTENT_STYLE_BROWSABLE_HINT, 0); 302 } else if (extras.containsKey(MediaConstants.CONTENT_STYLE_BROWSABLE_HINT_PRERELEASE)) { 303 return extras.getInt(MediaConstants.CONTENT_STYLE_BROWSABLE_HINT_PRERELEASE, 0); 304 } 305 } 306 return 0; 307 } 308 isPlayable()309 public boolean isPlayable() { 310 return mIsPlayable; 311 } 312 313 /** 314 * @return Content style hint for playable items, if provided as an extra, or 315 * 0 as default value if not provided. 316 */ getPlayableContentStyleHint()317 public int getPlayableContentStyleHint() { 318 Bundle extras = mMediaDescription.getExtras(); 319 if (extras != null) { 320 321 if (extras.containsKey(MediaConstants.CONTENT_STYLE_PLAYABLE_HINT)) { 322 return extras.getInt(MediaConstants.CONTENT_STYLE_PLAYABLE_HINT, 0); 323 } else if (extras.containsKey(MediaConstants.CONTENT_STYLE_PLAYABLE_HINT_PRERELEASE)) { 324 return extras.getInt(MediaConstants.CONTENT_STYLE_PLAYABLE_HINT_PRERELEASE, 0); 325 } 326 } 327 return 0; 328 } 329 330 /** 331 * @return Content style title group this item belongs to, or null if not provided 332 */ getTitleGrouping()333 public String getTitleGrouping() { 334 Bundle extras = mMediaDescription.getExtras(); 335 if (extras != null) { 336 if (extras.containsKey(MediaConstants.CONTENT_STYLE_GROUP_TITLE_HINT)) { 337 return extras.getString(MediaConstants.CONTENT_STYLE_GROUP_TITLE_HINT, null); 338 } else if (extras.containsKey( 339 MediaConstants.CONTENT_STYLE_GROUP_TITLE_HINT_PRERELEASE)) { 340 return extras.getString(MediaConstants.CONTENT_STYLE_GROUP_TITLE_HINT_PRERELEASE, 341 null); 342 } 343 } 344 return null; 345 } 346 347 @Override equals(Object o)348 public boolean equals(Object o) { 349 if (this == o) return true; 350 if (o == null || getClass() != o.getClass()) return false; 351 MediaItemMetadata that = (MediaItemMetadata) o; 352 return mIsBrowsable == that.mIsBrowsable 353 && mIsPlayable == that.mIsPlayable 354 && Objects.equals(getId(), that.getId()) 355 && Objects.equals(getTitle(), that.getTitle()) 356 && Objects.equals(getSubtitle(), that.getSubtitle()) 357 && Objects.equals(getAlbumTitle(), that.getAlbumTitle()) 358 && Objects.equals(getArtist(), that.getArtist()) 359 && Objects.equals(getNonEmptyArtworkUri(), that.getNonEmptyArtworkUri()) 360 && Objects.equals(mQueueId, that.mQueueId); 361 } 362 363 @Override hashCode()364 public int hashCode() { 365 return Objects.hash(mMediaDescription.getMediaId(), mQueueId, mIsBrowsable, mIsPlayable); 366 } 367 368 @Override describeContents()369 public int describeContents() { 370 return 0; 371 } 372 373 @Override writeToParcel(Parcel dest, int flags)374 public void writeToParcel(Parcel dest, int flags) { 375 dest.writeValue(mMediaDescription); 376 if (mQueueId == null) { 377 dest.writeByte((byte) (0x00)); 378 } else { 379 dest.writeByte((byte) (0x01)); 380 dest.writeLong(mQueueId); 381 } 382 dest.writeByte((byte) (mIsBrowsable ? 0x01 : 0x00)); 383 dest.writeByte((byte) (mIsPlayable ? 0x01 : 0x00)); 384 dest.writeString(mAlbumTitle); 385 dest.writeString(mArtist); 386 } 387 388 @SuppressWarnings("unused") 389 public static final Parcelable.Creator<MediaItemMetadata> CREATOR = 390 new Parcelable.Creator<MediaItemMetadata>() { 391 @Override 392 public MediaItemMetadata createFromParcel(Parcel in) { 393 return new MediaItemMetadata(in); 394 } 395 396 @Override 397 public MediaItemMetadata[] newArray(int size) { 398 return new MediaItemMetadata[size]; 399 } 400 }; 401 402 @Override toString()403 public String toString() { 404 return "[Id: " 405 + (mMediaDescription != null ? mMediaDescription.getMediaId() : "-") 406 + ", Queue Id: " 407 + (mQueueId != null ? mQueueId : "-") 408 + ", title: " 409 + mMediaDescription != null ? mMediaDescription.getTitle().toString() : "-" 410 + ", subtitle: " 411 + mMediaDescription != null ? mMediaDescription.getSubtitle().toString() : "-" 412 + ", album title: " 413 + mAlbumTitle != null ? mAlbumTitle : "-" 414 + ", artist: " 415 + mArtist != null ? mArtist : "-" 416 + ", album art URI: " 417 + (mMediaDescription != null ? mMediaDescription.getIconUri() : "-") 418 + "]"; 419 } 420 } 421