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