blob: 2796671d39d4fac8c650d69eb360f995ada64026 [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) {
231 mCurrentState = State.INITIALIZING;
232 if (retrieveMedia()) {
233 mCurrentState = State.INITIALIZED;
234 } else {
235 mCurrentState = State.NON_INITIALIZED;
236 }
237 return mCurrentState;
238 }
239
240 @Override
241 protected void onPostExecute(State current) {
242 if (callback != null) {
243 callback.onMusicCatalogReady(current == State.INITIALIZED);
244 }
245 }
246 }
247 .execute();
248 }
249
250 public synchronized boolean retrieveAllPlayLists() {
251 Cursor cursor = mContext.getContentResolver().query(
252 MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, null, null, null, null);
253 if (cursor == null) {
254 Log.e(TAG, "Failed to retreive playlist: cursor is null");
255 return false;
256 }
257 if (!cursor.moveToFirst()) {
258 Log.d(TAG, "Failed to move cursor to first row (no query result)");
259 cursor.close();
260 return true;
261 }
262 int idColumn = cursor.getColumnIndex(MediaStore.Audio.Playlists._ID);
263 int nameColumn = cursor.getColumnIndex(MediaStore.Audio.Playlists.NAME);
264 int pathColumn = cursor.getColumnIndex(MediaStore.Audio.Playlists.DATA);
265 do {
266 long thisId = cursor.getLong(idColumn);
267 String thisPath = cursor.getString(pathColumn);
268 String thisName = cursor.getString(nameColumn);
269 Log.i(TAG, "PlayList ID: " + thisId + " Name: " + thisName);
270 List<MediaMetadata> songList = retreivePlaylistMetadata(thisId, thisPath);
271 LogHelper.i(TAG, "Found ", songList.size(), " items for playlist name: ", thisName);
272 mMusicListByPlaylist.put(thisName, songList);
273 } while (cursor.moveToNext());
274 cursor.close();
275 return true;
276 }
277
278 public synchronized List<MediaMetadata> retreivePlaylistMetadata(
279 long playlistId, String playlistPath) {
280 Cursor cursor = mContext.getContentResolver().query(Uri.parse(playlistPath), null,
281 MediaStore.Audio.Playlists.Members.PLAYLIST_ID + " == " + playlistId, null, null);
282 if (cursor == null) {
283 Log.e(TAG, "Failed to retreive individual playlist: cursor is null");
284 return null;
285 }
286 if (!cursor.moveToFirst()) {
287 Log.d(TAG, "Failed to move cursor to first row (no query result for playlist)");
288 cursor.close();
289 return null;
290 }
291 List<Song> songList = new ArrayList<>();
292 int idColumn = cursor.getColumnIndex(MediaStore.Audio.Playlists.Members._ID);
293 int audioIdColumn = cursor.getColumnIndex(MediaStore.Audio.Playlists.Members.AUDIO_ID);
294 int orderColumn = cursor.getColumnIndex(MediaStore.Audio.Playlists.Members.PLAY_ORDER);
295 int audioPathColumn = cursor.getColumnIndex(MediaStore.Audio.Playlists.Members.DATA);
296 int audioNameColumn = cursor.getColumnIndex(MediaStore.Audio.Playlists.Members.TITLE);
297 do {
298 long thisId = cursor.getLong(idColumn);
299 long thisAudioId = cursor.getLong(audioIdColumn);
300 long thisOrder = cursor.getLong(orderColumn);
301 String thisAudioPath = cursor.getString(audioPathColumn);
302 Log.i(TAG,
303 "Playlist ID: " + playlistId + " Music ID: " + thisAudioId
304 + " Name: " + audioNameColumn);
305 if (!mMusicListById.containsKey(thisAudioId)) {
306 LogHelper.d(TAG, "Music does not exist");
307 continue;
308 }
309 Song song = mMusicListById.get(thisAudioId);
310 song.setSortKey(thisOrder);
311 songList.add(song);
312 } while (cursor.moveToNext());
313 cursor.close();
314 songList.sort(new Comparator<Song>() {
315 @Override
316 public int compare(Song s1, Song s2) {
317 long key1 = s1.getSortKey();
318 long key2 = s2.getSortKey();
319 if (key1 < key2) {
320 return -1;
321 } else if (key1 == key2) {
322 return 0;
323 } else {
324 return 1;
325 }
326 }
327 });
328 List<MediaMetadata> metadataList = new ArrayList<>();
329 for (Song song : songList) {
330 metadataList.add(song.getMetadata());
331 }
332 return metadataList;
333 }
334
335 private synchronized boolean retrieveMedia() {
Ajay Panicker96716ce2018-04-13 12:37:28 -0700336 if (mContext.checkSelfPermission(READ_EXTERNAL_STORAGE)
337 != PackageManager.PERMISSION_GRANTED) {
338 return false;
339 }
340
Jack Hef02d3c62017-02-21 00:39:22 -0500341 Cursor cursor =
342 mContext.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
343 null, MUSIC_SELECT_FILTER, null, MUSIC_SORT_ORDER);
344 if (cursor == null) {
345 Log.e(TAG, "Failed to retreive music: cursor is null");
346 mCurrentState = State.NON_INITIALIZED;
347 return false;
348 }
349 if (!cursor.moveToFirst()) {
350 Log.d(TAG, "Failed to move cursor to first row (no query result)");
351 cursor.close();
352 return true;
353 }
354 int idColumn = cursor.getColumnIndex(MediaStore.Audio.Media._ID);
355 int titleColumn = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE);
356 int pathColumn = cursor.getColumnIndex(MediaStore.Audio.Media.DATA);
357 do {
358 Log.i(TAG,
359 "Music ID: " + cursor.getString(idColumn)
360 + " Title: " + cursor.getString(titleColumn));
361 long thisId = cursor.getLong(idColumn);
362 String thisPath = cursor.getString(pathColumn);
363 MediaMetadata metadata = retrievMediaMetadata(thisId, thisPath);
364 Log.i(TAG, "MediaMetadata: " + metadata);
365 if (metadata == null) {
366 continue;
367 }
368 Song thisSong = new Song(thisId, metadata, null);
369 // Construct per feature database
370 mMusicList.add(metadata);
371 mMusicListById.put(thisId, thisSong);
372 mMusicListByMediaId.put(String.valueOf(thisId), thisSong);
373 addMusicToAlbumList(metadata);
374 addMusicToArtistList(metadata);
375 } while (cursor.moveToNext());
376 cursor.close();
377 return true;
378 }
379
380 private synchronized MediaMetadata retrievMediaMetadata(long musicId, String musicPath) {
381 LogHelper.d(TAG, "getting metadata for music: ", musicPath);
382 MediaMetadataRetriever retriever = new MediaMetadataRetriever();
383 Uri contentUri = ContentUris.withAppendedId(
384 android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, musicId);
385 if (!(new File(musicPath).exists())) {
386 LogHelper.d(TAG, "Does not exist, deleting item");
387 mContext.getContentResolver().delete(contentUri, null, null);
388 return null;
389 }
390 retriever.setDataSource(mContext, contentUri);
391 String title = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
392 String album = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM);
393 String artist = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST);
394 String durationString =
395 retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
396 long duration = durationString != null ? Long.parseLong(durationString) : 0;
397 MediaMetadata.Builder metadataBuilder =
398 new MediaMetadata.Builder()
399 .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, String.valueOf(musicId))
400 .putString(CUSTOM_METADATA_TRACK_SOURCE, musicPath)
401 .putString(MediaMetadata.METADATA_KEY_TITLE, title != null ? title : UNKOWN)
402 .putString(MediaMetadata.METADATA_KEY_ALBUM, album != null ? album : UNKOWN)
403 .putString(
404 MediaMetadata.METADATA_KEY_ARTIST, artist != null ? artist : UNKOWN)
405 .putLong(MediaMetadata.METADATA_KEY_DURATION, duration);
406 byte[] albumArtData = retriever.getEmbeddedPicture();
407 Bitmap bitmap;
408 if (albumArtData != null) {
409 bitmap = BitmapFactory.decodeByteArray(albumArtData, 0, albumArtData.length);
410 bitmap = MusicUtils.resizeBitmap(bitmap, getDefaultAlbumArt());
411 metadataBuilder.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, bitmap);
412 }
413 retriever.release();
414 return metadataBuilder.build();
415 }
416
417 private Bitmap getDefaultAlbumArt() {
418 BitmapFactory.Options opts = new BitmapFactory.Options();
419 opts.inPreferredConfig = Bitmap.Config.ARGB_8888;
420 return BitmapFactory.decodeStream(
421 mContext.getResources().openRawResource(R.drawable.albumart_mp_unknown), null,
422 opts);
423 }
424
425 private void addMusicToAlbumList(MediaMetadata metadata) {
426 String thisAlbum = metadata.getString(MediaMetadata.METADATA_KEY_ALBUM);
427 if (thisAlbum == null) {
428 thisAlbum = UNKOWN;
429 }
430 if (!mMusicListByAlbum.containsKey(thisAlbum)) {
431 mMusicListByAlbum.put(thisAlbum, new ArrayList<>());
432 }
433 mMusicListByAlbum.get(thisAlbum).add(metadata);
434 }
435
436 private void addMusicToArtistList(MediaMetadata metadata) {
437 String thisArtist = metadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
438 if (thisArtist == null) {
439 thisArtist = UNKOWN;
440 }
441 String thisAlbum = metadata.getString(MediaMetadata.METADATA_KEY_ALBUM);
442 if (thisAlbum == null) {
443 thisAlbum = UNKOWN;
444 }
445 if (!mArtistAlbumDb.containsKey(thisArtist)) {
446 mArtistAlbumDb.put(thisArtist, new ConcurrentHashMap<>());
447 }
448 Map<String, MediaMetadata> albumsMap = mArtistAlbumDb.get(thisArtist);
449 MediaMetadata.Builder builder;
450 long count = 0;
451 Bitmap thisAlbumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
452 if (albumsMap.containsKey(thisAlbum)) {
453 MediaMetadata album_metadata = albumsMap.get(thisAlbum);
454 count = album_metadata.getLong(MediaMetadata.METADATA_KEY_NUM_TRACKS);
455 Bitmap nAlbumArt = album_metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
456 builder = new MediaMetadata.Builder(album_metadata);
457 if (nAlbumArt != null) {
458 thisAlbumArt = null;
459 }
460 } else {
461 builder = new MediaMetadata.Builder();
462 builder.putString(MediaMetadata.METADATA_KEY_ALBUM, thisAlbum)
463 .putString(MediaMetadata.METADATA_KEY_ARTIST, thisArtist);
464 }
465 if (thisAlbumArt != null) {
466 builder.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, thisAlbumArt);
467 }
468 builder.putLong(MediaMetadata.METADATA_KEY_NUM_TRACKS, count + 1);
469 albumsMap.put(thisAlbum, builder.build());
470 }
471
472 public synchronized void updateMusic(String musicId, MediaMetadata metadata) {
473 Song song = mMusicListByMediaId.get(musicId);
474 if (song == null) {
475 return;
476 }
477
478 String oldGenre = song.getMetadata().getString(MediaMetadata.METADATA_KEY_GENRE);
479 String newGenre = metadata.getString(MediaMetadata.METADATA_KEY_GENRE);
480
481 song.setMetadata(metadata);
482
483 // if genre has changed, we need to rebuild the list by genre
484 if (!oldGenre.equals(newGenre)) {
485 // buildListsByGenre();
486 }
487 }
488}