/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.music.utils; import static android.Manifest.permission.READ_EXTERNAL_STORAGE; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.pm.PackageManager; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.drawable.BitmapDrawable; import android.media.MediaActionSound; import android.media.MediaMetadata; import android.media.MediaMetadataRetriever; import android.net.Uri; import android.os.AsyncTask; import android.provider.MediaStore; import android.util.Log; import com.android.music.MediaPlaybackService; import com.android.music.MusicUtils; import com.android.music.R; import java.io.File; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; /* A provider of music contents to the music application, it reads external storage for any music files, parse them and store them in this class for future use. */ public class MusicProvider { private static final String TAG = "MusicProvider"; // Public constants public static final String UNKOWN = "UNKNOWN"; // Uri source of this track public static final String CUSTOM_METADATA_TRACK_SOURCE = "__SOURCE__"; // Sort key for this tack public static final String CUSTOM_METADATA_SORT_KEY = "__SORT_KEY__"; // Content select criteria private static final String MUSIC_SELECT_FILTER = MediaStore.Audio.Media.IS_MUSIC + " != 0"; private static final String MUSIC_SORT_ORDER = MediaStore.Audio.Media.TITLE + " ASC"; // Categorized caches for music track data: private Context mContext; // Album Name --> list of Metadata private ConcurrentMap> mMusicListByAlbum; // Playlist Name --> list of Metadata private ConcurrentMap> mMusicListByPlaylist; // Artist Name --> Map of (album name --> album metadata) private ConcurrentMap> mArtistAlbumDb; private List mMusicList; private final ConcurrentMap mMusicListById; private final ConcurrentMap mMusicListByMediaId; enum State { NON_INITIALIZED, INITIALIZING, INITIALIZED } private volatile State mCurrentState = State.NON_INITIALIZED; public MusicProvider(Context context) { mContext = context; mArtistAlbumDb = new ConcurrentHashMap<>(); mMusicListByAlbum = new ConcurrentHashMap<>(); mMusicListByPlaylist = new ConcurrentHashMap<>(); mMusicListById = new ConcurrentHashMap<>(); mMusicList = new ArrayList<>(); mMusicListByMediaId = new ConcurrentHashMap<>(); mMusicListByPlaylist.put(MediaIDHelper.MEDIA_ID_NOW_PLAYING, new ArrayList<>()); } public boolean isInitialized() { return mCurrentState == State.INITIALIZED; } /** * Get an iterator over the list of artists * * @return list of artists */ public Iterable getArtists() { if (mCurrentState != State.INITIALIZED) { return Collections.emptyList(); } return mArtistAlbumDb.keySet(); } /** * Get an iterator over the list of albums * * @return list of albums */ public Iterable getAlbums() { if (mCurrentState != State.INITIALIZED) { return Collections.emptyList(); } ArrayList albumList = new ArrayList<>(); for (Map artist_albums : mArtistAlbumDb.values()) { albumList.addAll(artist_albums.values()); } return albumList; } /** * Get an iterator over the list of playlists * * @return list of playlists */ public Iterable getPlaylists() { if (mCurrentState != State.INITIALIZED) { return Collections.emptyList(); } return mMusicListByPlaylist.keySet(); } public Iterable getMusicList() { return mMusicList; } /** * Get albums of a certain artist * */ public Iterable getAlbumByArtist(String artist) { if (mCurrentState != State.INITIALIZED || !mArtistAlbumDb.containsKey(artist)) { return Collections.emptyList(); } return mArtistAlbumDb.get(artist).values(); } /** * Get music tracks of the given album * */ public Iterable getMusicsByAlbum(String album) { if (mCurrentState != State.INITIALIZED || !mMusicListByAlbum.containsKey(album)) { return Collections.emptyList(); } return mMusicListByAlbum.get(album); } /** * Get music tracks of the given playlist * */ public Iterable getMusicsByPlaylist(String playlist) { if (mCurrentState != State.INITIALIZED || !mMusicListByPlaylist.containsKey(playlist)) { return Collections.emptyList(); } return mMusicListByPlaylist.get(playlist); } /** * Return the MediaMetadata for the given musicID. * * @param musicId The unique, non-hierarchical music ID. */ public Song getMusicById(long musicId) { return mMusicListById.containsKey(musicId) ? mMusicListById.get(musicId) : null; } /** * Return the MediaMetadata for the given musicID. * * @param musicId The unique, non-hierarchical music ID. */ public Song getMusicByMediaId(String musicId) { return mMusicListByMediaId.containsKey(musicId) ? mMusicListByMediaId.get(musicId) : null; } /** * Very basic implementation of a search that filter music tracks which title containing * the given query. * */ public Iterable searchMusic(String titleQuery) { if (mCurrentState != State.INITIALIZED) { return Collections.emptyList(); } ArrayList result = new ArrayList<>(); titleQuery = titleQuery.toLowerCase(); for (Song song : mMusicListByMediaId.values()) { if (song.getMetadata() .getString(MediaMetadata.METADATA_KEY_TITLE) .toLowerCase() .contains(titleQuery)) { result.add(song.getMetadata()); } } return result; } public interface MusicProviderCallback { void onMusicCatalogReady(boolean success); } /** * Get the list of music tracks from disk and caches the track information * for future reference, keying tracks by musicId and grouping by genre. */ public void retrieveMediaAsync(final MusicProviderCallback callback) { Log.d(TAG, "retrieveMediaAsync called"); if (mCurrentState == State.INITIALIZED) { // Nothing to do, execute callback immediately callback.onMusicCatalogReady(true); return; } // Asynchronously load the music catalog in a separate thread new AsyncTask() { @Override protected State doInBackground(Void... params) { if (mCurrentState == State.INITIALIZED) { return mCurrentState; } mCurrentState = State.INITIALIZING; if (retrieveMedia()) { mCurrentState = State.INITIALIZED; } else { mCurrentState = State.NON_INITIALIZED; } return mCurrentState; } @Override protected void onPostExecute(State current) { if (callback != null) { callback.onMusicCatalogReady(current == State.INITIALIZED); } } } .execute(); } public synchronized boolean retrieveAllPlayLists() { Cursor cursor = mContext.getContentResolver().query( MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, null, null, null, null); if (cursor == null) { Log.e(TAG, "Failed to retreive playlist: cursor is null"); return false; } if (!cursor.moveToFirst()) { Log.d(TAG, "Failed to move cursor to first row (no query result)"); cursor.close(); return true; } int idColumn = cursor.getColumnIndex(MediaStore.Audio.Playlists._ID); int nameColumn = cursor.getColumnIndex(MediaStore.Audio.Playlists.NAME); int pathColumn = cursor.getColumnIndex(MediaStore.Audio.Playlists.DATA); do { long thisId = cursor.getLong(idColumn); String thisPath = cursor.getString(pathColumn); String thisName = cursor.getString(nameColumn); Log.i(TAG, "PlayList ID: " + thisId + " Name: " + thisName); List songList = retreivePlaylistMetadata(thisId, thisPath); LogHelper.i(TAG, "Found ", songList.size(), " items for playlist name: ", thisName); mMusicListByPlaylist.put(thisName, songList); } while (cursor.moveToNext()); cursor.close(); return true; } public synchronized List retreivePlaylistMetadata( long playlistId, String playlistPath) { Cursor cursor = mContext.getContentResolver().query(Uri.parse(playlistPath), null, MediaStore.Audio.Playlists.Members.PLAYLIST_ID + " == " + playlistId, null, null); if (cursor == null) { Log.e(TAG, "Failed to retreive individual playlist: cursor is null"); return null; } if (!cursor.moveToFirst()) { Log.d(TAG, "Failed to move cursor to first row (no query result for playlist)"); cursor.close(); return null; } List songList = new ArrayList<>(); int idColumn = cursor.getColumnIndex(MediaStore.Audio.Playlists.Members._ID); int audioIdColumn = cursor.getColumnIndex(MediaStore.Audio.Playlists.Members.AUDIO_ID); int orderColumn = cursor.getColumnIndex(MediaStore.Audio.Playlists.Members.PLAY_ORDER); int audioPathColumn = cursor.getColumnIndex(MediaStore.Audio.Playlists.Members.DATA); int audioNameColumn = cursor.getColumnIndex(MediaStore.Audio.Playlists.Members.TITLE); do { long thisId = cursor.getLong(idColumn); long thisAudioId = cursor.getLong(audioIdColumn); long thisOrder = cursor.getLong(orderColumn); String thisAudioPath = cursor.getString(audioPathColumn); Log.i(TAG, "Playlist ID: " + playlistId + " Music ID: " + thisAudioId + " Name: " + audioNameColumn); if (!mMusicListById.containsKey(thisAudioId)) { LogHelper.d(TAG, "Music does not exist"); continue; } Song song = mMusicListById.get(thisAudioId); song.setSortKey(thisOrder); songList.add(song); } while (cursor.moveToNext()); cursor.close(); songList.sort(new Comparator() { @Override public int compare(Song s1, Song s2) { long key1 = s1.getSortKey(); long key2 = s2.getSortKey(); if (key1 < key2) { return -1; } else if (key1 == key2) { return 0; } else { return 1; } } }); List metadataList = new ArrayList<>(); for (Song song : songList) { metadataList.add(song.getMetadata()); } return metadataList; } private synchronized boolean retrieveMedia() { if (mContext.checkSelfPermission(READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { return false; } Cursor cursor = mContext.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, MUSIC_SELECT_FILTER, null, MUSIC_SORT_ORDER); if (cursor == null) { Log.e(TAG, "Failed to retreive music: cursor is null"); mCurrentState = State.NON_INITIALIZED; return false; } if (!cursor.moveToFirst()) { Log.d(TAG, "Failed to move cursor to first row (no query result)"); cursor.close(); return true; } int idColumn = cursor.getColumnIndex(MediaStore.Audio.Media._ID); int titleColumn = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE); int pathColumn = cursor.getColumnIndex(MediaStore.Audio.Media.DATA); do { Log.i(TAG, "Music ID: " + cursor.getString(idColumn) + " Title: " + cursor.getString(titleColumn)); long thisId = cursor.getLong(idColumn); String thisPath = cursor.getString(pathColumn); MediaMetadata metadata = retrievMediaMetadata(thisId, thisPath); Log.i(TAG, "MediaMetadata: " + metadata); if (metadata == null) { continue; } Song thisSong = new Song(thisId, metadata, null); // Construct per feature database mMusicList.add(metadata); mMusicListById.put(thisId, thisSong); mMusicListByMediaId.put(String.valueOf(thisId), thisSong); addMusicToAlbumList(metadata); addMusicToArtistList(metadata); } while (cursor.moveToNext()); cursor.close(); return true; } private synchronized MediaMetadata retrievMediaMetadata(long musicId, String musicPath) { LogHelper.d(TAG, "getting metadata for music: ", musicPath); MediaMetadataRetriever retriever = new MediaMetadataRetriever(); Uri contentUri = ContentUris.withAppendedId( android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, musicId); if (!(new File(musicPath).exists())) { LogHelper.d(TAG, "Does not exist, deleting item"); mContext.getContentResolver().delete(contentUri, null, null); return null; } retriever.setDataSource(mContext, contentUri); String title = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE); String album = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM); String artist = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST); String durationString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); long duration = durationString != null ? Long.parseLong(durationString) : 0; MediaMetadata.Builder metadataBuilder = new MediaMetadata.Builder() .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, String.valueOf(musicId)) .putString(CUSTOM_METADATA_TRACK_SOURCE, musicPath) .putString(MediaMetadata.METADATA_KEY_TITLE, title != null ? title : UNKOWN) .putString(MediaMetadata.METADATA_KEY_ALBUM, album != null ? album : UNKOWN) .putString( MediaMetadata.METADATA_KEY_ARTIST, artist != null ? artist : UNKOWN) .putLong(MediaMetadata.METADATA_KEY_DURATION, duration); byte[] albumArtData = retriever.getEmbeddedPicture(); Bitmap bitmap; if (albumArtData != null) { bitmap = BitmapFactory.decodeByteArray(albumArtData, 0, albumArtData.length); bitmap = MusicUtils.resizeBitmap(bitmap, getDefaultAlbumArt()); metadataBuilder.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, bitmap); } retriever.release(); return metadataBuilder.build(); } private Bitmap getDefaultAlbumArt() { BitmapFactory.Options opts = new BitmapFactory.Options(); opts.inPreferredConfig = Bitmap.Config.ARGB_8888; return BitmapFactory.decodeStream( mContext.getResources().openRawResource(R.drawable.albumart_mp_unknown), null, opts); } private void addMusicToAlbumList(MediaMetadata metadata) { String thisAlbum = metadata.getString(MediaMetadata.METADATA_KEY_ALBUM); if (thisAlbum == null) { thisAlbum = UNKOWN; } if (!mMusicListByAlbum.containsKey(thisAlbum)) { mMusicListByAlbum.put(thisAlbum, new ArrayList<>()); } mMusicListByAlbum.get(thisAlbum).add(metadata); } private void addMusicToArtistList(MediaMetadata metadata) { String thisArtist = metadata.getString(MediaMetadata.METADATA_KEY_ARTIST); if (thisArtist == null) { thisArtist = UNKOWN; } String thisAlbum = metadata.getString(MediaMetadata.METADATA_KEY_ALBUM); if (thisAlbum == null) { thisAlbum = UNKOWN; } if (!mArtistAlbumDb.containsKey(thisArtist)) { mArtistAlbumDb.put(thisArtist, new ConcurrentHashMap<>()); } Map albumsMap = mArtistAlbumDb.get(thisArtist); MediaMetadata.Builder builder; long count = 0; Bitmap thisAlbumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART); if (albumsMap.containsKey(thisAlbum)) { MediaMetadata album_metadata = albumsMap.get(thisAlbum); count = album_metadata.getLong(MediaMetadata.METADATA_KEY_NUM_TRACKS); Bitmap nAlbumArt = album_metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART); builder = new MediaMetadata.Builder(album_metadata); if (nAlbumArt != null) { thisAlbumArt = null; } } else { builder = new MediaMetadata.Builder(); builder.putString(MediaMetadata.METADATA_KEY_ALBUM, thisAlbum) .putString(MediaMetadata.METADATA_KEY_ARTIST, thisArtist); } if (thisAlbumArt != null) { builder.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, thisAlbumArt); } builder.putLong(MediaMetadata.METADATA_KEY_NUM_TRACKS, count + 1); albumsMap.put(thisAlbum, builder.build()); } public synchronized void updateMusic(String musicId, MediaMetadata metadata) { Song song = mMusicListByMediaId.get(musicId); if (song == null) { return; } String oldGenre = song.getMetadata().getString(MediaMetadata.METADATA_KEY_GENRE); String newGenre = metadata.getString(MediaMetadata.METADATA_KEY_GENRE); song.setMetadata(metadata); // if genre has changed, we need to rebuild the list by genre if (!oldGenre.equals(newGenre)) { // buildListsByGenre(); } } }