Support MediaBrowserService in AOSP Music
* Use MediaSession and MediaController to interact between playback
service and playback applications
* Introduced MusicProvdier to scan music resources on local disk
* Removed unnecessary intents and unused activities
* Disabled playlist add, edit, delete function temporarily
Bug: 34748293
Test: make, playing music, browse through Bluetooth on Carkit
Change-Id: Ic88847aa0b3dd4ef5e13afcb839c78544ac05a2a
diff --git a/src/com/android/music/utils/MusicProvider.java b/src/com/android/music/utils/MusicProvider.java
new file mode 100644
index 0000000..bda92ea
--- /dev/null
+++ b/src/com/android/music/utils/MusicProvider.java
@@ -0,0 +1,480 @@
+/*
+ * 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 android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+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<String, List<MediaMetadata>> mMusicListByAlbum;
+ // Playlist Name --> list of Metadata
+ private ConcurrentMap<String, List<MediaMetadata>> mMusicListByPlaylist;
+ // Artist Name --> Map of (album name --> album metadata)
+ private ConcurrentMap<String, Map<String, MediaMetadata>> mArtistAlbumDb;
+ private List<MediaMetadata> mMusicList;
+ private final ConcurrentMap<Long, Song> mMusicListById;
+ private final ConcurrentMap<String, Song> 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<String> 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<MediaMetadata> getAlbums() {
+ if (mCurrentState != State.INITIALIZED) {
+ return Collections.emptyList();
+ }
+ ArrayList<MediaMetadata> albumList = new ArrayList<>();
+ for (Map<String, MediaMetadata> 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<String> getPlaylists() {
+ if (mCurrentState != State.INITIALIZED) {
+ return Collections.emptyList();
+ }
+ return mMusicListByPlaylist.keySet();
+ }
+
+ public Iterable<MediaMetadata> getMusicList() {
+ return mMusicList;
+ }
+
+ /**
+ * Get albums of a certain artist
+ *
+ */
+ public Iterable<MediaMetadata> 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<MediaMetadata> 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<MediaMetadata> 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<MediaMetadata> searchMusic(String titleQuery) {
+ if (mCurrentState != State.INITIALIZED) {
+ return Collections.emptyList();
+ }
+ ArrayList<MediaMetadata> 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<Void, Void, State>() {
+ @Override
+ protected State doInBackground(Void... params) {
+ 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<MediaMetadata> 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<MediaMetadata> 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<Song> 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<Song>() {
+ @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<MediaMetadata> metadataList = new ArrayList<>();
+ for (Song song : songList) {
+ metadataList.add(song.getMetadata());
+ }
+ return metadataList;
+ }
+
+ private synchronized boolean retrieveMedia() {
+ 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<String, MediaMetadata> 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();
+ }
+ }
+}
\ No newline at end of file