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