Home | History | Annotate | Download | only in utils
      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 
     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                 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 
    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() {
    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 
    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     }
    491 }
    492