1 /* 2 * Copyright (C) 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.bluetooth.audio_util; 18 19 import android.content.Context; 20 import android.media.MediaDescription; 21 import android.media.MediaMetadata; 22 import android.media.browse.MediaBrowser.MediaItem; 23 import android.media.session.MediaSession; 24 import android.os.Bundle; 25 26 import com.android.bluetooth.R; 27 28 import java.util.Objects; 29 30 public class Metadata implements Cloneable { 31 public String mediaId; 32 public String title; 33 public String artist; 34 public String album; 35 public String trackNum; 36 public String numTracks; 37 public String genre; 38 public String duration; 39 public Image image; 40 41 // Media ID is an implementation detail and doesn't need to be localized 42 public static final String EMPTY_MEDIA_ID = "Not Provided"; 43 public static final String EMPTY_TITLE = "Not Provided"; 44 public static final String EMPTY_ARTIST = ""; 45 public static final String EMPTY_ALBUM = ""; 46 public static final String EMPTY_TRACK_NUM = "1"; 47 public static final String EMPTY_NUM_TRACKS = "1"; 48 public static final String EMPTY_GENRE = ""; 49 public static final String EMPTY_DURATION = "0"; 50 51 @Override clone()52 public Metadata clone() { 53 Metadata data = new Metadata(); 54 data.mediaId = mediaId; 55 data.title = title; 56 data.artist = artist; 57 data.album = album; 58 data.trackNum = trackNum; 59 data.numTracks = numTracks; 60 data.genre = genre; 61 data.duration = duration; 62 data.image = image; 63 return data; 64 } 65 66 @Override equals(Object o)67 public boolean equals(Object o) { 68 if (o == null) return false; 69 if (!(o instanceof Metadata)) return false; 70 71 final Metadata m = (Metadata) o; 72 if (!Objects.equals(title, m.title)) return false; 73 if (!Objects.equals(artist, m.artist)) return false; 74 if (!Objects.equals(album, m.album)) return false; 75 if (!Objects.equals(trackNum, m.trackNum)) return false; 76 if (!Objects.equals(numTracks, m.numTracks)) return false; 77 if (!Objects.equals(genre, m.genre)) return false; 78 if (!Objects.equals(duration, m.duration)) return false; 79 // Actual image comparisons have shown to be very expensive. Since it's rare that 80 // an application changes the cover artwork between multiple images once it's not 81 // null anymore, we just look for changes between "something" and "nothing". 82 if ((image == null && m.image != null) || (image != null && m.image == null)) { 83 return false; 84 } 85 return true; 86 } 87 88 @Override hashCode()89 public int hashCode() { 90 // Do not hash the Image as it does not implement hashCode 91 return Objects.hash(mediaId, title, artist, album, trackNum, numTracks, genre, duration); 92 } 93 94 @Override toString()95 public String toString() { 96 return "{ mediaId=\"" 97 + mediaId 98 + "\" title=\"" 99 + title 100 + "\" artist=\"" 101 + artist 102 + "\" album=\"" 103 + album 104 + "\" duration=" 105 + duration 106 + " trackPosition=" 107 + trackNum 108 + "/" 109 + numTracks 110 + " image=" 111 + image 112 + " }"; 113 } 114 115 /** Replaces default values by {@code filledMetadata} non default values. */ replaceDefaults(Metadata filledMetadata)116 public void replaceDefaults(Metadata filledMetadata) { 117 if (filledMetadata == null) { 118 return; 119 } 120 121 Metadata empty = Util.empty_data(); 122 123 if (empty.mediaId.equals(mediaId)) { 124 mediaId = filledMetadata.mediaId; 125 } 126 if (empty.title.equals(title)) { 127 title = filledMetadata.title; 128 } 129 if (empty.artist.equals(artist)) { 130 artist = filledMetadata.artist; 131 } 132 if (empty.album.equals(album)) { 133 album = filledMetadata.album; 134 } 135 if (empty.trackNum.equals(trackNum)) { 136 trackNum = filledMetadata.trackNum; 137 } 138 if (empty.numTracks.equals(numTracks)) { 139 numTracks = filledMetadata.numTracks; 140 } 141 if (empty.genre.equals(genre)) { 142 genre = filledMetadata.genre; 143 } 144 if (empty.duration.equals(duration)) { 145 duration = filledMetadata.duration; 146 } 147 if (image == null) { 148 image = filledMetadata.image; 149 } 150 } 151 152 /** A Builder object to populate a Metadata from various different Media Framework objects */ 153 public static class Builder { 154 private Metadata mMetadata = new Metadata(); 155 private Context mContext = null; 156 157 /** Set the Media ID fot the Metadata Object */ setMediaId(String id)158 public Builder setMediaId(String id) { 159 mMetadata.mediaId = id; 160 return this; 161 } 162 163 /** Set the context this builder should use when resolving images */ useContext(Context context)164 public Builder useContext(Context context) { 165 mContext = context; 166 return this; 167 } 168 169 /** Extract the fields from a MediaMetadata object into a Metadata, if they exist */ fromMediaMetadata(MediaMetadata data)170 public Builder fromMediaMetadata(MediaMetadata data) { 171 if (data == null) return this; 172 173 // First, use the basic description available with the MediaMetadata 174 fromMediaDescription(data.getDescription()); 175 176 // Then, replace with better data if available on the MediaMetadata 177 if (data.containsKey(MediaMetadata.METADATA_KEY_MEDIA_ID)) { 178 mMetadata.mediaId = data.getString(MediaMetadata.METADATA_KEY_MEDIA_ID); 179 } 180 if (data.containsKey(MediaMetadata.METADATA_KEY_TITLE)) { 181 mMetadata.title = data.getString(MediaMetadata.METADATA_KEY_TITLE); 182 } 183 if (data.containsKey(MediaMetadata.METADATA_KEY_ARTIST)) { 184 mMetadata.artist = data.getString(MediaMetadata.METADATA_KEY_ARTIST); 185 } 186 if (data.containsKey(MediaMetadata.METADATA_KEY_ALBUM)) { 187 mMetadata.album = data.getString(MediaMetadata.METADATA_KEY_ALBUM); 188 } 189 if (data.containsKey(MediaMetadata.METADATA_KEY_TRACK_NUMBER)) { 190 mMetadata.trackNum = "" + data.getLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER); 191 } 192 if (data.containsKey(MediaMetadata.METADATA_KEY_NUM_TRACKS)) { 193 mMetadata.numTracks = "" + data.getLong(MediaMetadata.METADATA_KEY_NUM_TRACKS); 194 } 195 if (data.containsKey(MediaMetadata.METADATA_KEY_GENRE)) { 196 mMetadata.genre = data.getString(MediaMetadata.METADATA_KEY_GENRE); 197 } 198 if (data.containsKey(MediaMetadata.METADATA_KEY_DURATION)) { 199 mMetadata.duration = "" + data.getLong(MediaMetadata.METADATA_KEY_DURATION); 200 } 201 if ((mContext != null 202 && Util.areUriImagesSupported(mContext) 203 && (data.containsKey(MediaMetadata.METADATA_KEY_ART_URI) 204 || data.containsKey(MediaMetadata.METADATA_KEY_ALBUM_ART_URI) 205 || data.containsKey( 206 MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI))) 207 || data.containsKey(MediaMetadata.METADATA_KEY_ART) 208 || data.containsKey(MediaMetadata.METADATA_KEY_ALBUM_ART) 209 || data.containsKey(MediaMetadata.METADATA_KEY_DISPLAY_ICON)) { 210 mMetadata.image = new Image(mContext, data); 211 } 212 return this; 213 } 214 215 /** Extract the fields from a MediaItem object into a Metadata, if they exist */ fromMediaItem(MediaItem item)216 public Builder fromMediaItem(MediaItem item) { 217 if (item == null) return this; 218 return fromMediaDescription(item.getDescription()).setMediaId(item.getMediaId()); 219 } 220 221 /** Extract the fields from a MediaDescription object into a Metadata, if they exist */ fromMediaDescription(MediaDescription desc)222 public Builder fromMediaDescription(MediaDescription desc) { 223 if (desc == null) return this; 224 225 // Default the following mapping if they exist 226 if (desc.getTitle() != null) mMetadata.title = desc.getTitle().toString(); 227 if (desc.getSubtitle() != null) mMetadata.artist = desc.getSubtitle().toString(); 228 if (desc.getDescription() != null) mMetadata.album = desc.getDescription().toString(); 229 230 // Check for artwork 231 if (desc.getIconBitmap() != null) { 232 mMetadata.image = new Image(mContext, desc.getIconBitmap()); 233 } else if (mContext != null 234 && Util.areUriImagesSupported(mContext) 235 && desc.getIconUri() != null) { 236 mMetadata.image = new Image(mContext, desc.getIconUri()); 237 } 238 239 // Then, check the extras in the description for even better data 240 return fromBundle(desc.getExtras()).setMediaId(desc.getMediaId()); 241 } 242 243 /** 244 * Extract the fields from a MediaSession.QueueItem object into a Metadata, if they exist 245 */ fromQueueItem(MediaSession.QueueItem item)246 public Builder fromQueueItem(MediaSession.QueueItem item) { 247 if (item == null) return this; 248 return fromMediaDescription(item.getDescription()); 249 } 250 251 /** 252 * Extract the fields from a Bundle of MediaMetadata constants into a Metadata, if they 253 * exist 254 */ fromBundle(Bundle bundle)255 public Builder fromBundle(Bundle bundle) { 256 if (bundle == null) return this; 257 if (bundle.containsKey(MediaMetadata.METADATA_KEY_MEDIA_ID)) { 258 mMetadata.mediaId = bundle.getString(MediaMetadata.METADATA_KEY_MEDIA_ID); 259 } 260 if (bundle.containsKey(MediaMetadata.METADATA_KEY_TITLE)) { 261 mMetadata.title = bundle.getString(MediaMetadata.METADATA_KEY_TITLE); 262 } 263 if (bundle.containsKey(MediaMetadata.METADATA_KEY_ARTIST)) { 264 mMetadata.artist = bundle.getString(MediaMetadata.METADATA_KEY_ARTIST); 265 } 266 if (bundle.containsKey(MediaMetadata.METADATA_KEY_ALBUM)) { 267 mMetadata.album = bundle.getString(MediaMetadata.METADATA_KEY_ALBUM); 268 } 269 if (bundle.containsKey(MediaMetadata.METADATA_KEY_TRACK_NUMBER)) { 270 mMetadata.trackNum = "" + bundle.getLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER); 271 } 272 if (bundle.containsKey(MediaMetadata.METADATA_KEY_NUM_TRACKS)) { 273 mMetadata.numTracks = "" + bundle.getLong(MediaMetadata.METADATA_KEY_NUM_TRACKS); 274 } 275 if (bundle.containsKey(MediaMetadata.METADATA_KEY_GENRE)) { 276 mMetadata.genre = bundle.getString(MediaMetadata.METADATA_KEY_GENRE); 277 } 278 if (bundle.containsKey(MediaMetadata.METADATA_KEY_DURATION)) { 279 mMetadata.duration = "" + bundle.getLong(MediaMetadata.METADATA_KEY_DURATION); 280 } 281 if ((mContext != null 282 && Util.areUriImagesSupported(mContext) 283 && (bundle.containsKey(MediaMetadata.METADATA_KEY_ART_URI) 284 || bundle.containsKey(MediaMetadata.METADATA_KEY_ALBUM_ART_URI) 285 || bundle.containsKey( 286 MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI))) 287 || bundle.containsKey(MediaMetadata.METADATA_KEY_ART) 288 || bundle.containsKey(MediaMetadata.METADATA_KEY_ALBUM_ART) 289 || bundle.containsKey(MediaMetadata.METADATA_KEY_DISPLAY_ICON)) { 290 mMetadata.image = new Image(mContext, bundle); 291 } 292 return this; 293 } 294 295 /** Elect to use default values in the Metadata in place of any missing values */ useDefaults()296 public Builder useDefaults() { 297 if (mMetadata.mediaId == null) { 298 mMetadata.mediaId = EMPTY_MEDIA_ID; 299 } 300 if (mMetadata.title == null) { 301 mMetadata.title = 302 mContext != null ? mContext.getString(R.string.not_provided) : EMPTY_TITLE; 303 } 304 if (mMetadata.artist == null) mMetadata.artist = EMPTY_ARTIST; 305 if (mMetadata.album == null) mMetadata.album = EMPTY_ALBUM; 306 if (mMetadata.trackNum == null) mMetadata.trackNum = EMPTY_TRACK_NUM; 307 if (mMetadata.numTracks == null) mMetadata.numTracks = EMPTY_NUM_TRACKS; 308 if (mMetadata.genre == null) mMetadata.genre = EMPTY_GENRE; 309 if (mMetadata.duration == null) mMetadata.duration = EMPTY_DURATION; 310 // The default value chosen for an image is null. Update here if we pick something else 311 return this; 312 } 313 314 /** Get the final Metadata objects you're building */ build()315 public Metadata build() { 316 return mMetadata.clone(); 317 } 318 } 319 } 320