• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
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 
17 package com.android.music.utils;
18 
19 import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
20 
21 import android.content.ContentResolver;
22 import android.content.ContentUris;
23 import android.content.ContentValues;
24 import android.content.Context;
25 import android.content.pm.PackageManager;
26 import android.database.Cursor;
27 import android.graphics.Bitmap;
28 import android.graphics.BitmapFactory;
29 import android.graphics.drawable.BitmapDrawable;
30 import android.media.MediaActionSound;
31 import android.media.MediaMetadata;
32 import android.media.MediaMetadataRetriever;
33 import android.net.Uri;
34 import android.os.AsyncTask;
35 import android.provider.MediaStore;
36 import android.util.Log;
37 import com.android.music.MediaPlaybackService;
38 import com.android.music.MusicUtils;
39 import com.android.music.R;
40 
41 import java.io.File;
42 import java.util.*;
43 import java.util.concurrent.ConcurrentHashMap;
44 import java.util.concurrent.ConcurrentMap;
45 
46 /*
47 A provider of music contents to the music application, it reads external storage for any music
48 files, parse them and
49 store them in this class for future use.
50  */
51 public 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 
MusicProvider(Context context)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 
isInitialized()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      */
getArtists()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      */
getAlbums()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      */
getPlaylists()129     public Iterable<String> getPlaylists() {
130         if (mCurrentState != State.INITIALIZED) {
131             return Collections.emptyList();
132         }
133         return mMusicListByPlaylist.keySet();
134     }
135 
getMusicList()136     public Iterable<MediaMetadata> getMusicList() {
137         return mMusicList;
138     }
139 
140     /**
141      * Get albums of a certain artist
142      *
143      */
getAlbumByArtist(String artist)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      */
getMusicsByAlbum(String album)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      */
getMusicsByPlaylist(String playlist)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      */
getMusicById(long musicId)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      */
getMusicByMediaId(String musicId)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      */
searchMusic(String titleQuery)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 
onMusicCatalogReady(boolean success)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      */
retrieveMediaAsync(final MusicProviderCallback callback)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                 if (mCurrentState == State.INITIALIZED) {
232                     return mCurrentState;
233                 }
234                 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 
retrieveAllPlayLists()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 
retreivePlaylistMetadata( long playlistId, String playlistPath)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 
retrieveMedia()338     private synchronized boolean retrieveMedia() {
339         if (mContext.checkSelfPermission(READ_EXTERNAL_STORAGE)
340                 != PackageManager.PERMISSION_GRANTED) {
341             return false;
342         }
343 
344         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 
retrievMediaMetadata(long musicId, String musicPath)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 
getDefaultAlbumArt()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 
addMusicToAlbumList(MediaMetadata metadata)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 
addMusicToArtistList(MediaMetadata metadata)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 
updateMusic(String musicId, MediaMetadata metadata)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     }
491 }
492