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=\"" + mediaId + "\" title=\"" + title + "\" artist=\"" + artist 97 + "\" album=\"" + album + "\" duration=" + duration 98 + " trackPosition=" + trackNum + "/" + numTracks + " image=" + image + " }"; 99 } 100 101 /** 102 * Replaces default values by {@code filledMetadata} non default values. 103 */ replaceDefaults(Metadata filledMetadata)104 public void replaceDefaults(Metadata filledMetadata) { 105 if (filledMetadata == null) { 106 return; 107 } 108 109 Metadata empty = Util.empty_data(); 110 111 if (empty.mediaId.equals(mediaId)) { 112 mediaId = filledMetadata.mediaId; 113 } 114 if (empty.title.equals(title)) { 115 title = filledMetadata.title; 116 } 117 if (empty.artist.equals(artist)) { 118 artist = filledMetadata.artist; 119 } 120 if (empty.album.equals(album)) { 121 album = filledMetadata.album; 122 } 123 if (empty.trackNum.equals(trackNum)) { 124 trackNum = filledMetadata.trackNum; 125 } 126 if (empty.numTracks.equals(numTracks)) { 127 numTracks = filledMetadata.numTracks; 128 } 129 if (empty.genre.equals(genre)) { 130 genre = filledMetadata.genre; 131 } 132 if (empty.duration.equals(duration)) { 133 duration = filledMetadata.duration; 134 } 135 if (image == null) { 136 image = filledMetadata.image; 137 } 138 } 139 140 /** 141 * A Builder object to populate a Metadata from various different Media Framework objects 142 */ 143 public static class Builder { 144 private Metadata mMetadata = new Metadata(); 145 private Context mContext = null; 146 147 /** 148 * Set the Media ID fot the Metadata Object 149 */ setMediaId(String id)150 public Builder setMediaId(String id) { 151 mMetadata.mediaId = id; 152 return this; 153 } 154 155 /** 156 * Set the context this builder should use when resolving images 157 */ useContext(Context context)158 public Builder useContext(Context context) { 159 mContext = context; 160 return this; 161 } 162 163 /** 164 * Extract the fields from a MediaMetadata object into a Metadata, if they exist 165 */ fromMediaMetadata(MediaMetadata data)166 public Builder fromMediaMetadata(MediaMetadata data) { 167 if (data == null) return this; 168 169 // First, use the basic description available with the MediaMetadata 170 fromMediaDescription(data.getDescription()); 171 172 // Then, replace with better data if available on the MediaMetadata 173 if (data.containsKey(MediaMetadata.METADATA_KEY_MEDIA_ID)) { 174 mMetadata.mediaId = data.getString(MediaMetadata.METADATA_KEY_MEDIA_ID); 175 } 176 if (data.containsKey(MediaMetadata.METADATA_KEY_TITLE)) { 177 mMetadata.title = data.getString(MediaMetadata.METADATA_KEY_TITLE); 178 } 179 if (data.containsKey(MediaMetadata.METADATA_KEY_ARTIST)) { 180 mMetadata.artist = data.getString(MediaMetadata.METADATA_KEY_ARTIST); 181 } 182 if (data.containsKey(MediaMetadata.METADATA_KEY_ALBUM)) { 183 mMetadata.album = data.getString(MediaMetadata.METADATA_KEY_ALBUM); 184 } 185 if (data.containsKey(MediaMetadata.METADATA_KEY_TRACK_NUMBER)) { 186 mMetadata.trackNum = "" + data.getLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER); 187 } 188 if (data.containsKey(MediaMetadata.METADATA_KEY_NUM_TRACKS)) { 189 mMetadata.numTracks = "" + data.getLong(MediaMetadata.METADATA_KEY_NUM_TRACKS); 190 } 191 if (data.containsKey(MediaMetadata.METADATA_KEY_GENRE)) { 192 mMetadata.genre = data.getString(MediaMetadata.METADATA_KEY_GENRE); 193 } 194 if (data.containsKey(MediaMetadata.METADATA_KEY_DURATION)) { 195 mMetadata.duration = "" + data.getLong(MediaMetadata.METADATA_KEY_DURATION); 196 } 197 if ((mContext != null && Util.areUriImagesSupported(mContext) 198 && (data.containsKey(MediaMetadata.METADATA_KEY_ART_URI) 199 || data.containsKey(MediaMetadata.METADATA_KEY_ALBUM_ART_URI) 200 || data.containsKey(MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI))) 201 || data.containsKey(MediaMetadata.METADATA_KEY_ART) 202 || data.containsKey(MediaMetadata.METADATA_KEY_ALBUM_ART) 203 || data.containsKey(MediaMetadata.METADATA_KEY_DISPLAY_ICON)) { 204 mMetadata.image = new Image(mContext, data); 205 } 206 return this; 207 } 208 209 /** 210 * Extract the fields from a MediaItem object into a Metadata, if they exist 211 */ fromMediaItem(MediaItem item)212 public Builder fromMediaItem(MediaItem item) { 213 if (item == null) return this; 214 return fromMediaDescription(item.getDescription()).setMediaId(item.getMediaId()); 215 } 216 217 /** 218 * Extract the fields from a MediaDescription object into a Metadata, if they exist 219 */ fromMediaDescription(MediaDescription desc)220 public Builder fromMediaDescription(MediaDescription desc) { 221 if (desc == null) return this; 222 223 // Default the following mapping if they exist 224 if (desc.getTitle() != null) mMetadata.title = desc.getTitle().toString(); 225 if (desc.getSubtitle() != null) mMetadata.artist = desc.getSubtitle().toString(); 226 if (desc.getDescription() != null) mMetadata.album = desc.getDescription().toString(); 227 228 // Check for artwork 229 if (desc.getIconBitmap() != null) { 230 mMetadata.image = new Image(mContext, desc.getIconBitmap()); 231 } else if (mContext != null && Util.areUriImagesSupported(mContext) 232 && desc.getIconUri() != null) { 233 mMetadata.image = new Image(mContext, desc.getIconUri()); 234 } 235 236 // Then, check the extras in the description for even better data 237 return fromBundle(desc.getExtras()).setMediaId(desc.getMediaId()); 238 } 239 240 /** 241 * Extract the fields from a MediaSession.QueueItem object into a Metadata, if they exist 242 */ fromQueueItem(MediaSession.QueueItem item)243 public Builder fromQueueItem(MediaSession.QueueItem item) { 244 if (item == null) return this; 245 return fromMediaDescription(item.getDescription()); 246 } 247 248 /** 249 * Extract the fields from a Bundle of MediaMetadata constants into a Metadata, if they 250 * exist 251 */ fromBundle(Bundle bundle)252 public Builder fromBundle(Bundle bundle) { 253 if (bundle == null) return this; 254 if (bundle.containsKey(MediaMetadata.METADATA_KEY_MEDIA_ID)) { 255 mMetadata.mediaId = bundle.getString(MediaMetadata.METADATA_KEY_MEDIA_ID); 256 } 257 if (bundle.containsKey(MediaMetadata.METADATA_KEY_TITLE)) { 258 mMetadata.title = bundle.getString(MediaMetadata.METADATA_KEY_TITLE); 259 } 260 if (bundle.containsKey(MediaMetadata.METADATA_KEY_ARTIST)) { 261 mMetadata.artist = bundle.getString(MediaMetadata.METADATA_KEY_ARTIST); 262 } 263 if (bundle.containsKey(MediaMetadata.METADATA_KEY_ALBUM)) { 264 mMetadata.album = bundle.getString(MediaMetadata.METADATA_KEY_ALBUM); 265 } 266 if (bundle.containsKey(MediaMetadata.METADATA_KEY_TRACK_NUMBER)) { 267 mMetadata.trackNum = "" + bundle.getLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER); 268 } 269 if (bundle.containsKey(MediaMetadata.METADATA_KEY_NUM_TRACKS)) { 270 mMetadata.numTracks = "" + bundle.getLong(MediaMetadata.METADATA_KEY_NUM_TRACKS); 271 } 272 if (bundle.containsKey(MediaMetadata.METADATA_KEY_GENRE)) { 273 mMetadata.genre = bundle.getString(MediaMetadata.METADATA_KEY_GENRE); 274 } 275 if (bundle.containsKey(MediaMetadata.METADATA_KEY_DURATION)) { 276 mMetadata.duration = "" + bundle.getLong(MediaMetadata.METADATA_KEY_DURATION); 277 } 278 if ((mContext != null && Util.areUriImagesSupported(mContext) 279 && (bundle.containsKey(MediaMetadata.METADATA_KEY_ART_URI) 280 || bundle.containsKey(MediaMetadata.METADATA_KEY_ALBUM_ART_URI) 281 || bundle.containsKey(MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI))) 282 || bundle.containsKey(MediaMetadata.METADATA_KEY_ART) 283 || bundle.containsKey(MediaMetadata.METADATA_KEY_ALBUM_ART) 284 || bundle.containsKey(MediaMetadata.METADATA_KEY_DISPLAY_ICON)) { 285 mMetadata.image = new Image(mContext, bundle); 286 } 287 return this; 288 } 289 290 /** 291 * Elect to use default values in the Metadata in place of any missing values 292 */ useDefaults()293 public Builder useDefaults() { 294 if (mMetadata.mediaId == null) { 295 mMetadata.mediaId = EMPTY_MEDIA_ID; 296 } 297 if (mMetadata.title == null) { 298 mMetadata.title = 299 mContext != null ? mContext.getString(R.string.not_provided) 300 : EMPTY_TITLE; 301 } 302 if (mMetadata.artist == null) mMetadata.artist = EMPTY_ARTIST; 303 if (mMetadata.album == null) mMetadata.album = EMPTY_ALBUM; 304 if (mMetadata.trackNum == null) mMetadata.trackNum = EMPTY_TRACK_NUM; 305 if (mMetadata.numTracks == null) mMetadata.numTracks = EMPTY_NUM_TRACKS; 306 if (mMetadata.genre == null) mMetadata.genre = EMPTY_GENRE; 307 if (mMetadata.duration == null) mMetadata.duration = EMPTY_DURATION; 308 // The default value chosen for an image is null. Update here if we pick something else 309 return this; 310 } 311 312 /** 313 * Get the final Metadata objects you're building 314 */ build()315 public Metadata build() { 316 return mMetadata.clone(); 317 } 318 } 319 } 320