blob: bda92eab9c8e92b09722872974b46310946f28e7 [file] [log] [blame]
Jack Hef02d3c62017-02-21 00:39:22 -05001/*
2 * Copyright (C) 2017 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
17package com.android.music.utils;
18
19import android.content.ContentResolver;
20import android.content.ContentUris;
21import android.content.ContentValues;
22import android.content.Context;
23import android.database.Cursor;
24import android.graphics.Bitmap;
25import android.graphics.BitmapFactory;
26import android.graphics.drawable.BitmapDrawable;
27import android.media.MediaActionSound;
28import android.media.MediaMetadata;
29import android.media.MediaMetadataRetriever;
30import android.net.Uri;
31import android.os.AsyncTask;
32import android.provider.MediaStore;
33import android.util.Log;
34import com.android.music.MediaPlaybackService;
35import com.android.music.MusicUtils;
36import com.android.music.R;
37
38import java.io.File;
39import java.util.*;
40import java.util.concurrent.ConcurrentHashMap;
41import java.util.concurrent.ConcurrentMap;
42
43/*
44A provider of music contents to the music application, it reads external storage for any music
45files, parse them and
46store them in this class for future use.
47 */
48public class MusicProvider {
49 private static final String TAG = "MusicProvider";
50
51 // Public constants
52 public static final String UNKOWN = "UNKNOWN";
53 // Uri source of this track
54 public static final String CUSTOM_METADATA_TRACK_SOURCE = "__SOURCE__";
55 // Sort key for this tack
56 public static final String CUSTOM_METADATA_SORT_KEY = "__SORT_KEY__";
57
58 // Content select criteria
59 private static final String MUSIC_SELECT_FILTER = MediaStore.Audio.Media.IS_MUSIC + " != 0";
60 private static final String MUSIC_SORT_ORDER = MediaStore.Audio.Media.TITLE + " ASC";
61
62 // Categorized caches for music track data:
63 private Context mContext;
64 // Album Name --> list of Metadata
65 private ConcurrentMap<String, List<MediaMetadata>> mMusicListByAlbum;
66 // Playlist Name --> list of Metadata
67 private ConcurrentMap<String, List<MediaMetadata>> mMusicListByPlaylist;
68 // Artist Name --> Map of (album name --> album metadata)
69 private ConcurrentMap<String, Map<String, MediaMetadata>> mArtistAlbumDb;
70 private List<MediaMetadata> mMusicList;
71 private final ConcurrentMap<Long, Song> mMusicListById;
72 private final ConcurrentMap<String, Song> mMusicListByMediaId;
73
74 enum State { NON_INITIALIZED, INITIALIZING, INITIALIZED }
75
76 private volatile State mCurrentState = State.NON_INITIALIZED;
77
78 public MusicProvider(Context context) {
79 mContext = context;
80 mArtistAlbumDb = new ConcurrentHashMap<>();
81 mMusicListByAlbum = new ConcurrentHashMap<>();
82 mMusicListByPlaylist = new ConcurrentHashMap<>();
83 mMusicListById = new ConcurrentHashMap<>();
84 mMusicList = new ArrayList<>();
85 mMusicListByMediaId = new ConcurrentHashMap<>();
86 mMusicListByPlaylist.put(MediaIDHelper.MEDIA_ID_NOW_PLAYING, new ArrayList<>());
87 }
88
89 public boolean isInitialized() {
90 return mCurrentState == State.INITIALIZED;
91 }
92
93 /**
94 * Get an iterator over the list of artists
95 *
96 * @return list of artists
97 */
98 public Iterable<String> getArtists() {
99 if (mCurrentState != State.INITIALIZED) {
100 return Collections.emptyList();
101 }
102 return mArtistAlbumDb.keySet();
103 }
104
105 /**
106 * Get an iterator over the list of albums
107 *
108 * @return list of albums
109 */
110 public Iterable<MediaMetadata> getAlbums() {
111 if (mCurrentState != State.INITIALIZED) {
112 return Collections.emptyList();
113 }
114 ArrayList<MediaMetadata> albumList = new ArrayList<>();
115 for (Map<String, MediaMetadata> artist_albums : mArtistAlbumDb.values()) {
116 albumList.addAll(artist_albums.values());
117 }
118 return albumList;
119 }
120
121 /**
122 * Get an iterator over the list of playlists
123 *
124 * @return list of playlists
125 */
126 public Iterable<String> getPlaylists() {
127 if (mCurrentState != State.INITIALIZED) {
128 return Collections.emptyList();
129 }
130 return mMusicListByPlaylist.keySet();
131 }
132
133 public Iterable<MediaMetadata> getMusicList() {
134 return mMusicList;
135 }
136
137 /**
138 * Get albums of a certain artist
139 *
140 */
141 public Iterable<MediaMetadata> getAlbumByArtist(String artist) {
142 if (mCurrentState != State.INITIALIZED || !mArtistAlbumDb.containsKey(artist)) {
143 return Collections.emptyList();
144 }
145 return mArtistAlbumDb.get(artist).values();
146 }
147
148 /**
149 * Get music tracks of the given album
150 *
151 */
152 public Iterable<MediaMetadata> getMusicsByAlbum(String album) {
153 if (mCurrentState != State.INITIALIZED || !mMusicListByAlbum.containsKey(album)) {
154 return Collections.emptyList();
155 }
156 return mMusicListByAlbum.get(album);
157 }
158
159 /**
160 * Get music tracks of the given playlist
161 *
162 */
163 public Iterable<MediaMetadata> getMusicsByPlaylist(String playlist) {
164 if (mCurrentState != State.INITIALIZED || !mMusicListByPlaylist.containsKey(playlist)) {
165 return Collections.emptyList();
166 }
167 return mMusicListByPlaylist.get(playlist);
168 }
169
170 /**
171 * Return the MediaMetadata for the given musicID.
172 *
173 * @param musicId The unique, non-hierarchical music ID.
174 */
175 public Song getMusicById(long musicId) {
176 return mMusicListById.containsKey(musicId) ? mMusicListById.get(musicId) : null;
177 }
178
179 /**
180 * Return the MediaMetadata for the given musicID.
181 *
182 * @param musicId The unique, non-hierarchical music ID.
183 */
184 public Song getMusicByMediaId(String musicId) {
185 return mMusicListByMediaId.containsKey(musicId) ? mMusicListByMediaId.get(musicId) : null;
186 }
187
188 /**
189 * Very basic implementation of a search that filter music tracks which title containing
190 * the given query.
191 *
192 */
193 public Iterable<MediaMetadata> searchMusic(String titleQuery) {
194 if (mCurrentState != State.INITIALIZED) {
195 return Collections.emptyList();
196 }
197 ArrayList<MediaMetadata> result = new ArrayList<>();
198 titleQuery = titleQuery.toLowerCase();
199 for (Song song : mMusicListByMediaId.values()) {
200 if (song.getMetadata()
201 .getString(MediaMetadata.METADATA_KEY_TITLE)
202 .toLowerCase()
203 .contains(titleQuery)) {
204 result.add(song.getMetadata());
205 }
206 }
207 return result;
208 }
209
210 public interface MusicProviderCallback { void onMusicCatalogReady(boolean success); }
211
212 /**
213 * Get the list of music tracks from disk and caches the track information
214 * for future reference, keying tracks by musicId and grouping by genre.
215 */
216 public void retrieveMediaAsync(final MusicProviderCallback callback) {
217 Log.d(TAG, "retrieveMediaAsync called");
218 if (mCurrentState == State.INITIALIZED) {
219 // Nothing to do, execute callback immediately
220 callback.onMusicCatalogReady(true);
221 return;
222 }
223
224 // Asynchronously load the music catalog in a separate thread
225 new AsyncTask<Void, Void, State>() {
226 @Override
227 protected State doInBackground(Void... params) {
228 mCurrentState = State.INITIALIZING;
229 if (retrieveMedia()) {
230 mCurrentState = State.INITIALIZED;
231 } else {
232 mCurrentState = State.NON_INITIALIZED;
233 }
234 return mCurrentState;
235 }
236
237 @Override
238 protected void onPostExecute(State current) {
239 if (callback != null) {
240 callback.onMusicCatalogReady(current == State.INITIALIZED);
241 }
242 }
243 }
244 .execute();
245 }
246
247 public synchronized boolean retrieveAllPlayLists() {
248 Cursor cursor = mContext.getContentResolver().query(
249 MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, null, null, null, null);
250 if (cursor == null) {
251 Log.e(TAG, "Failed to retreive playlist: cursor is null");
252 return false;
253 }
254 if (!cursor.moveToFirst()) {
255 Log.d(TAG, "Failed to move cursor to first row (no query result)");
256 cursor.close();
257 return true;
258 }
259 int idColumn = cursor.getColumnIndex(MediaStore.Audio.Playlists._ID);
260 int nameColumn = cursor.getColumnIndex(MediaStore.Audio.Playlists.NAME);
261 int pathColumn = cursor.getColumnIndex(MediaStore.Audio.Playlists.DATA);
262 do {
263 long thisId = cursor.getLong(idColumn);
264 String thisPath = cursor.getString(pathColumn);
265 String thisName = cursor.getString(nameColumn);
266 Log.i(TAG, "PlayList ID: " + thisId + " Name: " + thisName);
267 List<MediaMetadata> songList = retreivePlaylistMetadata(thisId, thisPath);
268 LogHelper.i(TAG, "Found ", songList.size(), " items for playlist name: ", thisName);
269 mMusicListByPlaylist.put(thisName, songList);
270 } while (cursor.moveToNext());
271 cursor.close();
272 return true;
273 }
274
275 public synchronized List<MediaMetadata> retreivePlaylistMetadata(
276 long playlistId, String playlistPath) {
277 Cursor cursor = mContext.getContentResolver().query(Uri.parse(playlistPath), null,
278 MediaStore.Audio.Playlists.Members.PLAYLIST_ID + " == " + playlistId, null, null);
279 if (cursor == null) {
280 Log.e(TAG, "Failed to retreive individual playlist: cursor is null");
281 return null;
282 }
283 if (!cursor.moveToFirst()) {
284 Log.d(TAG, "Failed to move cursor to first row (no query result for playlist)");
285 cursor.close();
286 return null;
287 }
288 List<Song> songList = new ArrayList<>();
289 int idColumn = cursor.getColumnIndex(MediaStore.Audio.Playlists.Members._ID);
290 int audioIdColumn = cursor.getColumnIndex(MediaStore.Audio.Playlists.Members.AUDIO_ID);
291 int orderColumn = cursor.getColumnIndex(MediaStore.Audio.Playlists.Members.PLAY_ORDER);
292 int audioPathColumn = cursor.getColumnIndex(MediaStore.Audio.Playlists.Members.DATA);
293 int audioNameColumn = cursor.getColumnIndex(MediaStore.Audio.Playlists.Members.TITLE);
294 do {
295 long thisId = cursor.getLong(idColumn);
296 long thisAudioId = cursor.getLong(audioIdColumn);
297 long thisOrder = cursor.getLong(orderColumn);
298 String thisAudioPath = cursor.getString(audioPathColumn);
299 Log.i(TAG,
300 "Playlist ID: " + playlistId + " Music ID: " + thisAudioId
301 + " Name: " + audioNameColumn);
302 if (!mMusicListById.containsKey(thisAudioId)) {
303 LogHelper.d(TAG, "Music does not exist");
304 continue;
305 }
306 Song song = mMusicListById.get(thisAudioId);
307 song.setSortKey(thisOrder);
308 songList.add(song);
309 } while (cursor.moveToNext());
310 cursor.close();
311 songList.sort(new Comparator<Song>() {
312 @Override
313 public int compare(Song s1, Song s2) {
314 long key1 = s1.getSortKey();
315 long key2 = s2.getSortKey();
316 if (key1 < key2) {
317 return -1;
318 } else if (key1 == key2) {
319 return 0;
320 } else {
321 return 1;
322 }
323 }
324 });
325 List<MediaMetadata> metadataList = new ArrayList<>();
326 for (Song song : songList) {
327 metadataList.add(song.getMetadata());
328 }
329 return metadataList;
330 }
331
332 private synchronized boolean retrieveMedia() {
333 Cursor cursor =
334 mContext.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
335 null, MUSIC_SELECT_FILTER, null, MUSIC_SORT_ORDER);
336 if (cursor == null) {
337 Log.e(TAG, "Failed to retreive music: cursor is null");
338 mCurrentState = State.NON_INITIALIZED;
339 return false;
340 }
341 if (!cursor.moveToFirst()) {
342 Log.d(TAG, "Failed to move cursor to first row (no query result)");
343 cursor.close();
344 return true;
345 }
346 int idColumn = cursor.getColumnIndex(MediaStore.Audio.Media._ID);
347 int titleColumn = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE);
348 int pathColumn = cursor.getColumnIndex(MediaStore.Audio.Media.DATA);
349 do {
350 Log.i(TAG,
351 "Music ID: " + cursor.getString(idColumn)
352 + " Title: " + cursor.getString(titleColumn));
353 long thisId = cursor.getLong(idColumn);
354 String thisPath = cursor.getString(pathColumn);
355 MediaMetadata metadata = retrievMediaMetadata(thisId, thisPath);
356 Log.i(TAG, "MediaMetadata: " + metadata);
357 if (metadata == null) {
358 continue;
359 }
360 Song thisSong = new Song(thisId, metadata, null);
361 // Construct per feature database
362 mMusicList.add(metadata);
363 mMusicListById.put(thisId, thisSong);
364 mMusicListByMediaId.put(String.valueOf(thisId), thisSong);
365 addMusicToAlbumList(metadata);
366 addMusicToArtistList(metadata);
367 } while (cursor.moveToNext());
368 cursor.close();
369 return true;
370 }
371
372 private synchronized MediaMetadata retrievMediaMetadata(long musicId, String musicPath) {
373 LogHelper.d(TAG, "getting metadata for music: ", musicPath);
374 MediaMetadataRetriever retriever = new MediaMetadataRetriever();
375 Uri contentUri = ContentUris.withAppendedId(
376 android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, musicId);
377 if (!(new File(musicPath).exists())) {
378 LogHelper.d(TAG, "Does not exist, deleting item");
379 mContext.getContentResolver().delete(contentUri, null, null);
380 return null;
381 }
382 retriever.setDataSource(mContext, contentUri);
383 String title = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
384 String album = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM);
385 String artist = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST);
386 String durationString =
387 retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
388 long duration = durationString != null ? Long.parseLong(durationString) : 0;
389 MediaMetadata.Builder metadataBuilder =
390 new MediaMetadata.Builder()
391 .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, String.valueOf(musicId))
392 .putString(CUSTOM_METADATA_TRACK_SOURCE, musicPath)
393 .putString(MediaMetadata.METADATA_KEY_TITLE, title != null ? title : UNKOWN)
394 .putString(MediaMetadata.METADATA_KEY_ALBUM, album != null ? album : UNKOWN)
395 .putString(
396 MediaMetadata.METADATA_KEY_ARTIST, artist != null ? artist : UNKOWN)
397 .putLong(MediaMetadata.METADATA_KEY_DURATION, duration);
398 byte[] albumArtData = retriever.getEmbeddedPicture();
399 Bitmap bitmap;
400 if (albumArtData != null) {
401 bitmap = BitmapFactory.decodeByteArray(albumArtData, 0, albumArtData.length);
402 bitmap = MusicUtils.resizeBitmap(bitmap, getDefaultAlbumArt());
403 metadataBuilder.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, bitmap);
404 }
405 retriever.release();
406 return metadataBuilder.build();
407 }
408
409 private Bitmap getDefaultAlbumArt() {
410 BitmapFactory.Options opts = new BitmapFactory.Options();
411 opts.inPreferredConfig = Bitmap.Config.ARGB_8888;
412 return BitmapFactory.decodeStream(
413 mContext.getResources().openRawResource(R.drawable.albumart_mp_unknown), null,
414 opts);
415 }
416
417 private void addMusicToAlbumList(MediaMetadata metadata) {
418 String thisAlbum = metadata.getString(MediaMetadata.METADATA_KEY_ALBUM);
419 if (thisAlbum == null) {
420 thisAlbum = UNKOWN;
421 }
422 if (!mMusicListByAlbum.containsKey(thisAlbum)) {
423 mMusicListByAlbum.put(thisAlbum, new ArrayList<>());
424 }
425 mMusicListByAlbum.get(thisAlbum).add(metadata);
426 }
427
428 private void addMusicToArtistList(MediaMetadata metadata) {
429 String thisArtist = metadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
430 if (thisArtist == null) {
431 thisArtist = UNKOWN;
432 }
433 String thisAlbum = metadata.getString(MediaMetadata.METADATA_KEY_ALBUM);
434 if (thisAlbum == null) {
435 thisAlbum = UNKOWN;
436 }
437 if (!mArtistAlbumDb.containsKey(thisArtist)) {
438 mArtistAlbumDb.put(thisArtist, new ConcurrentHashMap<>());
439 }
440 Map<String, MediaMetadata> albumsMap = mArtistAlbumDb.get(thisArtist);
441 MediaMetadata.Builder builder;
442 long count = 0;
443 Bitmap thisAlbumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
444 if (albumsMap.containsKey(thisAlbum)) {
445 MediaMetadata album_metadata = albumsMap.get(thisAlbum);
446 count = album_metadata.getLong(MediaMetadata.METADATA_KEY_NUM_TRACKS);
447 Bitmap nAlbumArt = album_metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
448 builder = new MediaMetadata.Builder(album_metadata);
449 if (nAlbumArt != null) {
450 thisAlbumArt = null;
451 }
452 } else {
453 builder = new MediaMetadata.Builder();
454 builder.putString(MediaMetadata.METADATA_KEY_ALBUM, thisAlbum)
455 .putString(MediaMetadata.METADATA_KEY_ARTIST, thisArtist);
456 }
457 if (thisAlbumArt != null) {
458 builder.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, thisAlbumArt);
459 }
460 builder.putLong(MediaMetadata.METADATA_KEY_NUM_TRACKS, count + 1);
461 albumsMap.put(thisAlbum, builder.build());
462 }
463
464 public synchronized void updateMusic(String musicId, MediaMetadata metadata) {
465 Song song = mMusicListByMediaId.get(musicId);
466 if (song == null) {
467 return;
468 }
469
470 String oldGenre = song.getMetadata().getString(MediaMetadata.METADATA_KEY_GENRE);
471 String newGenre = metadata.getString(MediaMetadata.METADATA_KEY_GENRE);
472
473 song.setMetadata(metadata);
474
475 // if genre has changed, we need to rebuild the list by genre
476 if (!oldGenre.equals(newGenre)) {
477 // buildListsByGenre();
478 }
479 }
480}