package com.fastbootmobile.encore.providers.localprovider;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.MediaCodec;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.net.Uri;
import android.os.Handler;
import android.os.RemoteException;
import android.provider.MediaStore;
import android.util.Log;
import com.fastbootmobile.encore.model.Album;
import com.fastbootmobile.encore.model.Artist;
import com.fastbootmobile.encore.model.BoundEntity;
import com.fastbootmobile.encore.model.Genre;
import com.fastbootmobile.encore.model.Playlist;
import com.fastbootmobile.encore.model.SearchResult;
import com.fastbootmobile.encore.model.Song;
import com.fastbootmobile.encore.providers.IArtCallback;
import com.fastbootmobile.encore.providers.ProviderIdentifier;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
public class LocalProvider {
private static final String TAG = "LocalProvider";
private static final String PREFIX_SONG = "local:song:";
private static final String PREFIX_ALBUM = "local:album:";
private static final String PREFIX_ARTIST = "local:artist:";
private static final String PREFIX_PLAYLIST = "local:playlist:";
private Uri mUri;
private HashMap<String, LocalSong> mSongs;
private ContentResolver mContentResolver;
private HashMap<String, Playlist> mPlaylists;
private LocalSong mCurrentSong;
private MediaCodec mDecoder;
private MediaExtractor mExtractor;
private boolean mChangeMusic;
private MediaCodec.BufferInfo mInfo;
private ByteBuffer[] mOutputBuffers;
private ByteBuffer[] mInputBuffers;
private Context mContext;
private MediaFormat mFormat;
private LocalCallback mCallback;
private HashMap<String, Artist> mArtists;
private HashMap<String, Album> mAlbums;
private HashMap<String, Genre> mGenres;
private HashMap<String, Long> mAlbumsId;
private Handler mHandler = new Handler();
private boolean mSetup;
private boolean mPaused;
private SearchResult mSearchResult;
private boolean mIsEOS;
private int mPendingOutIndex = -1;
private int mPendingSize;
private final ContentObserver mAlbumContentObserver = new ContentObserver(mHandler) {
@Override
public void onChange(boolean self) {
try {
fetchAlbums();
} catch (SecurityException e) {
Log.e(TAG, "Cannot read albums because of a security exception", e);
}
}
@Override
public void onChange(boolean self, Uri uri) {
onChange(self);
}
};
private final ContentObserver mArtistContentObserver = new ContentObserver(mHandler) {
@Override
public void onChange(boolean self) {
try {
fetchArtists();
} catch (SecurityException e) {
Log.e(TAG, "Cannot read artists because of a security exception", e);
}
}
@Override
public void onChange(boolean self, Uri uri) {
onChange(self);
}
};
private final ContentObserver mGenreContentObserver = new ContentObserver(mHandler) {
@Override
public void onChange(boolean self) {
try {
fetchGenres(null);
} catch (SecurityException e) {
Log.e(TAG, "Cannot read genres because of a security exception", e);
}
}
@Override
public void onChange(boolean self, Uri uri) {
onChange(self);
}
};
public LocalProvider(Uri uri, ContentResolver cr, LocalCallback cb, Context context) {
mCallback = cb;
mContentResolver = cr;
mUri = uri;
mSongs = new HashMap<>();
mAlbums = new HashMap<>();
mArtists = new HashMap<>();
mPlaylists = new HashMap<>();
mGenres = new HashMap<>();
mAlbumsId = new HashMap<>();
mChangeMusic = true;
mContext = context;
mAudioPushRunnable.start();
mSetup = false;
}
public void notifyIdentifier(final ProviderIdentifier id) {
Set<String> keys = mSongs.keySet();
for (String key : keys) {
LocalSong lSong = mSongs.get(key);
if (lSong != null) {
lSong.getSong().setProvider(id);
}
}
keys = mAlbums.keySet();
for (String key : keys) {
mAlbums.get(key).setProvider(id);
}
keys = mArtists.keySet();
for (String key : keys) {
mArtists.get(key).setProvider(id);
}
keys = mPlaylists.keySet();
for (String key : keys) {
mPlaylists.get(key).setProvider(id);
}
keys = mGenres.keySet();
for (String key : keys) {
mGenres.get(key).setProvider(id);
}
}
/**
* The main function to use for polling all the local content, use it in a thread so it doesn't slow down the whole application
*/
public void poll() {
mSetup = false;
mContentResolver.registerContentObserver(mUri, true, mSongContentObserver);
mContentResolver.registerContentObserver(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, true, mPlaylistContentObserver);
mContentResolver.registerContentObserver(MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI, true, mGenreContentObserver);
mContentResolver.registerContentObserver(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, true, mAlbumContentObserver);
mContentResolver.registerContentObserver(MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI, true, mArtistContentObserver);
mSetup = true;
try {
fetchAlbums();
fetchArtists();
fetchSongs();
fetchPlaylists(null);
fetchGenres(MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI);
} catch (SecurityException e) {
// This happened once on a Nexus Player.
Log.e(TAG, "Security exception when fetching local provider data! " + e.getMessage());
}
}
private void fetchAlbums() {
final String[] proj = {"*"};
final Cursor cur = mContentResolver.query(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, proj, null, null, null);
if (cur != null) {
if (cur.moveToFirst()) {
final int albumName = cur.getColumnIndex(MediaStore.Audio.AlbumColumns.ALBUM);
final int artistName = cur.getColumnIndex(MediaStore.Audio.AlbumColumns.ARTIST);
final int albumKey = cur.getColumnIndex(MediaStore.Audio.AlbumColumns.ALBUM_KEY);
final int yearKey = cur.getColumnIndex(MediaStore.Audio.AlbumColumns.LAST_YEAR);
final int idKey = cur.getColumnIndex(MediaStore.Audio.Albums._ID);
do {
Album album = new Album(PREFIX_ALBUM + getAlbumUniqueName(cur.getString(albumKey), cur.getString(artistName)));
album.setName(cur.getString(albumName));
album.setIsLoaded(true);
album.setSourceLogo(PluginService.LOGO_REF);
album.setYear(cur.getInt(yearKey));
// we get the contents of the album
mAlbums.put(album.getRef(), album);
mAlbumsId.put(album.getRef(), cur.getLong(idKey));
} while (cur.moveToNext());
}
cur.close();
}
// first we poll all the musics
mContentResolver.registerContentObserver(mUri, true, mSongContentObserver);
mContentResolver.registerContentObserver(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, true, mSongContentObserver);
}
public void fetchSongs() {
// First we poll all the songs
final Cursor cur = mContentResolver.query(mUri, null,
MediaStore.Audio.Media.IS_MUSIC + " = 1", null, null);
if (cur != null) {
if (cur.moveToFirst()) {
// Fetch all the columns we are interested in
int artistKey = cur.getColumnIndex(MediaStore.Audio.Media.ARTIST_KEY);
int albumKey = cur.getColumnIndex(MediaStore.Audio.Media.ALBUM_KEY);
int titleKey = cur.getColumnIndex(MediaStore.Audio.Media.TITLE_KEY);
int artistColumn = cur.getColumnIndex(MediaStore.Audio.Media.ARTIST);
int titleColumn = cur.getColumnIndex(MediaStore.Audio.Media.TITLE);
int albumIdColumn = cur.getColumnIndex(MediaStore.Audio.Media.ALBUM_ID);
int durationColumn = cur.getColumnIndex(MediaStore.Audio.Media.DURATION);
int idColumn = cur.getColumnIndex(MediaStore.Audio.Media._ID);
int yearColumn = cur.getColumnIndex(MediaStore.Audio.Media.YEAR);
do {
// We create the unique ID the song have
final String uniquename = getSongUniqueName(cur.getString(artistKey), cur.getString(albumKey), cur.getString(titleKey));
Song s = new Song(PREFIX_SONG + uniquename);
s.setAvailable(true);
s.setTitle(cur.getString(titleColumn));
String artistSrc = cur.getString(artistKey);
if (artistSrc != null) {
s.setArtist(PREFIX_ARTIST + getArtistUniqueName(artistSrc));
}
s.setDuration((int) cur.getLong(durationColumn));
s.setAlbum(PREFIX_ALBUM + getAlbumUniqueName(cur.getString(albumKey), cur.getString(artistColumn)));
Album album = mAlbums.get(s.getAlbum());
if (album != null) {
album.addSong(s.getRef());
}
s.setYear(cur.getInt(yearColumn));
s.setIsLoaded(true); // Local songs are always fully loaded
s.setOfflineStatus(BoundEntity.OFFLINE_STATUS_READY); // Local songs are always offline
s.setSourceLogo(PluginService.LOGO_REF);
//we keep LocalSongs so we still have the id informations
final Long id = cur.getLong(idColumn);
final Long albumId = cur.getLong(albumIdColumn);
mSongs.put(s.getRef(), new LocalSong(s, id, albumId));
mCallback.songUpdated(s);
} while (cur.moveToNext());
}
cur.close();
}
for (Album album : mAlbums.values()) {
mCallback.albumUpdated(album);
}
}
public void fetchArtists() {
final String[] proj = {"*"};
// we poll the artists
final Cursor cur = mContentResolver.query(MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI, proj, null, null, null);
if (cur != null) {
if (cur.moveToFirst()) {
final int artistName = cur.getColumnIndex(MediaStore.Audio.ArtistColumns.ARTIST);
final int artistKey = cur.getColumnIndex(MediaStore.Audio.ArtistColumns.ARTIST_KEY);
final int artistId = cur.getColumnIndex(MediaStore.Audio.Artists._ID);
do {
String artistKeyStr = cur.getString(artistKey);
if (artistKeyStr == null || artistKeyStr.isEmpty()) {
artistKeyStr = cur.getString(artistName);
}
if (artistKeyStr == null || artistKeyStr.isEmpty()) {
artistKeyStr = String.valueOf(cur.getLong(artistId));
}
Artist artist = new Artist(PREFIX_ARTIST + getArtistUniqueName(artistKeyStr));
artist.setName(cur.getString(artistName));
artist.setIsLoaded(true);
// we get the albums from this artist
artist = getAlbumsArtists(artist, MediaStore.Audio.Artists.Albums.getContentUri("external", cur.getLong(artistId)));
if (artist != null) {
artist.setSourceLogo(PluginService.LOGO_REF);
mArtists.put(artist.getRef(), artist);
mCallback.artistUpdated(artist);
}
} while (cur.moveToNext());
}
cur.close();
}
}
public void fetchPlaylists(String idPlaylist) {
Uri uri;
uri = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI;
String request = null;
if (idPlaylist != null) {
request = MediaStore.Audio.Playlists._ID + " = " + idPlaylist;
}
Cursor cur;
String[] proj = {"*"};
// We now poll the playlists
cur = mContentResolver.query(uri, proj,
request, null, null);
if (cur != null && cur.moveToFirst()) {
final int idKey = cur.getColumnIndex(MediaStore.Audio.Playlists._ID);
final int nameKey = cur.getColumnIndex(MediaStore.Audio.Playlists.NAME);
do {
Long id = cur.getLong(idKey);
String name = cur.getString(nameKey);
Playlist play = new Playlist(PREFIX_PLAYLIST + getPlaylistUniqueName(Long.toString(id)));
play.setName(name);
play.setIsLoaded(true);
// we get the content of the playlist
play = getPlaylist(MediaStore.Audio.Playlists.Members.getContentUri("external", id), play);
if (play != null) {
mPlaylists.put(play.getRef(), play);
// we give to the app the new playlists when we finish polling it
mCallback.playlistUpdated(play);
}
} while (cur.moveToNext());
}
if (cur != null) {
cur.close();
}
}
public void fetchGenres(Uri uri) {
if (uri == null)
uri = MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI;
Cursor cur;
String[] proj = {"*"};
// now we poll the genre
cur = mContentResolver.query(uri, proj, null, null, null);
if (cur != null) {
if (cur.moveToFirst()) {
final int idKey = cur.getColumnIndex(MediaStore.Audio.Genres._ID);
final int nameKey = cur.getColumnIndex(MediaStore.Audio.Genres.NAME);
do {
Long id = cur.getLong(idKey);
String name = cur.getString(nameKey);
Genre genre = new Genre("local:genre:" + MD5(name));
genre.setName(name);
genre.setIsLoaded(true);
getGenreSongs(MediaStore.Audio.Genres.Members.getContentUri("external", id), genre);
genre.setSourceLogo(PluginService.LOGO_REF);
mGenres.put(genre.getRef(), genre);
mCallback.genreUpdated(genre);
} while (cur.moveToNext());
}
cur.close();
}
}
public boolean getSongArt(String songRef, IArtCallback callback) {
LocalSong ls = getLocalSong(songRef);
if (ls == null) {
Log.d(TAG, "Cannot find local song " + songRef + " for art request");
return false;
}
Long albumId = ls.getAlbumId();
if (albumId == null) {
return false;
}
return getAlbumArt(albumId, callback);
}
public boolean getAlbumArt(String albumRef, IArtCallback callback) {
Long albumId = mAlbumsId.get(albumRef);
if (albumId == null) {
return false;
}
return getAlbumArt(albumId, callback);
}
private boolean getAlbumArt(long albumId, final IArtCallback callback) {
Uri artworkUri = Uri.parse("content://media/external/audio/albumart");
Uri uri = ContentUris.withAppendedId(artworkUri, albumId);
try {
final InputStream in = mContentResolver.openInputStream(uri);
new Thread() {
public void run() {
Bitmap output = BitmapFactory.decodeStream(in);
try {
in.close();
} catch (IOException ignore) {
}
try {
callback.onArtLoaded(output);
} catch (RemoteException e) {
Log.e(TAG, "Got exception in the app when reporting album art", e);
}
}
}.start();
// We have an URI, so in theory we should be good
return true;
} catch (Exception e) {
// we can't get an album art so we return false
return false;
}
}
ContentObserver mSongContentObserver = new ContentObserver(mHandler) {
@Override
public void onChange(boolean self) {
try {
fetchSongs();
} catch (SecurityException e) {
Log.e(TAG, "Cannot read songs because of a security exception", e);
}
}
@Override
public void onChange(boolean self, Uri uri) {
onChange(self);
}
};
ContentObserver mPlaylistContentObserver = new ContentObserver(mHandler) {
@Override
public void onChange(boolean self) {
onChange(self, null);
}
@Override
public void onChange(boolean self, Uri uri) {
try {
fetchPlaylists(null);
} catch (SecurityException e) {
Log.e(TAG, "Cannot read playlists because of a security exception", e);
}
}
};
/**
* @return if the provider finished polling the content
*/
public boolean isSetup() {
return mSetup;
}
/**
* Returns an unique song identifier for the provided values
* @param artistKey The internal key of the artist
* @param albumKey The internal key of the album
* @param titleKey The internal key of the title
* @return An unique song identifier for the song
*/
private String getSongUniqueName(String artistKey, String albumKey, String titleKey) {
return MD5(artistKey + albumKey + titleKey);
}
private String getAlbumUniqueName(String albumKey, String artistName) {
return MD5(albumKey + artistName);
}
private String getArtistUniqueName(String artistKey) {
return MD5(artistKey);
}
private String getPlaylistUniqueName(String playlistId) {
return MD5(playlistId);
}
/**
* Get the albums by the given artist
*
* @param artist the artist
* @param uri the uri of the artist
* @return the artist with all its albums
*/
public Artist getAlbumsArtists(Artist artist, Uri uri) {
Cursor albums = mContentResolver.query(uri, null, null, null, null);
if (albums != null) {
// we only need the name of the album to generate the album local id
final int albumKey = albums.getColumnIndex(MediaStore.Audio.AlbumColumns.ALBUM_KEY);
if (albums.moveToFirst()) {
do {
artist.addAlbum(PREFIX_ALBUM + getAlbumUniqueName(albums.getString(albumKey), artist.getName()));
} while (albums.moveToNext());
}
albums.close();
}
return artist;
}
/**
* Fetch the playlist content
*
* @param uri the playlist's uri
* @param play the playlist
* @return the playlist with its content
*/
public Playlist getPlaylist(Uri uri, Playlist play) {
//what we info do we need
String[] projection = {
MediaStore.Audio.Playlists.Members.TITLE_KEY,
MediaStore.Audio.Playlists.Members.ARTIST_KEY,
MediaStore.Audio.Playlists.Members.ALBUM_KEY
};
Cursor tracks = mContentResolver.query(uri, projection, MediaStore.Audio.Media.IS_MUSIC + " != 0 ", null, null);
if (tracks != null) {
int artistKeyColumn = tracks.getColumnIndex(MediaStore.Audio.Playlists.Members.ARTIST_KEY);
int albumKeyColumn = tracks.getColumnIndex(MediaStore.Audio.Playlists.Members.ALBUM_KEY);
int titleKeyColumn = tracks.getColumnIndex(MediaStore.Audio.Playlists.Members.TITLE_KEY);
if (tracks.moveToFirst()) {
do {
// for each song we get its unique name and we put it in the playlist
play.addSong(PREFIX_SONG + getSongUniqueName(tracks.getString(artistKeyColumn),
tracks.getString(albumKeyColumn),
tracks.getString(titleKeyColumn)));
} while (tracks.moveToNext());
}
tracks.close();
}
return play;
}
/**
* Get the songs of the genre given
*
* @param uri the uri of the genre
* @param genre the genre to add the songs to
*/
public void getGenreSongs(Uri uri, Genre genre) {
String[] projection = {
MediaStore.Audio.Genres.Members.TITLE_KEY,
MediaStore.Audio.Genres.Members.ARTIST_KEY,
MediaStore.Audio.Genres.Members.ALBUM_KEY
};
Cursor tracks = mContentResolver.query(uri, projection, MediaStore.Audio.Media.IS_MUSIC + " != 0 ", null, null);
if (tracks != null) {
int titleKeyColumn = tracks.getColumnIndex(MediaStore.Audio.Genres.Members.TITLE_KEY);
int artistKeyColumn = tracks.getColumnIndex(MediaStore.Audio.Genres.Members.ARTIST_KEY);
int albumKeyColumn = tracks.getColumnIndex(MediaStore.Audio.Genres.Members.ALBUM_KEY);
if (tracks.moveToNext()) {
do {
genre.addSong(PREFIX_SONG + getSongUniqueName(tracks.getString(artistKeyColumn),
tracks.getString(albumKeyColumn),
tracks.getString(titleKeyColumn)));
} while (tracks.moveToNext());
}
tracks.close();
}
}
/**
* @return returns a list of the songs
*/
public List<Song> getSongs(int offset, int range) {
final ArrayList<Song> songs = new ArrayList<Song>();
final Collection<LocalSong> localSongs = new ArrayList<>(mSongs.values());
int index = 0;
for (LocalSong song : localSongs) {
if (index < offset) {
++index;
continue;
}
Song s = song.getSong();
if (s != null) {
s.setSourceLogo(PluginService.LOGO_REF);
songs.add(s);
}
if (index >= offset + range) {
break;
}
++index;
}
return songs;
}
/**
* @return A list of all genres
*/
public List<Genre> getGenres() {
return new ArrayList<Genre>(mGenres.values());
}
/**
* @return returns a list of the Artists
*/
public List<Artist> getArtists() {
return new ArrayList<Artist>(mArtists.values());
}
/**
* @return returns a list of the Albums
*/
public List<Album> getAlbums() {
return new ArrayList<Album>(mAlbums.values());
}
/**
* @param md5 the String to hash
* @return The String md5 hashed
*/
public String MD5(String md5) {
try {
java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5");
byte[] array = md.digest(md5.getBytes());
StringBuilder sb = new StringBuilder();
for (byte anArray : array) {
sb.append(Integer.toHexString((anArray & 0xFF) | 0x100).substring(1, 3));
}
return sb.toString();
} catch (java.security.NoSuchAlgorithmException ignored) {
}
return null;
}
/**
* @return returns a list of the Playlists
*/
public List<Playlist> getPlaylists() {
return new ArrayList<>(mPlaylists.values());
}
/**
* @param ref the unique reference of the Song wanted
* @return the song wanted
*/
public Song getSong(String ref) {
Song s = null;
if (!mSetup) {
Log.e(TAG, "Trying to load song before the end of the initialisation of the plugin, aborting");
return null;
}
try {
LocalSong lS = mSongs.get(ref);
if (lS != null) {
s = lS.getSong();
if (s != null) {
s.setSourceLogo(PluginService.LOGO_REF);
mCallback.artistUpdated(mArtists.get(s.getArtist()));
}
}
} catch (Exception e) {
Log.d("LocalProvider", "Song not found exception " + e.getMessage());
}
return s;
}
public Artist getArtist(String ref) {
Artist a = mArtists.get(ref);
if (a != null) {
a.setSourceLogo(PluginService.LOGO_REF);
}
return a;
}
public Album getAlbum(String ref) {
Album a = mAlbums.get(ref);
if (a != null) {
a.setSourceLogo(PluginService.LOGO_REF);
}
return a;
}
/**
* @param ref the unique reference of the LocalSong wanted
* @return the LocalSong wanted
*/
public LocalSong getLocalSong(String ref) {
LocalSong s = null;
try {
s = mSongs.get(ref);
if (s.getSong() != null) {
s.getSong().setSourceLogo(PluginService.LOGO_REF);
}
} catch (Exception e) {
Log.d("LocalProvider", "Song not found", e);
}
return s;
}
/**
* @param ref the unique reference of the Playlist
* @return the playlist
*/
public Playlist getPlaylist(String ref) {
return mPlaylists.get(ref);
}
/**
* @param ref the unique reference of the Playlist
* @return the playlist id
*/
public long getPlaylistId(String ref) {
String name = getPlaylist(ref).getName();
long id = -1;
Cursor cursor = mContentResolver.query(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
new String[]{MediaStore.Audio.Playlists._ID},
MediaStore.Audio.Playlists.NAME + "=?",
new String[]{name}, null);
if (cursor != null && cursor.moveToFirst()) {
id = cursor.getLong(0);
cursor.close();
}
return id;
}
/**
* Swaps two elements of a playlist
*
* @param oldPosition the position of the first element
* @param newPosition the position of the second element
* @param playlistRef the reference of the playlist
* @return true if the change has been saved
*/
public boolean onUserSwapPlaylistItem(int oldPosition, int newPosition, String playlistRef) {
long id = getPlaylistId(playlistRef);
Playlist playlist = getPlaylist(playlistRef);
//We get the LocalSong corresponding
LocalSong oldMusic = getLocalSong(playlist.songsList().get(oldPosition));
LocalSong newMusic = getLocalSong(playlist.songsList().get(newPosition));
// We modify the playlist corresponding
playlist.setSong(oldPosition, newMusic.getSong().getRef());
playlist.setSong(newPosition, oldMusic.getSong().getRef());
// We update the playlist list and the app list
mPlaylists.put(playlistRef, playlist);
mCallback.playlistUpdated(playlist);
// Now we modify the database
Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", id);
ContentValues oldValues, newValues;
oldValues = new ContentValues();
newValues = new ContentValues();
// We set the values to update
oldValues.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, oldPosition);
oldValues.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, newMusic.getId());
newValues.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, newPosition);
newValues.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, oldMusic.getId());
// We update the database
mContentResolver.update(uri, oldValues, MediaStore.Audio.Playlists.Members.PLAY_ORDER + "=" + oldPosition, null);
mContentResolver.update(uri, newValues, MediaStore.Audio.Playlists.Members.PLAY_ORDER + "=" + newPosition, null);
// Errors aren't supported for now
return true;
}
/**
* Deletes a playlist
*
* @param playlistRef the reference of the playlist
* @return true if the playlist has been deleted
*/
public boolean deletePlaylist(String playlistRef) {
Log.d(TAG, "Deleting playlist " + playlistRef);
long id = getPlaylistId(playlistRef);
String where = MediaStore.Audio.Playlists._ID + "=?";
String[] whereVal = {String.valueOf(id)};
mContentResolver.delete(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, where, whereVal);
mPlaylists.remove(playlistRef);
mCallback.playlistRemoved(playlistRef);
// Errors aren't supported for now
return true;
}
/**
* Renames a playlist
* @param playlistRef The reference of the playlist
* @param title The new title of the playlist
* @return true if success
*/
public boolean renamePlaylist(String playlistRef, String title) {
long id = getPlaylistId(playlistRef);
ContentValues values = new ContentValues();
String where = MediaStore.Audio.Playlists._ID + " =? ";
String[] whereVal = { Long.toString(id) };
values.put(MediaStore.Audio.Playlists.NAME, title);
mContentResolver.update(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, values, where, whereVal);
Playlist playlist = mPlaylists.get(playlistRef);
playlist.setName(title);
mCallback.playlistUpdated(playlist);
// Errors aren't supported for now
return true;
}
/**
* Delete a song from a playlist
* **Warning** the delete use the song's id and not the song's position for now, so all duplicate will also be deleted
*
* @param songPosition the position of the song in the playlist
* @param playlistRef the reference of the playlist
* @return true if the song has been deleted
*/
public boolean deleteSongFromPlaylist(int songPosition, String playlistRef) {
long id = getPlaylistId(playlistRef);
Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", id);
// We update the playlist
Playlist pl = getPlaylist(playlistRef);
pl.removeSong(songPosition);
mPlaylists.put(playlistRef, pl);
// We update the app
mCallback.playlistUpdated(pl);
LocalSong loc = getLocalSong(pl.songsList().get(songPosition));//we need the LocalSong to get the song id
//See warning: all song with the same AUDIO_ID will be deleted
int rowsDeleted = mContentResolver.delete(uri, MediaStore.Audio.Playlists.Members.AUDIO_ID + "=" + loc.getId(), null);
Log.d(TAG, "we delete song " + songPosition + " Rows deleted: " + rowsDeleted);
//the song have been deleted if rows have been deleted
return rowsDeleted > 0;
}
/**
* Adds a song to a playlist
*
* @param songRef the reference of the song
* @param playlistRef the reference of the playlist
* @return true if the song has been added
*/
public boolean addSongToPlaylist(String songRef, String playlistRef) {
//we update the local content
Playlist pl = getPlaylist(playlistRef);
long id = getPlaylistId(playlistRef);
Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", id);
ContentValues contentValues = new ContentValues();
String[] cols = new String[]{
"count(*)"
};
//we get the length of the playlist to set the PLAY_ORDER right
Cursor cur = mContentResolver.query(uri, cols, null, null, null);
if (cur != null) {
cur.moveToFirst();
final int base = cur.getInt(0);
cur.close();
contentValues.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, base);
contentValues.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, getLocalSong(songRef).getId());
uri = mContentResolver.insert(uri, contentValues);
if (uri != null) {
pl.addSong(songRef);
mPlaylists.put(playlistRef, pl);
//we update the app
mCallback.playlistUpdated(pl);
}
}
return uri != null;
}
/**
* Adds a new playlist
*
* @param playlistName the name of the playlist
* @return if the playlist has been added
*/
public String addPlaylist(String playlistName) {
// we set the new playlist locally
ContentValues mInserts = new ContentValues();
mInserts.put(MediaStore.Audio.Playlists.NAME, playlistName);
mInserts.put(MediaStore.Audio.Playlists.DATE_ADDED, System.currentTimeMillis());
mInserts.put(MediaStore.Audio.Playlists.DATE_MODIFIED, System.currentTimeMillis());
Uri uri = mContentResolver.insert(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, mInserts);
if (uri == null) {
return null;
} else {
String ref = PREFIX_PLAYLIST + getPlaylistUniqueName(Long.toString(ContentUris.parseId(uri)));
// we update the app
Playlist pl = new Playlist(ref);
pl.setName(playlistName);
pl.setIsLoaded(true);
mPlaylists.put(ref, pl);
mCallback.playlistUpdated(pl);
return ref;
}
}
/**
* Plays the given song
*
* @param ref the unique reference of the song
*/
public void playSong(String ref) {
// Pause the current song (if any), without notifying the app
pause(false);
mCurrentSong = null;
mChangeMusic = true;//we say that we change music
mCurrentSong = getLocalSong(ref);//we set the new song
synchronized (this) {
// We reset the decoder for this song
if (mDecoder != null) {
mDecoder.stop();
mDecoder.release();
}
getCurrentDecoder();
mPendingOutIndex = -1;
}
mPaused = false;
mIsEOS = false;
// we resume the decoder thread
synchronized (mAudioPushRunnable) {
mAudioPushRunnable.notify();
mPendingOutIndex = -1;
}
mCallback.songPlaying();
}
public void pause(boolean notify) {
if (mDecoder != null && !mPaused) {
mPaused = true;
if (notify) {
mCallback.songPaused();
}
}
}
public void resume() {
if (mDecoder != null && mPaused) {
mPaused = false;
if (mIsEOS && mCurrentSong != null) {
playSong(mCurrentSong.getSong().getRef());
} else if (mCurrentSong != null) {
mDecoder.flush();
synchronized (mAudioPushRunnable) {
mAudioPushRunnable.notifyAll();
}
mCallback.songPlaying();
}
}
}
/**
* Sets a new decoder if the music changed
*/
private void getCurrentDecoder() {
synchronized (this) {
if (mChangeMusic) {
mChangeMusic = false;
mExtractor = new MediaExtractor();
try {
mExtractor.setDataSource(mContext, mCurrentSong.getURI(), null);
} catch (Exception e) {
Log.d("LocalProvider", "Data source error", e);
return;
}
// if there is a track to play, use it as format info
if (mExtractor.getTrackCount() > 0) {
mFormat = mExtractor.getTrackFormat(0);
} else {
Log.e(TAG, "No track in the source file");
return;
}
// we setup the codec with the type we got
try {
mDecoder = MediaCodec.createDecoderByType(mFormat.getString(MediaFormat.KEY_MIME));
} catch (Exception e) {
// SDK > 19, an IOException might be thrown
Log.e(TAG, "Unable to create decoder", e);
return;
}
mDecoder.configure(mFormat, null, null, 0);
// get the sample rate to configure AudioTrack
Log.d(TAG, "Sample rate: " + mFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE));
mExtractor.selectTrack(0);
// we get the buffer information to get notified of changes
mInfo = new MediaCodec.BufferInfo();
// we start decoding
mDecoder.start();
mInputBuffers = mDecoder.getInputBuffers();
mOutputBuffers = mDecoder.getOutputBuffers();
}
}
}
public void seekTo(long timeMs) {
mExtractor.seekTo(timeMs * 1000, MediaExtractor.SEEK_TO_CLOSEST_SYNC);
}
private final Thread mAudioPushRunnable = new Thread() {
public void run() {
mIsEOS = false;
byte[] outArray = new byte[8192];
while (!isInterrupted()) {
synchronized (mAudioPushRunnable) {
if (mIsEOS || mCurrentSong == null || mPaused) {
try {
mAudioPushRunnable.wait();
} catch (InterruptedException e) {
Log.e(TAG, e.getMessage());
}
}
synchronized (LocalProvider.this) {
// if we did not finish the file
if (!mIsEOS && !mPaused && mDecoder != null) {
// Input decoding
int inIndex = -2;
try {
// Try to dequeue an output buffer (timeout 30ms)
inIndex = mDecoder.dequeueInputBuffer(TimeUnit.MILLISECONDS.toMicros(30));
} catch (IllegalStateException ignored) {
}
// if we have a buffer available
if (inIndex >= 0) {
// we get the buffer
ByteBuffer buffer = mInputBuffers[inIndex];
// we retrieve the current encoded sample size
int sampleSize;
try {
sampleSize = mExtractor.readSampleData(buffer, 0);
} catch (IllegalArgumentException e) {
Log.w(TAG, "Got illegal argument while reading sample data from buffer", e);
sampleSize = 0;
}
if (sampleSize < 0) {
// we are at the end of the file
mDecoder.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
mIsEOS = true;
mCallback.songFinished();
} else if (sampleSize > 0) {
//we queue the encoded sample in the decoder
try {
mDecoder.queueInputBuffer(inIndex, 0, sampleSize, 0, 0);
mExtractor.advance();
} catch (Exception e) {
Log.d(TAG, e.toString());
continue;
}
} else {
// We already error'd out when reading, so if we can't advance,
// assume song is EOS
try {
mExtractor.advance();
} catch(Exception e) {
mIsEOS = true;
mCallback.songFinished();
}
}
}
// Output processing
int outIndex = -1;
if (mPendingOutIndex >= 0) {
// We have a pending buffer, we'll dequeue later, but for now try
// to write the sample again
outIndex = MediaCodec.INFO_TRY_AGAIN_LATER;
} else {
try {
// Try to dequeue an output buffer (timeout 30ms)
outIndex = mDecoder.dequeueOutputBuffer(mInfo, TimeUnit.MILLISECONDS.toMicros(30));
} catch (IllegalStateException ignored) {
}
}
switch (outIndex) {//we act according to the decoder output
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
mOutputBuffers = mDecoder.getOutputBuffers();
break;
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
mFormat = mDecoder.getOutputFormat();
break;
case MediaCodec.INFO_TRY_AGAIN_LATER:
// No buffer is available. This might be due to the upstream
// app buffers being full and returning 0 on write operations.
// We ping it and retry to write what we had.
if (mPendingOutIndex >= 0) {
int written = mCallback.musicDelivery(outArray, mPendingSize,
mFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT),
mFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE));
if (written > 0 || written == -1) {
try {
// We don't need this buffer anymore
mDecoder.releaseOutputBuffer(mPendingOutIndex, true);
} catch (IllegalStateException ignored) {
}
mPendingOutIndex = -1;
}
}
break;
default:
ByteBuffer out = mOutputBuffers[outIndex];
int size = mInfo.size;
if (size > 0) {
if (outArray.length < mInfo.offset + mInfo.size) {
outArray = new byte[mInfo.offset + mInfo.size];
}
out.position(mInfo.offset);
out.limit(mInfo.offset + mInfo.size);
out.get(outArray, 0, mInfo.size);
// We deliver the array
int written = mCallback.musicDelivery(outArray, size,
mFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT),
mFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE));
if (written > 0) {
try {
// We don't need this buffer anymore
mDecoder.releaseOutputBuffer(outIndex, true);
} catch (IllegalStateException ignored) {
}
} else {
mPendingOutIndex = outIndex;
mPendingSize = mInfo.size;
}
}
break;
}
}
}
}
}
}
};
public void startSearch(final String query) {
Log.d(TAG, "Starting search for " + query);
final Thread searchThread = new Thread() {
public void run() {
mSearchResult = new SearchResult(query);
final List<String> songsList = new ArrayList<>();
final List<String> albumList = new ArrayList<>();
final List<String> playlistList = new ArrayList<>();
final List<String> artistList = new ArrayList<>();
final String queryUpper = query.toUpperCase();
for (LocalSong song : mSongs.values()) {
String title = song.getSong().getTitle();
if (title != null) {
if (title.equalsIgnoreCase(query)) {
songsList.add(0, song.getSong().getRef());
} else if (title.toUpperCase().contains(queryUpper)) {
songsList.add(song.getSong().getRef());
}
}
}
for (Album album : mAlbums.values()) {
String name = album.getName();
if (name != null) {
if (name.equalsIgnoreCase(query)) {
albumList.add(0, album.getRef());
} else if (name.toUpperCase().contains(queryUpper)) {
albumList.add(album.getRef());
}
}
}
for (Playlist playlist : mPlaylists.values()) {
String name = playlist.getName();
if (name != null) {
if (name.equalsIgnoreCase(query)) {
playlistList.add(0, playlist.getRef());
} else if (name.toUpperCase().contains(queryUpper)) {
playlistList.add(playlist.getRef());
}
}
}
for (Artist artist : mArtists.values()) {
String name = artist.getName();
if (name != null) {
if (name.equalsIgnoreCase(query)) {
artistList.add(0, artist.getRef());
} else if (name.toUpperCase().contains(queryUpper)) {
artistList.add(artist.getRef());
}
}
}
if (mSearchResult.getQuery().equals(query)) {
Log.d(TAG, "Sending result size: "
+ (songsList.size() + albumList.size() + artistList.size() + playlistList.size()));
mSearchResult.setSongsList(songsList);
mSearchResult.setAlbumsList(albumList);
mSearchResult.setArtistList(artistList);
mSearchResult.setPlaylistList(playlistList);
mCallback.searchFinished(mSearchResult);
} else {
Log.d(TAG, "Query results dumped - outdated");
}
}
};
searchThread.start();
}
/**
* A little class to store ids and retrieve uri of a song
*/
public static class LocalSong {
private Song mSong;
private long mId;
private long mAlbumId;
public LocalSong(Song song, long id, long albumId) {
mSong = song;
mId = id;
mAlbumId = albumId;
}
/**
* @return the id of the song
*/
public Long getId() {
return mId;
}
public Long getAlbumId() {
return mAlbumId;
}
/**
* @return the Song
*/
public Song getSong() {
return mSong;
}
/**
* @return the local URI of the song
*/
public Uri getURI() {
Uri uri = ContentUris.withAppendedId(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, mId);
Log.d("LocalProvider", "URI:" + uri.toString());
return uri;
}
}
/**
* Callback interface to communicate with the service
*/
public interface LocalCallback {
int musicDelivery(byte[] data, int frames, int channels, int sampleRate);
void artistUpdated(final Artist artist);
void albumUpdated(final Album album);
void songUpdated(final Song song);
void playlistUpdated(final Playlist playlist);
void playlistRemoved(final String playlistRef);
void genreUpdated(final Genre genre);
void searchFinished(final SearchResult searchResult);
void songFinished();
void songPaused();
void songPlaying();
}
}