blob: d60b58512e9c2a731cddf9d26b36e5136d34ec87 [file] [log] [blame]
/*
* 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) {
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<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();
}
}
}