diff --git a/src/ch/blinkenlights/android/medialibrary/MediaLibrary.java b/src/ch/blinkenlights/android/medialibrary/MediaLibrary.java new file mode 100644 index 00000000..088f301d --- /dev/null +++ b/src/ch/blinkenlights/android/medialibrary/MediaLibrary.java @@ -0,0 +1,384 @@ +/* + * Copyright (C) 2016 Adrian Ulrich + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package ch.blinkenlights.android.medialibrary; + +import android.content.Context; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.ContentObserver; +import android.provider.MediaStore; + +import java.util.ArrayList; +import java.io.File; + +public class MediaLibrary { + + public static final String TABLE_SONGS = "songs"; + public static final String TABLE_ALBUMS = "albums"; + public static final String TABLE_CONTRIBUTORS = "contributors"; + public static final String TABLE_CONTRIBUTORS_SONGS = "contributors_songs"; + public static final String TABLE_GENRES = "genres"; + public static final String TABLE_GENRES_SONGS = "genres_songs"; + public static final String TABLE_PLAYLISTS = "playlists"; + public static final String TABLE_PLAYLISTS_SONGS = "playlists_songs"; + public static final String VIEW_ARTISTS = "_artists"; + public static final String VIEW_ALBUMS_ARTISTS = "_albums_artists"; + public static final String VIEW_SONGS_ALBUMS_ARTISTS = "_songs_albums_artists"; + public static final String VIEW_PLAYLIST_SONGS = "_playlists_songs"; + + private static MediaLibraryBackend sBackend; + + private static MediaScanner sScanner; + + private static MediaLibraryBackend getBackend(Context context) { + if (sBackend == null) { + // -> unlikely +// synchronized(sLock) { + if (sBackend == null) { + sBackend = new MediaLibraryBackend(context); + + sScanner = new MediaScanner(sBackend); + File dir = new File("/storage"); +// sScanner.startScan(dir); + } +// } + } + return sBackend; + } + + + /** + * Perform a media query on the database, returns a cursor + * + * @param context the context to use + * @param table the table to query, one of MediaLibrary.TABLE_* + * @param projection the columns to returns in this query + * @param selection the selection (WHERE) to use + * @param selectionArgs arguments for the selection + * @param orderBy how the result should be sorted + */ + public static Cursor queryLibrary(Context context, String table, String[] projection, String selection, String[] selectionArgs, String orderBy) { + return getBackend(context).query(false, table, projection, selection, selectionArgs, null, null, orderBy, null); + } + + /** + * Removes a single song from the database + * + * @param context the context to use + * @param id the song id to delete + */ + public static void removeSong(Context context, long id) { + getBackend(context).delete(TABLE_SONGS, SongColumns._ID+"="+id, null); + } + + /** + * Updates the play or skipcount of a song + * + * @param context the context to use + * @param id the song id to update + * @return boolean true if song was played, false if skipped + */ + public static void updateSongPlayCounts(Context context, long id, boolean played) { + final String column = played ? MediaLibrary.SongColumns.PLAYCOUNT : MediaLibrary.SongColumns.SKIPCOUNT; + ContentValues v = new ContentValues(); + v.put(column, column+" + 1"); + getBackend(context).update(MediaLibrary.TABLE_SONGS, v, MediaLibrary.SongColumns._ID+"="+id, null, false); + } + + /** + * Creates a new empty playlist + * + * @param context the context to use + * @param name the name of the new playlist + * @return long the id of the created playlist, -1 on error + */ + public static long createPlaylist(Context context, String name) { + ContentValues v = new ContentValues(); + v.put(MediaLibrary.PlaylistColumns.NAME, name); + return getBackend(context).insert(MediaLibrary.TABLE_PLAYLISTS, null, v); + } + + /** + * Deletes a playlist and all of its child elements + * + * @param context the context to use + * @param id the playlist id to delete + * @return boolean true if the playlist was deleted + */ + public static boolean removePlaylist(Context context, long id) { + // first, wipe all songs + getBackend(context).delete(MediaLibrary.TABLE_PLAYLISTS_SONGS, MediaLibrary.PlaylistSongColumns.PLAYLIST_ID+"="+id, null); + int rows = getBackend(context).delete(MediaLibrary.TABLE_PLAYLISTS, MediaLibrary.PlaylistColumns._ID+"="+id, null); + return (rows > 0); + } + + /** + * Adds a batch of songs to a playlist + * + * @param context the context to use + * @param playlistId the id of the playlist parent + * @param ids an array list with the song ids to insert + * @return the number of added items + */ + public static int addToPlaylist(Context context, long playlistId, ArrayList ids) { + long pos = 0; + // First we need to get the position of the last item + String[] projection = { MediaLibrary.PlaylistSongColumns.POSITION }; + String selection = MediaLibrary.PlaylistSongColumns.PLAYLIST_ID+"="+playlistId; + String order = MediaLibrary.PlaylistSongColumns.POSITION; + Cursor cursor = queryLibrary(context, MediaLibrary.TABLE_PLAYLISTS_SONGS, projection, selection, null, order); + if (cursor.moveToLast()) + pos = cursor.getLong(0) + 1; + cursor.close(); + + ArrayList bulk = new ArrayList(); + for (Long id : ids) { + if (getBackend(context).isSongExisting(id) == false) + continue; + + ContentValues v = new ContentValues(); + v.put(MediaLibrary.PlaylistSongColumns.PLAYLIST_ID, playlistId); + v.put(MediaLibrary.PlaylistSongColumns.SONG_ID, id); + v.put(MediaLibrary.PlaylistSongColumns.POSITION, pos); + bulk.add(v); + pos++; + } + return getBackend(context).bulkInsert(MediaLibrary.TABLE_PLAYLISTS_SONGS, null, bulk); + } + + /** + * Registers a new content observer for the media library + * + * @param context the context to use + * @param observer the content observer we are going to call on changes + */ + public static void registerContentObserver(Context context, ContentObserver observer) { + getBackend(context).registerContentObserver(observer); + } + + /** + * Returns true if we are currently scanning for media + */ + public static boolean isScannerRunning(Context context) { + // FIXME: IMPLEMENT THIS + return false; + } + + /** + * Returns the 'key' of given string used for sorting and searching + * + * @param name the string to convert + * @return the the key of given name + */ + public static String keyFor(String name) { + return MediaStore.Audio.keyFor(name); + } + + // Columns of Song entries + public interface SongColumns { + /** + * The id of this song in the database + */ + public static final String _ID = "_id"; + /** + * The title of this song + */ + public static final String TITLE = "title"; + /** + * The sortable title of this song + */ + public static final String TITLE_SORT = "title_sort"; + /** + * The position in the album of this song + */ + public static final String SONG_NUMBER = "song_num"; + /** + * The album where this song belongs to + */ + public static final String ALBUM_ID = "album_id"; + /** + * How often the song was played + */ + public static final String PLAYCOUNT = "playcount"; + /** + * How often the song was skipped + */ + public static final String SKIPCOUNT = "skipcount"; + /** + * The duration of this song + */ + public static final String DURATION = "duration"; + /** + * The path to the music file + */ + public static final String PATH = "path"; + /** + * The mtime of this item + */ + public static final String MTIME = "mtime"; + } + + // Columns of Album entries + public interface AlbumColumns { + /** + * The id of this album in the database + */ + public static final String _ID = SongColumns._ID; + /** + * The title of this album + */ + public static final String ALBUM = "album"; + /** + * The sortable title of this album + */ + public static final String ALBUM_SORT = "album_sort"; + /** + * How many songs are on this album + */ + public static final String SONG_COUNT = "song_count"; + /** + * The disc number of this album + */ + public static final String DISC_NUMBER = "disc_num"; + /** + * The total amount of discs + */ + public static final String DISC_COUNT = "disc_count"; + /** + * The primary contributor / artist reference for this album + */ + public static final String PRIMARY_ARTIST_ID = "primary_artist_id"; + /** + * The year of this album + */ + public static final String YEAR = "year"; + /** + * The mtime of this item + */ + public static final String MTIME = "mtime"; + } + + // Columns of Contributors entries + public interface ContributorColumns { + /** + * The id of this contributor + */ + public static final String _ID = SongColumns._ID; + /** + * The name of this contributor + */ + public static final String _CONTRIBUTOR = "_contributor"; + /** + * The sortable title of this contributor + */ + public static final String _CONTRIBUTOR_SORT = "_contributor_sort"; + /** + * The mtime of this item + */ + public static final String MTIME = "mtime"; + /** + * ONLY IN VIEWS - the artist + */ + public static final String ARTIST = "artist"; + /** + * ONLY IN VIEWS - the artist_sort key + */ + public static final String ARTIST_SORT = "artist_sort"; + /** + * ONLY IN VIEWS - the artist id + */ + public static final String ARTIST_ID = "artist_id"; + } + + // Songs <-> Contributor mapping + public interface ContributorSongColumns { + /** + * The role of this entry + */ + public static final String ROLE = "role"; + /** + * the contirbutor id this maps to + */ + public static final String _CONTRIBUTOR_ID = "_contributor_id"; + /** + * the song this maps to + */ + public static final String SONG_ID = "song_id"; + } + + // Columns of Genres entries + public interface GenreColumns { + /** + * The id of this genre + */ + public static final String _ID = SongColumns._ID; + /** + * The name of this genre + */ + public static final String _GENRE = "_genre"; + /** + * The sortable title of this genre + */ + public static final String _GENRE_SORT = "_genre_sort"; + } + + // Songs <-> Contributor mapping + public interface GenreSongColumns { + /** + * the genre id this maps to + */ + public static final String _GENRE_ID = "_genre_id"; + /** + * the song this maps to + */ + public static final String SONG_ID = "song_id"; + } + + // Playlists + public interface PlaylistColumns { + /** + * The id of this playlist + */ + public static final String _ID = SongColumns._ID; + /** + * The name of this playlist + */ + public static final String NAME = "name"; + } + + // Song <-> Playlist mapping + public interface PlaylistSongColumns { + /** + * The ID of this entry + */ + public static final String _ID = SongColumns._ID; + /** + * The playlist this entry belongs to + */ + public static final String PLAYLIST_ID = "playlist_id"; + /** + * The song this entry references to + */ + public static final String SONG_ID = "song_id"; + /** + * The order attribute + */ + public static final String POSITION = "position"; + } + +} diff --git a/src/ch/blinkenlights/android/medialibrary/MediaLibraryBackend.java b/src/ch/blinkenlights/android/medialibrary/MediaLibraryBackend.java new file mode 100644 index 00000000..420df8c1 --- /dev/null +++ b/src/ch/blinkenlights/android/medialibrary/MediaLibraryBackend.java @@ -0,0 +1,347 @@ +/* + * Copyright (C) 2016 Adrian Ulrich + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package ch.blinkenlights.android.medialibrary; + +import android.content.Context; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.database.ContentObserver; +import android.database.DatabaseUtils; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.database.Cursor; +import android.util.Log; +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class MediaLibraryBackend extends SQLiteOpenHelper { + /** + * Enables or disables debugging + */ + private static final boolean DEBUG = true; + /** + * The database version we are using + */ + private static final int DATABASE_VERSION = 1; + /** + * on-disk file to store the database + */ + private static final String DATABASE_NAME = "media-library.db"; + /** + * The tag to use for log messages + */ + private static final String TAG = "VanillaMediaLibraryBackend"; + /** + * Regexp to detect genre queries which we can optimize + */ + private static final Pattern sQueryMatchGenreSearch = Pattern.compile("(^|.+ )"+MediaLibrary.GenreSongColumns._GENRE_ID+"=(\\d+)$"); + /** + * Regexp to detect costy artist_id queries which we can optimize + */ + private static final Pattern sQueryMatchArtistSearch = Pattern.compile("(^|.+ )"+MediaLibrary.ContributorColumns.ARTIST_ID+"=(\\d+)$"); + /** + * A list of registered content observers + */ + private ContentObserver mContentObserver; + + /** + * Constructor for the MediaLibraryBackend helper + * + * @param Context the context to use + */ + MediaLibraryBackend(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + /** + * Called when database does not exist + * + * @param dbh the writeable database handle + */ + @Override + public void onCreate(SQLiteDatabase dbh) { + MediaSchema.createDatabaseSchema(dbh); + } + + /** + * Called when the existing database + * schema is outdated + * + * @param dbh the writeable database handle + * @param oldVersion the current version in use + * @param newVersion the target version + */ + @Override + public void onUpgrade(SQLiteDatabase dbh, int oldVersion, int newVersion) { + } + + /** + * Returns true if given song id is already present in the library + * + * @param id the song id to query + * @return true if a song with given id exists + */ + public boolean isSongExisting(long id) { + long count = DatabaseUtils.queryNumEntries(getReadableDatabase(), MediaLibrary.TABLE_SONGS, MediaLibrary.SongColumns._ID+"=?", new String[]{""+id}); + return count != 0; + } + + /** + * Registers a new observer which we call on database changes + * + * @param observer the observer to register + */ + public void registerContentObserver(ContentObserver observer) { + if (mContentObserver == null) { + mContentObserver = observer; + } else { + throw new IllegalStateException("ContentObserver was already registered"); + } + } + + /** + * Sends a callback to the registered observer + */ + private void notifyObserver() { + if (mContentObserver != null) + mContentObserver.onChange(true); + } + + + /** + * Wrapper for SQLiteDatabse.delete() function + * + * @param table the table to delete data from + * @param whereClause the selection + * @param whereArgs arguments to selection + * @return the number of affected rows + */ + public int delete(String table, String whereClause, String[] whereArgs) { + SQLiteDatabase dbh = getWritableDatabase(); + int res = dbh.delete(table, whereClause, whereArgs); + if (res > 0) { + cleanOrphanedEntries(); + notifyObserver(); + } + return res; + } + + /** + * Wrapper for SQLiteDatabase.update() function + * + * @param table the table to update + * @param values the data to set / modify + * @param whereClause the selection + * @param whereArgs arguments to selection + * @param userVisible controls if we shall call notifyObserver() to refresh the UI + * @return the number of affected rows + */ + public int update (String table, ContentValues values, String whereClause, String[] whereArgs, boolean userVisible) { + SQLiteDatabase dbh = getWritableDatabase(); + int res = dbh.update(table, values, whereClause, whereArgs); + if (res > 0 && userVisible == true) { + // Note: we are not running notifyObserver for performance reasons here + // Code which changes relations should just delete + re-insert data + notifyObserver(); + } + return res; + } + + /** + * Wrapper for SQLiteDatabase.insert() function + * + * @param table the table to insert data to + * @param nullColumnHack android hackery (see SQLiteDatabase documentation) + * @param values the values to insert + */ + public long insert (String table, String nullColumnHack, ContentValues values) { + long result = -1; + try { + result = getWritableDatabase().insertOrThrow(table, nullColumnHack, values); + } catch (Exception e) { + // avoid logspam as done by insert() + } + + if (result != -1) + notifyObserver(); + return result; + } + + /** + * Wrapper for SQLiteDatabase.insert() function working in one transaction + * + * @param table the table to insert data to + * @param nullColumnHack android hackery (see SQLiteDatabase documentation) + * @param valuesList an array list of ContentValues to insert + * @return the number of inserted rows + */ + public int bulkInsert (String table, String nullColumnHack, ArrayList valuesList) { + SQLiteDatabase dbh = getWritableDatabase(); + + int count = 0; + + dbh.beginTransactionNonExclusive(); + try { + for(ContentValues values : valuesList) { + try { + long result = dbh.insertOrThrow(table, nullColumnHack, values); + if (result > 0) + count++; + } catch (Exception e) { + // avoid logspam + } + } + dbh.setTransactionSuccessful(); + } finally { + dbh.endTransaction(); + } + + if (count > 0) + notifyObserver(); + + return count; + } + + /** + * Wrappr for SQLiteDatabase.query() function + */ + public Cursor query (boolean distinct, String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit) { + + if (selection != null) { + if (MediaLibrary.VIEW_SONGS_ALBUMS_ARTISTS.equals(table)) { + // artist matches in the song-view are costy: try to give sqlite a hint + Matcher artistMatch = sQueryMatchArtistSearch.matcher(selection); + if (artistMatch.matches()) { + selection = artistMatch.group(1); + final String artistId = artistMatch.group(2); + + selection += MediaLibrary.SongColumns._ID+" IN (SELECT "+MediaLibrary.ContributorSongColumns.SONG_ID+" FROM "+MediaLibrary.TABLE_CONTRIBUTORS_SONGS+" WHERE " + + MediaLibrary.ContributorSongColumns.ROLE+"=0 AND "+MediaLibrary.ContributorSongColumns._CONTRIBUTOR_ID+"="+artistId+")"; + } + } + + // Genre queries are a special beast: 'optimize' all of them + Matcher genreMatch = sQueryMatchGenreSearch.matcher(selection); + if (genreMatch.matches()) { + selection = genreMatch.group(1); // keep the non-genre search part of the query + final String genreId = genreMatch.group(2); // and extract the searched genre id + final String songsQuery = buildSongIdFromGenreSelect(genreId); + + if(table.equals(MediaLibrary.VIEW_SONGS_ALBUMS_ARTISTS)) { + selection += MediaLibrary.SongColumns._ID+" IN ("+songsQuery+") "; + } + + if (table.equals(MediaLibrary.VIEW_ARTISTS)) { + selection += MediaLibrary.ContributorColumns.ARTIST_ID+" IN ("+ buildSongIdFromGenreSelect(MediaLibrary.ContributorColumns.ARTIST_ID, songsQuery)+") "; + } + + if (table.equals(MediaLibrary.VIEW_ALBUMS_ARTISTS)) { + selection += MediaLibrary.AlbumColumns._ID+" IN ("+ buildSongIdFromGenreSelect(MediaLibrary.SongColumns.ALBUM_ID, songsQuery)+") "; + } + } + } + + if (DEBUG) + debugQuery(distinct, table, columns, selection, selectionArgs, groupBy, having, orderBy, limit); + + Cursor cursor = getReadableDatabase().query(distinct, table, columns, selection, selectionArgs, groupBy, having, orderBy, limit); + if (cursor != null) { + // Hold on! This is not some kind of black magic - it makes '''sense''': + // SQLites count() performance is pretty poor, but most queries will call getCount() during their + // lifetime anyway - unfortunately this might happen in the main thread, causing some lag. + // Androids SQLite class caches the result of getCount() calls, so we are going to run it + // here as we are (hopefully!) in a background thread anyway. + cursor.getCount(); + } + return cursor; + } + + /** + * Returns a select query to get all songs from a genre + * + * @param genreId the id to query as a string + * @return an SQL string which should return song id's for the queried genre + */ + private String buildSongIdFromGenreSelect(String genreId) { + final String query = "SELECT "+MediaLibrary.GenreSongColumns.SONG_ID+" FROM "+MediaLibrary.TABLE_GENRES_SONGS+" WHERE " + +MediaLibrary.GenreSongColumns._GENRE_ID+"="+genreId+" GROUP BY "+MediaLibrary.GenreSongColumns.SONG_ID; + return query; + } + + /** + * Returns a select query to get artists or albums from a genre + * + * @param target the target to query + * @param genreSelect the select string generated by buildSongIdFromGenreSelect + * @return an SQL string + */ + private String buildSongIdFromGenreSelect(String target, String genreSelect) { + final String query = "SELECT "+target+" FROM "+MediaLibrary.VIEW_SONGS_ALBUMS_ARTISTS+" WHERE " + +MediaLibrary.SongColumns._ID+" IN ("+genreSelect+") GROUP BY "+target; + return query; + } + + /** + * Purges orphaned entries from the media library + */ + private void cleanOrphanedEntries() { + SQLiteDatabase dbh = getWritableDatabase(); + dbh.execSQL("DELETE FROM "+MediaLibrary.TABLE_ALBUMS+" WHERE "+MediaLibrary.AlbumColumns._ID+" NOT IN (SELECT "+MediaLibrary.SongColumns.ALBUM_ID+" FROM "+MediaLibrary.TABLE_SONGS+");"); + dbh.execSQL("DELETE FROM "+MediaLibrary.TABLE_GENRES_SONGS+" WHERE "+MediaLibrary.GenreSongColumns.SONG_ID+" NOT IN (SELECT "+MediaLibrary.SongColumns._ID+" FROM "+MediaLibrary.TABLE_SONGS+");"); + dbh.execSQL("DELETE FROM "+MediaLibrary.TABLE_GENRES+" WHERE "+MediaLibrary.GenreColumns._ID+" NOT IN (SELECT "+MediaLibrary.GenreSongColumns._GENRE_ID+" FROM "+MediaLibrary.TABLE_GENRES_SONGS+");"); + dbh.execSQL("DELETE FROM "+MediaLibrary.TABLE_CONTRIBUTORS_SONGS+" WHERE "+MediaLibrary.ContributorSongColumns.SONG_ID+" NOT IN (SELECT "+MediaLibrary.SongColumns._ID+" FROM "+MediaLibrary.TABLE_SONGS+");"); + dbh.execSQL("DELETE FROM "+MediaLibrary.TABLE_CONTRIBUTORS+" WHERE "+MediaLibrary.ContributorColumns._ID+" NOT IN (SELECT "+MediaLibrary.ContributorSongColumns._CONTRIBUTOR_ID+" FROM "+MediaLibrary.TABLE_CONTRIBUTORS_SONGS+");"); + } + + /** + * Debug function to print and benchmark queries + */ + private void debugQuery(boolean distinct, String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit) { + final String LT = "VanillaMusicSQL"; + Log.v(LT, "---- start query ---"); + Log.v(LT, "SELECT"); + for (String c : columns) { + Log.v(LT, " "+c); + } + Log.v(LT, "FROM "+table+" WHERE "+selection+" "); + if (selectionArgs != null) { + Log.v(LT, " /* with args: "); + for (String a : selectionArgs) { + Log.v(LT, a+", "); + } + Log.v(LT, " */"); + } + Log.v(LT, " GROUP BY "+groupBy+" HAVING "+having+" ORDER BY "+orderBy+" LIMIT "+limit); + + Log.v(LT, "DBH = "+getReadableDatabase()); + + Cursor dryRun = getReadableDatabase().query(distinct, table, columns, selection, selectionArgs, groupBy, having, orderBy, limit); + long results = 0; + long startAt = System.currentTimeMillis(); + if (dryRun != null) { + while(dryRun.moveToNext()) { + results++; + } + } + dryRun.close(); + long tookMs = System.currentTimeMillis() - startAt; + Log.v(LT, "--- finished in "+tookMs+" ms with count="+results); + } + +} diff --git a/src/ch/blinkenlights/android/medialibrary/MediaScanner.java b/src/ch/blinkenlights/android/medialibrary/MediaScanner.java new file mode 100644 index 00000000..d89e4e1d --- /dev/null +++ b/src/ch/blinkenlights/android/medialibrary/MediaScanner.java @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2016 Adrian Ulrich + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package ch.blinkenlights.android.medialibrary; + +import ch.blinkenlights.bastp.Bastp; +import android.content.ContentValues; +import android.media.MediaMetadataRetriever; +import android.util.Log; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Message; +import android.os.Process; + +import java.io.File; +import java.util.HashMap; +import java.util.Vector; + +public class MediaScanner implements Handler.Callback { + /** + * The backend instance we are acting on + */ + private MediaLibraryBackend mBackend; + /** + * Our message handler + */ + private Handler mHandler; + + /** + * Constructs a new MediaScanner instance + * + * @param backend the backend to use + */ + public MediaScanner(MediaLibraryBackend backend) { + mBackend = backend; + HandlerThread handlerThread = new HandlerThread("MediaScannerThred", Process.THREAD_PRIORITY_LOWEST); + handlerThread.start(); + mHandler = new Handler(handlerThread.getLooper(), this); + } + + public void startScan(File dir) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_SCAN_DIRECTORY, 1, 0, dir)); + } + + private static final int MSG_SCAN_DIRECTORY = 1; + private static final int MSG_SCAN_FILE = 2; + @Override + public boolean handleMessage(Message message) { + File file = (File)message.obj; + switch (message.what) { + case MSG_SCAN_DIRECTORY: { + boolean recursive = (message.arg1 == 0 ? false : true); + scanDirectory(file, recursive); + break; + } + case MSG_SCAN_FILE: { + scanFile(file); + break; + } + default: { + throw new IllegalArgumentException(); + } + } + return true; + } + + + private void scanDirectory(File dir, boolean recursive) { + if (dir.isDirectory() == false) + return; + + File[] dirents = dir.listFiles(); + if (dirents == null) + return; + + for (File file : dirents) { + if (file.isFile()) { + Log.v("VanillaMusic", "MediaScanner: inspecting file "+file); + //scanFile(file); + mHandler.sendMessage(mHandler.obtainMessage(MSG_SCAN_FILE, 0, 0, file)); + } + else if (file.isDirectory() && recursive) { + Log.v("VanillaMusic", "MediaScanner: scanning subdir "+file); + //scanDirectory(file, recursive); + mHandler.sendMessage(mHandler.obtainMessage(MSG_SCAN_DIRECTORY, 1, 0, file)); + } + } + } + + private void scanFile(File file) { + String path = file.getAbsolutePath(); + long songId = hash63(path); + + HashMap tags = (new Bastp()).getTags(path); + if (tags.containsKey("type") == false) + return; // no tags found + +Log.v("VanillaMusic", "> Found mime "+((String)tags.get("type"))); + + if (mBackend.isSongExisting(songId)) { + Log.v("VanillaMusic", "Skipping already known song with id "+songId); + return; + } + + MediaMetadataRetriever data = new MediaMetadataRetriever(); + try { + data.setDataSource(path); + } catch (Exception e) { + Log.w("VanillaMusic", "Failed to extract metadata from " + path); + } + + String duration = data.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); + if (duration == null) + return; // not a supported media file! + + if (data.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO) == null) + return; // no audio -> do not index + + if (data.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO) != null) + return; // has a video stream -> do not index + + String title = (tags.containsKey("TITLE") ? (String)((Vector)tags.get("TITLE")).get(0) : "Untitled"); + String album = (tags.containsKey("ALBUM") ? (String)((Vector)tags.get("ALBUM")).get(0) : "No Album"); + String artist = (tags.containsKey("ARTIST") ? (String)((Vector)tags.get("ARTIST")).get(0) : "Unknown Artist"); + + String songnum = data.extractMetadata(MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER); + String composer = data.extractMetadata(MediaMetadataRetriever.METADATA_KEY_COMPOSER); + + long albumId = hash63(album); + long artistId = hash63(artist); + long composerId = hash63(composer); + + ContentValues v = new ContentValues(); + v.put(MediaLibrary.SongColumns._ID, songId); + v.put(MediaLibrary.SongColumns.TITLE, title); + v.put(MediaLibrary.SongColumns.TITLE_SORT, MediaLibrary.keyFor(title)); + v.put(MediaLibrary.SongColumns.ALBUM_ID, albumId); + v.put(MediaLibrary.SongColumns.DURATION, duration); + v.put(MediaLibrary.SongColumns.SONG_NUMBER,songnum); + v.put(MediaLibrary.SongColumns.PATH, path); + mBackend.insert(MediaLibrary.TABLE_SONGS, null, v); + + v.clear(); + v.put(MediaLibrary.AlbumColumns._ID, albumId); + v.put(MediaLibrary.AlbumColumns.ALBUM, album); + v.put(MediaLibrary.AlbumColumns.ALBUM_SORT, MediaLibrary.keyFor(album)); + v.put(MediaLibrary.AlbumColumns.PRIMARY_ARTIST_ID, artistId); + mBackend.insert(MediaLibrary.TABLE_ALBUMS, null, v); + + v.clear(); + v.put(MediaLibrary.ContributorColumns._ID, artistId); + v.put(MediaLibrary.ContributorColumns._CONTRIBUTOR, artist); + v.put(MediaLibrary.ContributorColumns._CONTRIBUTOR_SORT, MediaLibrary.keyFor(artist)); + mBackend.insert(MediaLibrary.TABLE_CONTRIBUTORS, null, v); + + v.clear(); + v.put(MediaLibrary.ContributorSongColumns._CONTRIBUTOR_ID, artistId); + v.put(MediaLibrary.ContributorSongColumns.SONG_ID, songId); + v.put(MediaLibrary.ContributorSongColumns.ROLE, 0); + mBackend.insert(MediaLibrary.TABLE_CONTRIBUTORS_SONGS, null, v); + + if (composer != null) { + v.clear(); + v.put(MediaLibrary.ContributorColumns._ID, composerId); + v.put(MediaLibrary.ContributorColumns._CONTRIBUTOR, composer); + v.put(MediaLibrary.ContributorColumns._CONTRIBUTOR_SORT, MediaLibrary.keyFor(composer)); + mBackend.insert(MediaLibrary.TABLE_CONTRIBUTORS, null, v); + + v.clear(); + v.put(MediaLibrary.ContributorSongColumns._CONTRIBUTOR_ID, composerId); + v.put(MediaLibrary.ContributorSongColumns.SONG_ID, songId); + v.put(MediaLibrary.ContributorSongColumns.ROLE, 1); + mBackend.insert(MediaLibrary.TABLE_CONTRIBUTORS_SONGS, null, v); + } + + if (tags.containsKey("GENRE")) { + Vector genres = (Vector)tags.get("GENRE"); + for (String genre : genres) { + long genreId = hash63(genre); + v.clear(); + v.put(MediaLibrary.GenreColumns._ID, genreId); + v.put(MediaLibrary.GenreColumns._GENRE, genre); + v.put(MediaLibrary.GenreColumns._GENRE_SORT, MediaLibrary.keyFor(genre)); + mBackend.insert(MediaLibrary.TABLE_GENRES, null, v); + + v.clear(); + v.put(MediaLibrary.GenreSongColumns._GENRE_ID, genreId); + v.put(MediaLibrary.GenreSongColumns.SONG_ID, songId); + mBackend.insert(MediaLibrary.TABLE_GENRES_SONGS, null, v); + } + } + + Log.v("VanillaMusic", "MediaScanner: inserted "+path); + } + + + /** + * Simple 63 bit hash function for strings + */ + private long hash63(String str) { + if (str == null) + return 0; + + long hash = 0; + int len = str.length(); + for (int i = 0; i < len ; i++) { + hash = 31*hash + str.charAt(i); + } + return (hash < 0 ? hash*-1 : hash); + } + + +} + diff --git a/src/ch/blinkenlights/android/medialibrary/MediaSchema.java b/src/ch/blinkenlights/android/medialibrary/MediaSchema.java new file mode 100644 index 00000000..866efc80 --- /dev/null +++ b/src/ch/blinkenlights/android/medialibrary/MediaSchema.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2016 Adrian Ulrich + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package ch.blinkenlights.android.medialibrary; + +import android.database.sqlite.SQLiteDatabase; + +public class MediaSchema { + /** + * SQL Schema of `songs' table + */ + private static final String DATABASE_CREATE_SONGS = "CREATE TABLE "+ MediaLibrary.TABLE_SONGS + " (" + + MediaLibrary.SongColumns._ID +" INTEGER PRIMARY KEY, " + + MediaLibrary.SongColumns.TITLE +" TEXT NOT NULL, " + + MediaLibrary.SongColumns.TITLE_SORT +" VARCHAR(64) NOT NULL, " + + MediaLibrary.SongColumns.SONG_NUMBER +" INTEGER, " + + MediaLibrary.SongColumns.ALBUM_ID +" INTEGER NOT NULL, " + + MediaLibrary.SongColumns.PLAYCOUNT +" INTEGER NOT NULL DEFAULT 0, " + + MediaLibrary.SongColumns.SKIPCOUNT +" INTEGER NOT NULL DEFAULT 0, " + + MediaLibrary.SongColumns.MTIME +" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, " + + MediaLibrary.SongColumns.DURATION +" INTEGER NOT NULL, " + + MediaLibrary.SongColumns.PATH +" VARCHAR(4096) NOT NULL " + + ");"; + + /** + * SQL Schema of `albums' table + */ + private static final String DATABASE_CREATE_ALBUMS = "CREATE TABLE "+ MediaLibrary.TABLE_ALBUMS + " (" + + MediaLibrary.AlbumColumns._ID +" INTEGER PRIMARY KEY, " + + MediaLibrary.AlbumColumns.ALBUM +" TEXT NOT NULL, " + + MediaLibrary.AlbumColumns.ALBUM_SORT +" VARCHAR(64) NOT NULL, " + + MediaLibrary.AlbumColumns.SONG_COUNT +" INTEGER, " + + MediaLibrary.AlbumColumns.DISC_NUMBER +" INTEGER, " + + MediaLibrary.AlbumColumns.DISC_COUNT +" INTEGER, " + + MediaLibrary.AlbumColumns.YEAR +" INTEGER, " + + MediaLibrary.AlbumColumns.PRIMARY_ARTIST_ID +" INTEGER NOT NULL DEFAULT 0, " + + MediaLibrary.AlbumColumns.MTIME +" TIMESTAMP DEFAULT CURRENT_TIMESTAMP " + + ");"; + + /** + * SQL Schema of `contributors' table + */ + private static final String DATABASE_CREATE_CONTRIBUTORS = "CREATE TABLE "+ MediaLibrary.TABLE_CONTRIBUTORS + " (" + + MediaLibrary.ContributorColumns._ID +" INTEGER PRIMARY KEY, " + + MediaLibrary.ContributorColumns._CONTRIBUTOR +" TEXT NOT NULL, " + + MediaLibrary.ContributorColumns._CONTRIBUTOR_SORT +" TEXT NOT NULL, " + + MediaLibrary.ContributorColumns.MTIME +" TIMESTAMP DEFAULT CURRENT_TIMESTAMP " + + ");"; + + /** + * SQL Schema of 'contributors<->songs' table + */ + private static final String DATABASE_CREATE_CONTRIBUTORS_SONGS = "CREATE TABLE "+ MediaLibrary.TABLE_CONTRIBUTORS_SONGS+ " (" + + MediaLibrary.ContributorSongColumns.ROLE +" INTEGER, " + + MediaLibrary.ContributorSongColumns._CONTRIBUTOR_ID +" INTEGER, " + + MediaLibrary.ContributorSongColumns.SONG_ID +" INTEGER, " + + "PRIMARY KEY("+MediaLibrary.ContributorSongColumns.ROLE+"," + +MediaLibrary.ContributorSongColumns._CONTRIBUTOR_ID+"," + +MediaLibrary.ContributorSongColumns.SONG_ID+") " + + ");"; + + /** + * song, role index on contributors_songs table + */ + private static final String INDEX_IDX_CONTRIBUTORS_SONGS = "CREATE INDEX idx_contributors_songs ON "+MediaLibrary.TABLE_CONTRIBUTORS_SONGS + +" ("+MediaLibrary.ContributorSongColumns.SONG_ID+", "+MediaLibrary.ContributorSongColumns.ROLE+")" + +";"; + + /** + * SQL Schema of `genres' table + */ + private static final String DATABASE_CREATE_GENRES = "CREATE TABLE "+ MediaLibrary.TABLE_GENRES + " (" + + MediaLibrary.GenreColumns._ID +" INTEGER PRIMARY KEY, " + + MediaLibrary.GenreColumns._GENRE +" TEXT NOT NULL, " + + MediaLibrary.GenreColumns._GENRE_SORT +" TEXT NOT NULL " + + ");"; + + /** + * SQL Schema of 'genres<->songs' table + */ + private static final String DATABASE_CREATE_GENRES_SONGS = "CREATE TABLE "+ MediaLibrary.TABLE_GENRES_SONGS + " (" + + MediaLibrary.GenreSongColumns._GENRE_ID +" INTEGER, " + + MediaLibrary.GenreSongColumns.SONG_ID +" INTEGER, " + + "PRIMARY KEY("+MediaLibrary.GenreSongColumns._GENRE_ID+"," + +MediaLibrary.GenreSongColumns.SONG_ID+") " + + ");"; + + /** + * SQL Schema for the playlists table + */ + private static final String DATABASE_CREATE_PLAYLISTS = "CREATE TABLE "+ MediaLibrary.TABLE_PLAYLISTS +" (" + + MediaLibrary.PlaylistColumns._ID +" INTEGER PRIMARY KEY, " + + MediaLibrary.PlaylistColumns.NAME +" TEXT NOT NULL " + + ");"; + + /** + * SQL Schema of 'songs<->playlists' table + */ + private static final String DATABASE_CREATE_PLAYLISTS_SONGS = "CREATE TABLE "+ MediaLibrary.TABLE_PLAYLISTS_SONGS + " (" + + MediaLibrary.PlaylistSongColumns._ID +" INTEGER PRIMARY KEY, " + + MediaLibrary.PlaylistSongColumns.PLAYLIST_ID +" INTEGER NOT NULL, " + + MediaLibrary.PlaylistSongColumns.SONG_ID +" INTEGER NOT NULL, " + + MediaLibrary.PlaylistSongColumns.POSITION +" INTEGER NOT NULL " + + ");"; + + /** + * Index to select a playlist quickly + */ + private static final String INDEX_IDX_PLAYLIST_ID = "CREATE INDEX idx_playlist_id ON "+MediaLibrary.TABLE_PLAYLISTS_SONGS + +" ("+MediaLibrary.PlaylistSongColumns.PLAYLIST_ID+")" + +";"; + + /** + * Additional columns to select for artist info + */ + private static final String VIEW_ARTIST_SELECT = "_artist."+MediaLibrary.ContributorColumns._CONTRIBUTOR+" AS "+MediaLibrary.ContributorColumns.ARTIST + +",_artist."+MediaLibrary.ContributorColumns._CONTRIBUTOR_SORT+" AS "+MediaLibrary.ContributorColumns.ARTIST_SORT + +",_artist."+MediaLibrary.ContributorColumns._ID+" AS "+MediaLibrary.ContributorColumns.ARTIST_ID; + + /** + * View which includes song, album and artist information + */ + private static final String VIEW_CREATE_SONGS_ALBUMS_ARTISTS = "CREATE VIEW "+ MediaLibrary.VIEW_SONGS_ALBUMS_ARTISTS+ " AS " + + "SELECT *, " + VIEW_ARTIST_SELECT + " FROM " + MediaLibrary.TABLE_SONGS + +" LEFT JOIN "+MediaLibrary.TABLE_ALBUMS+" ON "+MediaLibrary.TABLE_SONGS+"."+MediaLibrary.SongColumns.ALBUM_ID+" = "+MediaLibrary.TABLE_ALBUMS+"."+MediaLibrary.AlbumColumns._ID + +" LEFT JOIN "+MediaLibrary.TABLE_CONTRIBUTORS_SONGS+" ON "+MediaLibrary.TABLE_CONTRIBUTORS_SONGS+"."+MediaLibrary.ContributorSongColumns.ROLE+"=0 " + +" AND "+MediaLibrary.TABLE_CONTRIBUTORS_SONGS+"."+MediaLibrary.ContributorSongColumns.SONG_ID+" = "+MediaLibrary.TABLE_SONGS+"."+MediaLibrary.SongColumns._ID + +" LEFT JOIN "+MediaLibrary.TABLE_CONTRIBUTORS+" AS _artist ON _artist."+MediaLibrary.ContributorColumns._ID+" = "+MediaLibrary.TABLE_CONTRIBUTORS_SONGS+"."+MediaLibrary.ContributorSongColumns._CONTRIBUTOR_ID + +" ;"; + + /** + * View which includes album and artist information + */ + private static final String VIEW_CREATE_ALBUMS_ARTISTS = "CREATE VIEW "+ MediaLibrary.VIEW_ALBUMS_ARTISTS+ " AS " + + "SELECT *, " + VIEW_ARTIST_SELECT + " FROM " + MediaLibrary.TABLE_ALBUMS + +" LEFT JOIN "+MediaLibrary.TABLE_CONTRIBUTORS+" AS _artist" + +" ON _artist."+MediaLibrary.ContributorColumns._ID+" = "+MediaLibrary.TABLE_ALBUMS+"."+MediaLibrary.AlbumColumns.PRIMARY_ARTIST_ID + +" ;"; + + /** + * View which includes artist information + */ + private static final String VIEW_CREATE_ARTISTS = "CREATE VIEW "+ MediaLibrary.VIEW_ARTISTS+ " AS " + + "SELECT *, " + VIEW_ARTIST_SELECT + " FROM "+MediaLibrary.TABLE_CONTRIBUTORS+" AS _artist WHERE "+MediaLibrary.ContributorColumns._ID+" IN " + +" (SELECT "+MediaLibrary.ContributorSongColumns._CONTRIBUTOR_ID+" FROM "+MediaLibrary.TABLE_CONTRIBUTORS_SONGS + +" WHERE "+MediaLibrary.ContributorSongColumns.ROLE+"=0 GROUP BY "+MediaLibrary.ContributorSongColumns._CONTRIBUTOR_ID+")" + +" ;"; + + /** + * View like VIEW_CREATE_ARTISTS but includes playlist information + */ + private static final String VIEW_CREATE_PLAYLIST_SONGS = "CREATE VIEW "+ MediaLibrary.VIEW_PLAYLIST_SONGS+" AS " + + "SELECT *, " + VIEW_ARTIST_SELECT + " FROM " + MediaLibrary.TABLE_PLAYLISTS_SONGS + +" LEFT JOIN "+MediaLibrary.TABLE_SONGS+" ON "+MediaLibrary.TABLE_PLAYLISTS_SONGS+"."+MediaLibrary.PlaylistSongColumns.SONG_ID+"="+MediaLibrary.TABLE_SONGS+"."+MediaLibrary.SongColumns._ID + // -> same sql as VIEW_CREATE_SONGS_ALBUMS_ARTISTS follows: + +" LEFT JOIN "+MediaLibrary.TABLE_ALBUMS+" ON "+MediaLibrary.TABLE_SONGS+"."+MediaLibrary.SongColumns.ALBUM_ID+" = "+MediaLibrary.TABLE_ALBUMS+"."+MediaLibrary.AlbumColumns._ID + +" LEFT JOIN "+MediaLibrary.TABLE_CONTRIBUTORS_SONGS+" ON "+MediaLibrary.TABLE_CONTRIBUTORS_SONGS+"."+MediaLibrary.ContributorSongColumns.ROLE+"=0 " + +" AND "+MediaLibrary.TABLE_CONTRIBUTORS_SONGS+"."+MediaLibrary.ContributorSongColumns.SONG_ID+" = "+MediaLibrary.TABLE_SONGS+"."+MediaLibrary.SongColumns._ID + +" LEFT JOIN "+MediaLibrary.TABLE_CONTRIBUTORS+" AS _artist ON _artist."+MediaLibrary.ContributorColumns._ID+" = "+MediaLibrary.TABLE_CONTRIBUTORS_SONGS+"."+MediaLibrary.ContributorSongColumns._CONTRIBUTOR_ID + +" ;"; + + /** + * Creates a new database schema on dbh + * + * @param dbh the writeable dbh to act on + */ + public static void createDatabaseSchema(SQLiteDatabase dbh) { + dbh.execSQL(DATABASE_CREATE_SONGS); + dbh.execSQL(DATABASE_CREATE_ALBUMS); + dbh.execSQL(DATABASE_CREATE_CONTRIBUTORS); + dbh.execSQL(DATABASE_CREATE_CONTRIBUTORS_SONGS); + dbh.execSQL(INDEX_IDX_CONTRIBUTORS_SONGS); + dbh.execSQL(DATABASE_CREATE_GENRES); + dbh.execSQL(DATABASE_CREATE_GENRES_SONGS); + dbh.execSQL(DATABASE_CREATE_PLAYLISTS); + dbh.execSQL(DATABASE_CREATE_PLAYLISTS_SONGS); + dbh.execSQL(INDEX_IDX_PLAYLIST_ID); + dbh.execSQL(VIEW_CREATE_SONGS_ALBUMS_ARTISTS); + dbh.execSQL(VIEW_CREATE_ALBUMS_ARTISTS); + dbh.execSQL(VIEW_CREATE_ARTISTS); + dbh.execSQL(VIEW_CREATE_PLAYLIST_SONGS); + } + +} diff --git a/src/ch/blinkenlights/android/vanilla/AudioPickerActivity.java b/src/ch/blinkenlights/android/vanilla/AudioPickerActivity.java index cd46511a..7124633f 100644 --- a/src/ch/blinkenlights/android/vanilla/AudioPickerActivity.java +++ b/src/ch/blinkenlights/android/vanilla/AudioPickerActivity.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014-2015 Adrian Ulrich + * Copyright (C) 2014-2016 Adrian Ulrich * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,6 +22,7 @@ import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; +import android.provider.MediaStore; import android.view.View; import android.view.Window; import android.widget.TextView; @@ -124,10 +125,24 @@ public class AudioPickerActivity extends PlaybackActivity { Song song = new Song(-1); Cursor cursor = null; - if (uri.getScheme().equals("content")) - cursor = MediaUtils.queryResolver(getContentResolver(), uri, Song.FILLED_PROJECTION, null, null, null); - if (uri.getScheme().equals("file")) + if (uri.getScheme().equals("content")) { + // check if the native content resolver has a path for this + Cursor pathCursor = getContentResolver().query(uri, new String[]{ MediaStore.Audio.Media.DATA }, null, null, null); + if (pathCursor != null) { + if (pathCursor.moveToNext()) { + String mediaPath = pathCursor.getString(0); + if (mediaPath != null) { // this happens on android 4.x sometimes?! + QueryTask query = MediaUtils.buildFileQuery(mediaPath, Song.FILLED_PROJECTION); + cursor = query.runQuery(this); + } + } + pathCursor.close(); + } + } + + if (uri.getScheme().equals("file")) { cursor = MediaUtils.getCursorForFileQuery(uri.getPath()); + } if (cursor != null) { if (cursor.moveToNext()) { diff --git a/src/ch/blinkenlights/android/vanilla/FullPlaybackActivity.java b/src/ch/blinkenlights/android/vanilla/FullPlaybackActivity.java index 77a4660e..b2869267 100644 --- a/src/ch/blinkenlights/android/vanilla/FullPlaybackActivity.java +++ b/src/ch/blinkenlights/android/vanilla/FullPlaybackActivity.java @@ -340,7 +340,7 @@ public class FullPlaybackActivity extends SlidingPlaybackActivity PlaylistTask playlistTask = new PlaylistTask(playlistId, getString(R.string.playlist_favorites)); playlistTask.audioIds = new ArrayList(); playlistTask.audioIds.add(song.id); - int action = Playlist.isInPlaylist(getContentResolver(), playlistId, song) ? MSG_REMOVE_FROM_PLAYLIST : MSG_ADD_TO_PLAYLIST; + int action = Playlist.isInPlaylist(this, playlistId, song) ? MSG_REMOVE_FROM_PLAYLIST : MSG_ADD_TO_PLAYLIST; mHandler.sendMessage(mHandler.obtainMessage(action, playlistTask)); } break; @@ -616,7 +616,7 @@ public class FullPlaybackActivity extends SlidingPlaybackActivity case MSG_NOTIFY_PLAYLIST_CHANGED: // triggers a fav-refresh case MSG_LOAD_FAVOURITE_INFO: if (mCurrentSong != null) { - boolean found = Playlist.isInPlaylist(getContentResolver(), Playlist.getFavoritesId(this, false), mCurrentSong); + boolean found = Playlist.isInPlaylist(this, Playlist.getFavoritesId(this, false), mCurrentSong); mUiHandler.sendMessage(mUiHandler.obtainMessage(MSG_COMMIT_FAVOURITE_INFO, found)); } break; diff --git a/src/ch/blinkenlights/android/vanilla/LazyCoverView.java b/src/ch/blinkenlights/android/vanilla/LazyCoverView.java index b5ddbc4e..e393eb34 100644 --- a/src/ch/blinkenlights/android/vanilla/LazyCoverView.java +++ b/src/ch/blinkenlights/android/vanilla/LazyCoverView.java @@ -137,7 +137,7 @@ public class LazyCoverView extends ImageView if (bitmap == null) { if (payload.key.mediaType == MediaUtils.TYPE_ALBUM) { // We only display real covers for queries using the album id as key - Song song = MediaUtils.getSongByTypeId(mContext.getContentResolver(), payload.key.mediaType, payload.key.mediaId); + Song song = MediaUtils.getSongByTypeId(mContext, payload.key.mediaType, payload.key.mediaId); if (song != null) { bitmap = song.getSmallCover(mContext); } diff --git a/src/ch/blinkenlights/android/vanilla/LibraryActivity.java b/src/ch/blinkenlights/android/vanilla/LibraryActivity.java index 0121b475..73f17bb7 100644 --- a/src/ch/blinkenlights/android/vanilla/LibraryActivity.java +++ b/src/ch/blinkenlights/android/vanilla/LibraryActivity.java @@ -23,6 +23,8 @@ package ch.blinkenlights.android.vanilla; +import ch.blinkenlights.android.medialibrary.MediaLibrary; + import android.app.AlertDialog; import android.content.ContentResolver; import android.content.DialogInterface; @@ -547,7 +549,7 @@ public class LibraryActivity /** * Set a new limiter of the given type built from the first - * MediaStore.Audio.Media row that matches the selection. + * MediaLibrary.VIEW_SONGS_ALBUMS_ARTISTS row that matches the selection. * * @param limiterType The type of limiter to create. Must be either * MediaUtils.TYPE_ARTIST or MediaUtils.TYPE_ALBUM. @@ -555,10 +557,10 @@ public class LibraryActivity */ private void setLimiter(int limiterType, String selection) { - ContentResolver resolver = getContentResolver(); - Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; - String[] projection = new String[] { MediaStore.Audio.Media.ARTIST_ID, MediaStore.Audio.Media.ALBUM_ID, MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ALBUM }; - Cursor cursor = MediaUtils.queryResolver(resolver, uri, projection, selection, null, null); + String[] projection = new String[] { MediaLibrary.ContributorColumns.ARTIST_ID, MediaLibrary.SongColumns.ALBUM_ID, MediaLibrary.ContributorColumns.ARTIST, MediaLibrary.AlbumColumns.ALBUM }; + QueryTask query = new QueryTask(MediaLibrary.VIEW_SONGS_ALBUMS_ARTISTS, projection, selection, null, null); + Cursor cursor = query.runQuery(getApplicationContext()); + if (cursor != null) { if (cursor.moveToNext()) { String[] fields; @@ -566,11 +568,11 @@ public class LibraryActivity switch (limiterType) { case MediaUtils.TYPE_ARTIST: fields = new String[] { cursor.getString(2) }; - data = String.format("artist_id=%d", cursor.getLong(0)); + data = String.format(MediaLibrary.ContributorColumns.ARTIST_ID+"=%d", cursor.getLong(0)); break; case MediaUtils.TYPE_ALBUM: fields = new String[] { cursor.getString(2), cursor.getString(3) }; - data = String.format("album_id=%d", cursor.getLong(1)); + data = String.format(MediaLibrary.SongColumns.ALBUM_ID+"=%d", cursor.getLong(1)); break; default: throw new IllegalArgumentException("setLimiter() does not support limiter type " + limiterType); diff --git a/src/ch/blinkenlights/android/vanilla/LibraryPagerAdapter.java b/src/ch/blinkenlights/android/vanilla/LibraryPagerAdapter.java index 6391cc4d..b5d1f2ff 100644 --- a/src/ch/blinkenlights/android/vanilla/LibraryPagerAdapter.java +++ b/src/ch/blinkenlights/android/vanilla/LibraryPagerAdapter.java @@ -214,7 +214,6 @@ public class LibraryPagerAdapter mUiHandler = new Handler(this); mWorkerHandler = new Handler(workerLooper, this); mCurrentPage = -1; - activity.getContentResolver().registerContentObserver(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, true, mPlaylistObserver); } /** diff --git a/src/ch/blinkenlights/android/vanilla/MediaAdapter.java b/src/ch/blinkenlights/android/vanilla/MediaAdapter.java index 5c6af109..cae3cb0e 100644 --- a/src/ch/blinkenlights/android/vanilla/MediaAdapter.java +++ b/src/ch/blinkenlights/android/vanilla/MediaAdapter.java @@ -23,6 +23,8 @@ package ch.blinkenlights.android.vanilla; +import ch.blinkenlights.android.medialibrary.MediaLibrary; + import android.content.Context; import android.content.Intent; import android.database.Cursor; @@ -48,6 +50,7 @@ import java.util.regex.Pattern; import java.util.ArrayList; import java.lang.StringBuilder; +import android.util.Log; /** * MediaAdapter provides an adapter backed by a MediaStore content provider. * It generates simple one- or two-line text views to display each media @@ -66,9 +69,6 @@ public class MediaAdapter , SectionIndexer { private static final Pattern SPACE_SPLIT = Pattern.compile("\\s+"); - - private static final String SORT_MAGIC_PLAYCOUNT = "__PLAYCOUNT_SORT"; - /** * The string to use for length==0 db fields */ @@ -96,9 +96,9 @@ public class MediaAdapter */ private final int mType; /** - * The URI of the content provider backing this adapter. + * The table / view to use for this query */ - private Uri mStore; + private String mSource; /** * The fields to use from the content provider. The last field will be * displayed in the MediaView, as will the first field if there are @@ -133,11 +133,6 @@ public class MediaAdapter * ASC or DESC as appropriate before being passed to the query. */ private String[] mAdapterSortValues; - /** - * Same as mAdapterSortValues, but describes the query to do if we - * are returning songs for a `foreign' adapter (which migt have different column names) - */ - private String[] mSongSortValues; /** * The index of the current of the current sort mode in mSortValues, or * the inverse of the index (in which case sort should be descending @@ -189,53 +184,51 @@ public class MediaAdapter switch (type) { case MediaUtils.TYPE_ARTIST: - mStore = MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI; - mFields = new String[] { MediaStore.Audio.Artists.ARTIST }; - mFieldKeys = new String[] { MediaStore.Audio.Artists.ARTIST_KEY }; - mSortEntries = new int[] { R.string.name, R.string.number_of_tracks }; - mAdapterSortValues = new String[] { "artist_key %1$s", "number_of_tracks %1$s,artist_key %1$s" }; - mSongSortValues = new String[] { "artist_key %1$s,track", "artist_key %1$s,track" /* cannot sort by number_of_tracks */ }; + mSource = MediaLibrary.VIEW_ARTISTS; + mFields = new String[] { MediaLibrary.ContributorColumns.ARTIST }; + mFieldKeys = new String[] { MediaLibrary.ContributorColumns.ARTIST_SORT }; + mSortEntries = new int[] { R.string.name, R.string.date_added }; + mAdapterSortValues = new String[] { MediaLibrary.ContributorColumns.ARTIST_SORT+" %1$s", MediaLibrary.ContributorColumns.MTIME+" %1$s" }; break; case MediaUtils.TYPE_ALBUM: - mStore = MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI; - mFields = new String[] { MediaStore.Audio.Albums.ALBUM, MediaStore.Audio.Albums.ARTIST }; - // Why is there no artist_key column constant in the album MediaStore? The column does seem to exist. - mFieldKeys = new String[] { MediaStore.Audio.Albums.ALBUM_KEY, "artist_key" }; - mSortEntries = new int[] { R.string.name, R.string.artist_album, R.string.artist_year_album, R.string.number_of_tracks, R.string.date_added }; - mAdapterSortValues = new String[] { "album_key %1$s", "artist_key %1$s,album_key %1$s", "artist_key %1$s,minyear %1$s,album_key %1$s", "numsongs %1$s,album_key %1$s", "_id %1$s" }; - mSongSortValues = new String[] { "album_key %1$s,track", "artist_key %1$s,album_key %1$s,track", "artist_key %1$s,year %1$s,album_key %1$s,track", "album_key %1$s,track", "album_id %1$s,track" }; + mSource = MediaLibrary.VIEW_ALBUMS_ARTISTS; + mFields = new String[] { MediaLibrary.AlbumColumns.ALBUM, MediaLibrary.ContributorColumns.ARTIST }; + mFieldKeys = new String[] { MediaLibrary.AlbumColumns.ALBUM_SORT, MediaLibrary.ContributorColumns.ARTIST_SORT }; + mSortEntries = new int[] { R.string.name, R.string.artist_album, R.string.year, R.string.date_added }; + mAdapterSortValues = new String[] { MediaLibrary.AlbumColumns.ALBUM_SORT+" %1$s", MediaLibrary.ContributorColumns.ARTIST_SORT+" %1$s,"+MediaLibrary.AlbumColumns.ALBUM_SORT+" %1$s", + MediaLibrary.AlbumColumns.YEAR+" %1$s", MediaLibrary.AlbumColumns.MTIME+" %1$s" }; break; case MediaUtils.TYPE_SONG: - mStore = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; - mFields = new String[] { MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.ALBUM, MediaStore.Audio.Media.ARTIST }; - mFieldKeys = new String[] { MediaStore.Audio.Media.TITLE_KEY, MediaStore.Audio.Media.ALBUM_KEY, MediaStore.Audio.Media.ARTIST_KEY }; - mSortEntries = new int[] { R.string.name, R.string.artist_album_track, R.string.artist_album_title, - R.string.artist_year_album, R.string.album_track, - R.string.year, R.string.date_added, R.string.song_playcount }; - mAdapterSortValues = new String[] { "title_key %1$s", "artist_key %1$s,album_key %1$s,track", "artist_key %1$s,album_key %1$s,title_key %1$s", - "artist_key %1$s,year %1$s,album_key %1$s,track", "album_key %1$s,track", - "year %1$s,title_key %1$s","_id %1$s", SORT_MAGIC_PLAYCOUNT }; - mSongSortValues = mAdapterSortValues; + mSource = MediaLibrary.VIEW_SONGS_ALBUMS_ARTISTS; + mFields = new String[] { MediaLibrary.SongColumns.TITLE, MediaLibrary.AlbumColumns.ALBUM, MediaLibrary.ContributorColumns.ARTIST }; + mFieldKeys = new String[] { MediaLibrary.SongColumns.TITLE_SORT, MediaLibrary.AlbumColumns.ALBUM_SORT, MediaLibrary.ContributorColumns.ARTIST_SORT }; + mSortEntries = new int[] { R.string.name, R.string.artist_album_track, R.string.artist_album_title, R.string.album_track, R.string.year, R.string.date_added, R.string.song_playcount }; + mAdapterSortValues = new String[] { MediaLibrary.SongColumns.TITLE_SORT+" %1$s", + MediaLibrary.ContributorColumns.ARTIST_SORT+" %1$s,"+MediaLibrary.AlbumColumns.ALBUM_SORT+" %1$s,"+MediaLibrary.AlbumColumns.DISC_NUMBER+","+MediaLibrary.SongColumns.SONG_NUMBER, + MediaLibrary.ContributorColumns.ARTIST_SORT+" %1$s,"+MediaLibrary.AlbumColumns.ALBUM_SORT+" %1$s,"+MediaLibrary.SongColumns.TITLE_SORT+" %1$s", + MediaLibrary.AlbumColumns.ALBUM_SORT+" %1$s,"+MediaLibrary.AlbumColumns.DISC_NUMBER+","+MediaLibrary.SongColumns.SONG_NUMBER, + MediaLibrary.AlbumColumns.YEAR+" %1$s,"+MediaLibrary.AlbumColumns.ALBUM_SORT+" %1$s,"+MediaLibrary.AlbumColumns.DISC_NUMBER+","+MediaLibrary.SongColumns.SONG_NUMBER, + MediaLibrary.SongColumns.MTIME+" %1$s", + MediaLibrary.SongColumns.PLAYCOUNT+" %1$s", + }; // Songs covers are cached per-album mCoverCacheType = MediaUtils.TYPE_ALBUM; coverCacheKey = MediaStore.Audio.Albums.ALBUM_ID; break; case MediaUtils.TYPE_PLAYLIST: - mStore = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI; - mFields = new String[] { MediaStore.Audio.Playlists.NAME }; + mSource = MediaLibrary.TABLE_PLAYLISTS; + mFields = new String[] { MediaLibrary.PlaylistColumns.NAME }; mFieldKeys = null; mSortEntries = new int[] { R.string.name, R.string.date_added }; - mAdapterSortValues = new String[] { "name %1$s", "date_added %1$s" }; - mSongSortValues = null; + mAdapterSortValues = new String[] { MediaLibrary.PlaylistColumns.NAME+" %1$s", MediaLibrary.PlaylistColumns._ID+" %1$s" }; mExpandable = true; break; case MediaUtils.TYPE_GENRE: - mStore = MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI; - mFields = new String[] { MediaStore.Audio.Genres.NAME }; - mFieldKeys = null; + mSource = MediaLibrary.TABLE_GENRES; + mFields = new String[] { MediaLibrary.GenreColumns._GENRE }; + mFieldKeys = new String[] { MediaLibrary.GenreColumns._GENRE_SORT }; mSortEntries = new int[] { R.string.name }; - mAdapterSortValues = new String[] { "name %1$s" }; - mSongSortValues = null; + mAdapterSortValues = new String[] { MediaLibrary.GenreColumns._GENRE_SORT+" %1$s" }; break; default: throw new IllegalArgumentException("Invalid value for type: " + type); @@ -260,8 +253,8 @@ public class MediaAdapter private String getFirstSortColumn() { int mode = mSortMode < 0 ? ~mSortMode : mSortMode; // get current sort mode String column = SPACE_SPLIT.split(mAdapterSortValues[mode])[0]; - if(column.endsWith("_key")) { // we want human-readable string, not machine-composed - column = column.substring(0, column.length() - 4); + if(column.endsWith("_sort")) { // we want human-readable string, not machine-composed + column = column.substring(0, column.length() - 5); } return column; @@ -291,17 +284,23 @@ public class MediaAdapter * Build the query to be run with runQuery(). * * @param projection The columns to query. - * @param forceMusicCheck Force the is_music check to be added to the - * selection. + * @param returnSongs return songs instead of mType if true. */ - private QueryTask buildQuery(String[] projection, boolean returnSongs) - { + private QueryTask buildQuery(String[] projection, boolean returnSongs) { + Log.v("VanillaMusic", "constrain = "+mConstraint); + Log.v("VanillaMusic", "limiter = "+ (mLimiter == null ? "NULL" : mLimiter.data)); + Log.v("VanillaMusic", "sortMode = "+mSortMode); + + + String source = mSource; String constraint = mConstraint; Limiter limiter = mLimiter; StringBuilder selection = new StringBuilder(); String[] selectionArgs = null; + String[] enrichedProjection = projection; + // Assemble the sort string as requested by the user int mode = mSortMode; String sortDir; if (mode < 0) { @@ -311,47 +310,27 @@ public class MediaAdapter sortDir = "ASC"; } - // Use the song-sort mapping if we are returning songs - String sortStringRaw = (returnSongs ? mSongSortValues[mode] : mAdapterSortValues[mode]); - String[] enrichedProjection = projection; - - // Magic sort mode: sort by playcount - if (sortStringRaw == SORT_MAGIC_PLAYCOUNT) { - ArrayList topSongs = (new PlayCountsHelper(mContext)).getTopSongs(4096); - int sortWeight = -1 * topSongs.size(); // Sort mode is actually reversed (default: mostplayed -> leastplayed) - - StringBuilder sb = new StringBuilder("CASE WHEN _id=0 THEN 0"); // include dummy statement in initial string -> topSongs may be empty - for (Long id : topSongs) { - sb.append(" WHEN _id="+id+" THEN "+sortWeight); - sortWeight++; - } - sb.append(" ELSE 0 END %1s"); - sortStringRaw = sb.toString(); - } else if (returnSongs == false) { - // This is an 'adapter native' query: include the first sorting column - // in the projection to make it useable for the fast-scroller - enrichedProjection = Arrays.copyOf(projection, projection.length + 1); - enrichedProjection[projection.length] = getFirstSortColumn(); + // Fetch current sorting mode and sort by disc+track if we are going to look up the songs table + String sortRaw = mAdapterSortValues[mode]; + if (returnSongs) { + sortRaw += ", "+MediaLibrary.AlbumColumns.DISC_NUMBER+", "+MediaLibrary.SongColumns.SONG_NUMBER; } - String sort = String.format(sortStringRaw, sortDir); - - if (returnSongs || mType == MediaUtils.TYPE_SONG) - selection.append(MediaStore.Audio.Media.IS_MUSIC+" AND length(_data)"); + // ...and assemble the SQL string we are really going to use + String sort = String.format(sortRaw, sortDir); + // include the constraint (aka: search string) if any if (constraint != null && constraint.length() != 0) { String[] needles; String[] keySource; - // If we are using sorting keys, we need to change our constraint - // into a list of collation keys. Otherwise, just split the - // constraint with no modification. if (mFieldKeys != null) { - String colKey = MediaStore.Audio.keyFor(constraint); + String colKey = MediaLibrary.keyFor(constraint); String spaceColKey = DatabaseUtils.getCollationKey(" "); needles = colKey.split(spaceColKey); keySource = mFieldKeys; } else { + // only used for playlists, maybe we should just update the schema ? needles = SPACE_SPLIT.split(constraint); keySource = mFields; } @@ -379,31 +358,28 @@ public class MediaAdapter } } - QueryTask query; - if(mType == MediaUtils.TYPE_GENRE && !returnSongs) { - query = MediaUtils.buildGenreExcludeEmptyQuery(enrichedProjection, selection.toString(), - selectionArgs, sort); - } else if (limiter != null && limiter.type == MediaUtils.TYPE_GENRE) { - // Genre is not standard metadata for MediaStore.Audio.Media. - // We have to query it through a separate provider. : / - query = MediaUtils.buildGenreQuery((Long)limiter.data, enrichedProjection, selection.toString(), selectionArgs, sort, mType, returnSongs); - } else { - if (limiter != null) { - if (selection.length() != 0) - selection.append(" AND "); - selection.append(limiter.data); + if (limiter != null) { + if (selection.length() != 0) { + selection.append(" AND "); } - query = new QueryTask(mStore, enrichedProjection, selection.toString(), selectionArgs, sort); - if (returnSongs) // force query on song provider as we are requested to return songs - query.uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + selection.append(limiter.data); } + + if (returnSongs == true) { + source = MediaLibrary.VIEW_SONGS_ALBUMS_ARTISTS; + } else { + enrichedProjection = Arrays.copyOf(projection, projection.length + 1); + enrichedProjection[projection.length] = getFirstSortColumn(); + } + + QueryTask query = new QueryTask(source, enrichedProjection, selection.toString(), selectionArgs, sort); return query; } @Override public Cursor query() { - return buildQuery(mProjection, false).runQuery(mContext.getContentResolver()); + return buildQuery(mProjection, false).runQuery(mContext); } @Override @@ -467,15 +443,15 @@ public class MediaAdapter switch (mType) { case MediaUtils.TYPE_ARTIST: fields = new String[] { cursor.getString(2) }; - data = String.format("%s=%d", MediaStore.Audio.Media.ARTIST_ID, id); + data = String.format("%s=%d", MediaLibrary.ContributorColumns.ARTIST_ID, id); break; case MediaUtils.TYPE_ALBUM: fields = new String[] { cursor.getString(3), cursor.getString(2) }; - data = String.format("%s=%d", MediaStore.Audio.Media.ALBUM_ID, id); + data = String.format("%s=%d", MediaLibrary.SongColumns.ALBUM_ID, id); break; case MediaUtils.TYPE_GENRE: fields = new String[] { cursor.getString(2) }; - data = id; + data = String.format("%s=%d", MediaLibrary.GenreSongColumns._GENRE_ID, id); break; default: throw new IllegalStateException("getLimiter() is not supported for media type: " + mType); @@ -588,7 +564,7 @@ public class MediaAdapter */ public void setSortMode(int i) { - mSortMode = i; + mSortMode = (i < mSortEntries.length ? i : 0); } /** diff --git a/src/ch/blinkenlights/android/vanilla/MediaUtils.java b/src/ch/blinkenlights/android/vanilla/MediaUtils.java index 0462c005..b759fa9e 100644 --- a/src/ch/blinkenlights/android/vanilla/MediaUtils.java +++ b/src/ch/blinkenlights/android/vanilla/MediaUtils.java @@ -22,6 +22,8 @@ package ch.blinkenlights.android.vanilla; +import ch.blinkenlights.android.medialibrary.MediaLibrary; + import java.io.File; import java.util.ArrayList; import java.util.Collections; @@ -32,6 +34,8 @@ import java.util.Random; import java.util.Vector; import java.util.zip.CRC32; +import android.util.Log; + import junit.framework.Assert; import android.content.ActivityNotFoundException; @@ -89,19 +93,19 @@ public class MediaUtils { /** * The default sort order for media queries. First artist, then album, then - * track number. + * song number. */ - private static final String DEFAULT_SORT = "artist_key,album_key,track"; + private static final String DEFAULT_SORT = "artist_sort,album_sort,disc_num,song_num"; /** - * The default sort order for albums. First the album, then tracknumber + * The default sort order for albums. First the album, then songnumber */ - private static final String ALBUM_SORT = "album_key,track"; + private static final String ALBUM_SORT = "artist_sort,disc_num,song_num"; /** * The default sort order for files. Simply use the path */ - private static final String FILE_SORT = "_data"; + private static final String FILE_SORT = "path"; /** * Cached random instance. @@ -141,35 +145,36 @@ public class MediaUtils { */ private static QueryTask buildMediaQuery(int type, long id, String[] projection, String select) { - Uri media = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; StringBuilder selection = new StringBuilder(); String sort = DEFAULT_SORT; switch (type) { case TYPE_SONG: - selection.append(MediaStore.Audio.Media._ID); + selection.append(MediaLibrary.SongColumns._ID); break; case TYPE_ARTIST: - selection.append(MediaStore.Audio.Media.ARTIST_ID); + selection.append(MediaLibrary.ContributorColumns.ARTIST_ID); break; case TYPE_ALBUM: - selection.append(MediaStore.Audio.Media.ALBUM_ID); + selection.append(MediaLibrary.SongColumns.ALBUM_ID); sort = ALBUM_SORT; break; + case TYPE_GENRE: + selection.append(MediaLibrary.GenreSongColumns._GENRE_ID); + break; default: throw new IllegalArgumentException("Invalid type specified: " + type); } selection.append('='); selection.append(id); - selection.append(" AND length(_data) AND "+MediaStore.Audio.Media.IS_MUSIC); if (select != null) { selection.append(" AND "); selection.append(select); } - QueryTask result = new QueryTask(media, projection, selection.toString(), null, sort); + QueryTask result = new QueryTask(MediaLibrary.VIEW_SONGS_ALBUMS_ARTISTS, projection, selection.toString(), null, sort); result.type = type; return result; } @@ -180,139 +185,16 @@ public class MediaUtils { * * @param id The id of the playlist in MediaStore.Audio.Playlists. * @param projection The columns to query. - * @param selection The selection to pass to the query, or null. * @return The initialized query. */ - public static QueryTask buildPlaylistQuery(long id, String[] projection, String selection) - { - Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", id); - String sort = MediaStore.Audio.Playlists.Members.PLAY_ORDER; - QueryTask result = new QueryTask(uri, projection, selection, null, sort); + public static QueryTask buildPlaylistQuery(long id, String[] projection) { + String sort = MediaLibrary.PlaylistSongColumns.POSITION; + String selection = MediaLibrary.PlaylistSongColumns.PLAYLIST_ID+"="+id; + QueryTask result = new QueryTask(MediaLibrary.VIEW_PLAYLIST_SONGS, projection, selection, null, sort); result.type = TYPE_PLAYLIST; return result; } - /** - * Builds a query that will return all the songs in the genre with the - * given id. - * - * @param id The id of the genre in MediaStore.Audio.Genres. - * @param projection The columns to query. - * @param selection The selection to pass to the query, or null. - * @param selectionArgs The arguments to substitute into the selection. - * @param sort The sort order. - * @param type The media type to query and return - * @param returnSongs returns matching songs instead of `type' if true - */ - public static QueryTask buildGenreQuery(long id, String[] projection, String selection, String[] selectionArgs, String sort, int type, boolean returnSongs) - { - // Note: This function works on a raw sql query with way too much internal - // knowledge about the mediaProvider SQL table layout. Yes: it's ugly. - // The reason for this mess is that android has a very crippled genre implementation - // and does, for example, not allow us to query the albumbs beloging to a genre. - - Uri uri = MediaStore.Audio.Genres.Members.getContentUri("external", id); - String[] clonedProjection = projection.clone(); // we modify the projection, but this should not be visible to the caller - String sql = ""; - String authority = "audio"; - - if (type == TYPE_ARTIST) - authority = "artist_info"; - if (type == TYPE_ALBUM) - authority = "album_info"; - - // Our raw SQL query includes the album_info table (well: it's actually a view) - // which shares some columns with audio. - // This regexp should matche duplicate column names and forces them to use - // the audio table as a source - final String _FORCE_AUDIO_SRC = "(^|[ |,\\(])(_id|album(_\\w+)?|artist(_\\w+)?)"; - - // Prefix the SELECTed rows with the current table authority name - for (int i=0 ;i 0) - sql += " AND("+selection.replaceAll(_FORCE_AUDIO_SRC, "$1audio.$2")+")"; - - if (type == TYPE_ARTIST) - sql += " AND(artist_info._id = audio.artist_id)" + (returnSongs ? "" : " GROUP BY artist_info._id"); - - if (type == TYPE_ALBUM) - sql += " AND(album_info._id = audio.album_id)" + (returnSongs ? "" : " GROUP BY album_info._id"); - - if (sort != null && sort.length() > 0) - sql += " ORDER BY "+sort.replaceAll(_FORCE_AUDIO_SRC, "$1audio.$2"); - - // We are now turning this into an sql injection. Fun times. - clonedProjection[0] = sql +" --"; - - QueryTask result = new QueryTask(uri, clonedProjection, selection, selectionArgs, sort); - result.type = TYPE_GENRE; - return result; - } - - /** - * Creates a {@link QueryTask} for genres. The query will select only genres that have at least - * one song associated with them. - * - * @param projection The fields of the genre table that should be returned. - * @param selection Additional constraints for the query (added to the WHERE section). '?'s - * will be replaced by values in {@code selectionArgs}. Can be null. - * @param selectionArgs Arguments for {@code selection}. Can be null. See - * {@link android.content.ContentProvider#query(Uri, String[], String, String[], String)} - * @param sort How the returned genres should be sorted (added to the ORDER BY section) - * @return The QueryTask for the genres - */ - public static QueryTask buildGenreExcludeEmptyQuery(String[] projection, String selection, String[] selectionArgs, String sort) { - /* - * An example SQLite query that we're building in this function - SELECT DISTINCT _id, name - FROM audio_genres - WHERE - EXISTS( - SELECT audio_id, genre_id, audio._id - FROM audio_genres_map, audio - WHERE (genre_id == audio_genres._id) - AND (audio_id == audio._id)) - ORDER BY name DESC - */ - Uri uri = MediaStore.Audio.Genres.getContentUri("external"); - StringBuilder sql = new StringBuilder(); - // Don't want multiple identical genres - sql.append("DISTINCT "); - - // Add the projection fields to the query - sql.append(TextUtils.join(", ", projection)).append(' '); - - sql.append("FROM audio_genres "); - // Limit to genres that contain at least one valid song - sql.append("WHERE EXISTS( ") - .append("SELECT audio_id, genre_id, audio._id ") - .append("FROM audio_genres_map, audio ") - .append("WHERE (genre_id == audio_genres._id) AND (audio_id == audio._id) ") - .append(") "); - - if (!TextUtils.isEmpty(selection)) - sql.append(" AND(" + selection + ") "); - - if(!TextUtils.isEmpty(sort)) - sql.append(" ORDER BY ").append(sort); - - // Ignore the framework generated query - sql.append(" -- "); - String[] injectedProjection = new String[1]; - injectedProjection[0] = sql.toString(); - - // Don't pass the selection/sort as we've already added it to the query - return new QueryTask(uri, injectedProjection, null, selectionArgs, null); - } - /** * Builds a query with the given information. * @@ -329,11 +211,10 @@ public class MediaUtils { case TYPE_ARTIST: case TYPE_ALBUM: case TYPE_SONG: + case TYPE_GENRE: return buildMediaQuery(type, id, projection, selection); case TYPE_PLAYLIST: - return buildPlaylistQuery(id, projection, selection); - case TYPE_GENRE: - return buildGenreQuery(id, projection, selection, null, MediaStore.Audio.Genres.Members.TITLE_KEY, TYPE_SONG, true); + return buildPlaylistQuery(id, projection); default: throw new IllegalArgumentException("Specified type not valid: " + type); } @@ -343,15 +224,15 @@ public class MediaUtils { * Query the MediaStore to determine the id of the genre the song belongs * to. * - * @param resolver A ContentResolver to use. + * @param context The context to use * @param id The id of the song to query the genre for. */ - public static long queryGenreForSong(ContentResolver resolver, long id) - { - String[] projection = { "_id" }; - Uri uri = MediaStore.Audio.Genres.getContentUriForAudioId("external", (int)id); - Cursor cursor = queryResolver(resolver, uri, projection, null, null, null); + public static long queryGenreForSong(Context context, long id) { + String[] projection = { MediaLibrary.GenreSongColumns._GENRE_ID }; + String query = MediaLibrary.GenreSongColumns.SONG_ID+"=?"; + String[] queryArgs = new String[] { id+"" }; + Cursor cursor = MediaLibrary.queryLibrary(context, MediaLibrary.TABLE_GENRES_SONGS, projection, query, queryArgs, null); if (cursor != null) { if (cursor.moveToNext()) return cursor.getLong(0); @@ -379,7 +260,7 @@ public class MediaUtils { /** * Shuffle a Song list using Collections.shuffle(). * - * @param albumShuffle If true, preserve the order of tracks inside albums. + * @param albumShuffle If true, preserve the order of songs inside albums. */ public static void shuffle(List list, boolean albumShuffle) { @@ -429,17 +310,14 @@ public class MediaUtils { /** * Determine if any songs are available from the library. * - * @param resolver A ContentResolver to use. + * @param context The Context to use * @return True if it's possible to retrieve any songs, false otherwise. For * example, false could be returned if there are no songs in the library. */ - public static boolean isSongAvailable(ContentResolver resolver) - { + public static boolean isSongAvailable(Context context) { if (sSongCount == -1) { - Uri media = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; - String selection = MediaStore.Audio.Media.IS_MUSIC; - selection += " AND length(_data)"; - Cursor cursor = queryResolver(resolver, media, new String[]{"count(_id)"}, selection, null, null); + QueryTask query = new QueryTask(MediaLibrary.TABLE_SONGS, new String[]{"count(*)"}, null, null, null); + Cursor cursor = query.runQuery(context); if (cursor == null) { sSongCount = 0; } else { @@ -456,14 +334,11 @@ public class MediaUtils { * Returns a shuffled array contaning the ids of all the songs on the * device's library. * - * @param resolver A ContentResolver to use. + * @param context The Context to use */ - private static long[] queryAllSongs(ContentResolver resolver) - { - Uri media = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; - String selection = MediaStore.Audio.Media.IS_MUSIC; - selection += " AND length(_data)"; - Cursor cursor = queryResolver(resolver, media, Song.EMPTY_PROJECTION, selection, null, null); + private static long[] queryAllSongs(Context context) { + QueryTask query = new QueryTask(MediaLibrary.TABLE_SONGS, Song.EMPTY_PROJECTION, null, null, null); + Cursor cursor = query.runQuery(context); if (cursor == null || cursor.getCount() == 0) { sSongCount = 0; return null; @@ -485,29 +360,9 @@ public class MediaUtils { } /** - * Runs a query on the passed content resolver. - * Catches (and returns null on) SecurityException (= user revoked read permission) - * - * @param resolver The content resolver to use - * @param uri the uri to query - * @param projection the projection to use - * @param selection the selection to use - * @param selectionArgs arguments for the selection - * @param sortOrder sort order of the returned result - * - * @return a cursor or null + * Called if we detected a medium change + * This flushes some cached data */ - public static Cursor queryResolver(ContentResolver resolver, Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) - { - Cursor cursor = null; - try { - cursor = resolver.query(uri, projection, selection, selectionArgs, sortOrder); - } catch(java.lang.SecurityException e) { - // we do not have read permission - just return a null cursor - } - return cursor; - } - public static void onMediaChange() { sSongCount = -1; @@ -526,9 +381,8 @@ public class MediaUtils { return; } - ContentResolver resolver = ctx.getContentResolver(); - String[] projection = new String [] { MediaStore.Audio.Media._ID, MediaStore.Audio.Media.DATA }; - Cursor cursor = buildQuery(type, id, projection, null).runQuery(resolver); + String[] projection = new String [] { MediaLibrary.SongColumns._ID, MediaLibrary.SongColumns.PATH }; + Cursor cursor = buildQuery(type, id, projection, null).runQuery(ctx); if(cursor == null) { return; } @@ -555,10 +409,10 @@ public class MediaUtils { * @param type The MediaTye to query * @param id The id of given type to query */ - public static Song getSongByTypeId(ContentResolver resolver, int type, long id) { + public static Song getSongByTypeId(Context context, int type, long id) { Song song = new Song(-1); QueryTask query = buildQuery(type, id, Song.FILLED_PROJECTION, null); - Cursor cursor = query.runQuery(resolver); + Cursor cursor = query.runQuery(context); if (cursor != null) { if (cursor.getCount() > 0) { cursor.moveToPosition(0); @@ -573,14 +427,14 @@ public class MediaUtils { * Returns a song randomly selected from all the songs in the Android * MediaStore. * - * @param resolver A ContentResolver to use. + * @param context The Context to use */ - public static Song getRandomSong(ContentResolver resolver) + public static Song getRandomSong(Context context) { long[] songs = sAllSongs; if (songs == null) { - songs = queryAllSongs(resolver); + songs = queryAllSongs(context); if (songs == null) return null; sAllSongs = songs; @@ -590,7 +444,7 @@ public class MediaUtils { shuffle(sAllSongs); } - Song result = getSongByTypeId(resolver, MediaUtils.TYPE_SONG, sAllSongs[sAllSongsIdx]); + Song result = getSongByTypeId(context, MediaUtils.TYPE_SONG, sAllSongs[sAllSongsIdx]); result.flags |= Song.FLAG_RANDOM; sAllSongsIdx++; return result; @@ -670,11 +524,10 @@ public class MediaUtils { -> ended with a % for the LIKE query */ path = addDirEndSlash(sanitizeMediaPath(path)) + "%"; - final String query = "_data LIKE ? AND "+MediaStore.Audio.Media.IS_MUSIC; + final String query = MediaLibrary.SongColumns.PATH+" LIKE ?"; String[] qargs = { path }; - Uri media = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; - QueryTask result = new QueryTask(media, projection, query, qargs, FILE_SORT); + QueryTask result = new QueryTask(MediaLibrary.VIEW_SONGS_ALBUMS_ARTISTS, projection, query, qargs, FILE_SORT); result.type = TYPE_FILE; return result; } diff --git a/src/ch/blinkenlights/android/vanilla/MirrorLinkMediaBrowserService.java b/src/ch/blinkenlights/android/vanilla/MirrorLinkMediaBrowserService.java index 74088921..1eb63218 100644 --- a/src/ch/blinkenlights/android/vanilla/MirrorLinkMediaBrowserService.java +++ b/src/ch/blinkenlights/android/vanilla/MirrorLinkMediaBrowserService.java @@ -422,7 +422,6 @@ public class MirrorLinkMediaBrowserService extends MediaBrowserService try { Cursor cursor = adapter.query(); Context context = getApplicationContext(); - ContentResolver resolver = context.getContentResolver(); if (cursor == null) { return; @@ -436,7 +435,7 @@ public class MirrorLinkMediaBrowserService extends MediaBrowserService final String label = cursor.getString(2); long mediaId = Long.parseLong(id); - Song song = MediaUtils.getSongByTypeId(resolver, mediaType, mediaId); + Song song = MediaUtils.getSongByTypeId(context, mediaType, mediaId); MediaBrowser.MediaItem item = new MediaBrowser.MediaItem( new MediaDescription.Builder() .setMediaId(MediaID.toString(mediaType, mediaId, label)) diff --git a/src/ch/blinkenlights/android/vanilla/PlayCountsHelper.java b/src/ch/blinkenlights/android/vanilla/PlayCountsHelper.java index 11375d51..b96067f3 100644 --- a/src/ch/blinkenlights/android/vanilla/PlayCountsHelper.java +++ b/src/ch/blinkenlights/android/vanilla/PlayCountsHelper.java @@ -17,68 +17,23 @@ package ch.blinkenlights.android.vanilla; +import ch.blinkenlights.android.medialibrary.MediaLibrary; + import android.content.Context; -import android.content.ContentResolver; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; import android.database.Cursor; -import android.util.Log; import java.util.ArrayList; -public class PlayCountsHelper extends SQLiteOpenHelper { +public class PlayCountsHelper { - /** - * SQL constants and CREATE TABLE statements used by - * this java class - */ - private static final int DATABASE_VERSION = 2; - private static final String DATABASE_NAME = "playcounts.db"; - private static final String TABLE_PLAYCOUNTS = "playcounts"; - private static final String DATABASE_CREATE = "CREATE TABLE "+TABLE_PLAYCOUNTS + " (" - + "type INTEGER, " - + "type_id BIGINT, " - + "playcount INTEGER, " - + "skipcount INTEGER);"; - private static final String INDEX_UNIQUE_CREATE = "CREATE UNIQUE INDEX idx_uniq ON "+TABLE_PLAYCOUNTS - + " (type, type_id);"; - private static final String INDEX_TYPE_CREATE = "CREATE INDEX idx_type ON "+TABLE_PLAYCOUNTS - + " (type);"; - - private Context ctx; - - public PlayCountsHelper(Context context) { - super(context, DATABASE_NAME, null, DATABASE_VERSION); - ctx = context; - } - - @Override - public void onCreate(SQLiteDatabase dbh) { - dbh.execSQL(DATABASE_CREATE); - dbh.execSQL(INDEX_UNIQUE_CREATE); - dbh.execSQL(INDEX_TYPE_CREATE); - } - - @Override - public void onUpgrade(SQLiteDatabase dbh, int oldVersion, int newVersion) { - if (oldVersion < 2) { - dbh.execSQL("ALTER TABLE "+TABLE_PLAYCOUNTS+" ADD COLUMN skipcount INTEGER"); - dbh.execSQL("UPDATE "+TABLE_PLAYCOUNTS+" SET skipcount=0"); - } + public PlayCountsHelper() { } /** * Counts this song object as 'played' or 'skipped' */ - public void countSong(Song song, boolean played) { - long id = Song.getId(song); - final String column = played ? "playcount" : "skipcount"; - - SQLiteDatabase dbh = getWritableDatabase(); - dbh.execSQL("INSERT OR IGNORE INTO "+TABLE_PLAYCOUNTS+" (type, type_id, playcount, skipcount) VALUES ("+MediaUtils.TYPE_SONG+", "+id+", 0, 0);"); // Creates row if not exists - dbh.execSQL("UPDATE "+TABLE_PLAYCOUNTS+" SET "+column+"="+column+"+1 WHERE type="+MediaUtils.TYPE_SONG+" AND type_id="+id+";"); - dbh.close(); - - performGC(MediaUtils.TYPE_SONG); + public static void countSong(Context context, Song song, boolean played) { + final long id = Song.getId(song); + MediaLibrary.updateSongPlayCounts(context, id, played); } @@ -86,52 +41,15 @@ public class PlayCountsHelper extends SQLiteOpenHelper { /** * Returns a sorted array list of most often listen song ids */ - public ArrayList getTopSongs(int limit) { + public static ArrayList getTopSongs(Context context, int limit) { ArrayList payload = new ArrayList(); - SQLiteDatabase dbh = getReadableDatabase(); - - Cursor cursor = dbh.rawQuery("SELECT type_id FROM "+TABLE_PLAYCOUNTS+" WHERE type="+MediaUtils.TYPE_SONG+" AND playcount != 0 ORDER BY playcount DESC limit "+limit, null); - - while (cursor.moveToNext()) { + Cursor cursor = MediaLibrary.queryLibrary(context, MediaLibrary.TABLE_SONGS, new String[]{ MediaLibrary.SongColumns._ID }, MediaLibrary.SongColumns.PLAYCOUNT+" > 0", null, MediaLibrary.SongColumns.PLAYCOUNT+" DESC"); + while (cursor.moveToNext() && limit > 0) { payload.add(cursor.getLong(0)); + limit --; } - cursor.close(); - dbh.close(); return payload; } - /** - * Picks a random amount of 'type' items from the provided DBH - * and checks them against Androids media database. - * Items not found in the media library are removed from the DBH's database - */ - private int performGC(int type) { - SQLiteDatabase dbh = getWritableDatabase(); - ArrayList toCheck = new ArrayList(); // List of songs we are going to check - QueryTask query; // Reused query object - Cursor cursor; // recycled cursor - int removed = 0; // Amount of removed items - - // We are just grabbing a bunch of random IDs - cursor = dbh.rawQuery("SELECT type_id FROM "+TABLE_PLAYCOUNTS+" WHERE type="+type+" ORDER BY RANDOM() LIMIT 10", null); - while (cursor.moveToNext()) { - toCheck.add(cursor.getLong(0)); - } - cursor.close(); - - for (Long id : toCheck) { - query = MediaUtils.buildQuery(type, id, null, null); - cursor = query.runQuery(ctx.getContentResolver()); - if(cursor.getCount() == 0) { - dbh.execSQL("DELETE FROM "+TABLE_PLAYCOUNTS+" WHERE type="+type+" AND type_id="+id); - removed++; - } - cursor.close(); - } - Log.v("VanillaMusic", "performGC: items removed="+removed); - dbh.close(); - return removed; - } - } diff --git a/src/ch/blinkenlights/android/vanilla/PlaybackActivity.java b/src/ch/blinkenlights/android/vanilla/PlaybackActivity.java index 0a7a08b8..84b0bb3a 100644 --- a/src/ch/blinkenlights/android/vanilla/PlaybackActivity.java +++ b/src/ch/blinkenlights/android/vanilla/PlaybackActivity.java @@ -440,7 +440,7 @@ public abstract class PlaybackActivity extends Activity case MSG_CREATE_PLAYLIST: { PlaylistTask playlistTask = (PlaylistTask)message.obj; int nextAction = message.arg1; - long playlistId = Playlist.createPlaylist(getContentResolver(), playlistTask.name); + long playlistId = Playlist.createPlaylist(this, playlistTask.name); playlistTask.playlistId = playlistId; mHandler.sendMessage(mHandler.obtainMessage(nextAction, playlistTask)); break; @@ -498,11 +498,11 @@ public abstract class PlaybackActivity extends Activity int count = 0; if (playlistTask.query != null) { - count += Playlist.addToPlaylist(getContentResolver(), playlistTask.playlistId, playlistTask.query); + count += Playlist.addToPlaylist(this, playlistTask.playlistId, playlistTask.query); } if (playlistTask.audioIds != null) { - count += Playlist.addToPlaylist(getContentResolver(), playlistTask.playlistId, playlistTask.audioIds); + count += Playlist.addToPlaylist(this, playlistTask.playlistId, playlistTask.audioIds); } String message = getResources().getQuantityString(R.plurals.added_to_playlist, count, count, playlistTask.name); @@ -553,7 +553,7 @@ public abstract class PlaybackActivity extends Activity message = res.getString(R.string.delete_file_failed, file); } } else if (type == MediaUtils.TYPE_PLAYLIST) { - Playlist.deletePlaylist(getContentResolver(), id); + Playlist.deletePlaylist(this, id); } else { int count = PlaybackService.get(this).deleteMedia(type, id); message = res.getQuantityString(R.plurals.deleted, count, count); diff --git a/src/ch/blinkenlights/android/vanilla/PlaybackService.java b/src/ch/blinkenlights/android/vanilla/PlaybackService.java index bb5c38e5..8f5aab46 100644 --- a/src/ch/blinkenlights/android/vanilla/PlaybackService.java +++ b/src/ch/blinkenlights/android/vanilla/PlaybackService.java @@ -23,6 +23,8 @@ package ch.blinkenlights.android.vanilla; +import ch.blinkenlights.android.medialibrary.MediaLibrary; + import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; @@ -420,10 +422,6 @@ public final class PlaybackService extends Service * Reference to precreated BASTP Object */ private BastpUtil mBastpUtil; - /** - * Reference to Playcounts helper class - */ - private PlayCountsHelper mPlayCounts; @Override public void onCreate() @@ -435,8 +433,6 @@ public final class PlaybackService extends Service mTimeline.setCallback(this); int state = loadState(); - mPlayCounts = new PlayCountsHelper(this); - mMediaPlayer = getNewMediaPlayer(); mPreparedMediaPlayer = getNewMediaPlayer(); // We only have a single audio session @@ -488,7 +484,7 @@ public final class PlaybackService extends Service filter.addAction(Intent.ACTION_SCREEN_ON); registerReceiver(mReceiver, filter); - getContentResolver().registerContentObserver(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, true, mObserver); + MediaLibrary.registerContentObserver(getApplicationContext(), mObserver); mRemoteControlClient = new RemoteControl().getClient(this); mRemoteControlClient.initializeRemote(); @@ -1298,7 +1294,7 @@ public final class PlaybackService extends Service Song song = mTimeline.shiftCurrentSong(delta); mCurrentSong = song; if (song == null) { - if (MediaUtils.isSongAvailable(getContentResolver())) { + if (MediaUtils.isSongAvailable(getApplicationContext())) { int flag = finishAction(mState) == SongTimeline.FINISH_RANDOM ? FLAG_ERROR : FLAG_EMPTY_QUEUE; synchronized (mStateLock) { updateState((mState | flag) & ~FLAG_NO_MEDIA); @@ -1448,7 +1444,7 @@ public final class PlaybackService extends Service public void onMediaChange() { - if (MediaUtils.isSongAvailable(getContentResolver())) { + if (MediaUtils.isSongAvailable(getApplicationContext())) { if ((mState & FLAG_NO_MEDIA) != 0) setCurrentSong(0); } else { @@ -1569,15 +1565,15 @@ public final class PlaybackService extends Service case MSG_UPDATE_PLAYCOUNTS: Song song = (Song)message.obj; boolean played = message.arg1 == 1; - mPlayCounts.countSong(song, played); + PlayCountsHelper.countSong(getApplicationContext(), song, played); // Update the playcounts playlist in ~20% of all cases if enabled if (mAutoPlPlaycounts > 0 && Math.random() > 0.8) { ContentResolver resolver = getContentResolver(); // Add an invisible whitespace to adjust our sorting String playlistName = "\u200B"+getString(R.string.autoplaylist_playcounts_name, mAutoPlPlaycounts); - long id = Playlist.createPlaylist(resolver, playlistName); - ArrayList items = mPlayCounts.getTopSongs(mAutoPlPlaycounts); - Playlist.addToPlaylist(resolver, id, items); + long id = Playlist.createPlaylist(getApplicationContext(), playlistName); + ArrayList items = PlayCountsHelper.getTopSongs(getApplicationContext(), mAutoPlPlaycounts); + Playlist.addToPlaylist(getApplicationContext(), id, items); } @@ -1670,25 +1666,20 @@ public final class PlaybackService extends Service public int deleteMedia(int type, long id) { int count = 0; - - ContentResolver resolver = getContentResolver(); - String[] projection = new String [] { MediaStore.Audio.Media._ID, MediaStore.Audio.Media.DATA }; - Cursor cursor = MediaUtils.buildQuery(type, id, projection, null).runQuery(resolver); + String[] projection = new String [] { MediaLibrary.SongColumns._ID, MediaLibrary.SongColumns.PATH }; + Cursor cursor = MediaUtils.buildQuery(type, id, projection, null).runQuery(getApplicationContext()); if (cursor != null) { while (cursor.moveToNext()) { if (new File(cursor.getString(1)).delete()) { long songId = cursor.getLong(0); - String where = MediaStore.Audio.Media._ID + '=' + songId; - resolver.delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, where, null); + MediaLibrary.removeSong(getApplicationContext(), songId); mTimeline.removeSong(songId); ++count; } } - cursor.close(); } - return count; } @@ -1788,6 +1779,7 @@ public final class PlaybackService extends Service mHandler.sendMessage(mHandler.obtainMessage(MSG_QUERY, query)); } + /** * Enqueues all the songs with the same album/artist/genre as the passed * song. @@ -1813,7 +1805,7 @@ public final class PlaybackService extends Service id = song.albumId; break; case MediaUtils.TYPE_GENRE: - id = MediaUtils.queryGenreForSong(getContentResolver(), song.id); + id = MediaUtils.queryGenreForSong(getApplicationContext(), song.id); break; default: throw new IllegalArgumentException("Unsupported media type: " + type); diff --git a/src/ch/blinkenlights/android/vanilla/Playlist.java b/src/ch/blinkenlights/android/vanilla/Playlist.java index f7e8a230..4d349607 100644 --- a/src/ch/blinkenlights/android/vanilla/Playlist.java +++ b/src/ch/blinkenlights/android/vanilla/Playlist.java @@ -23,50 +23,49 @@ package ch.blinkenlights.android.vanilla; -import java.util.ArrayList; +import ch.blinkenlights.android.medialibrary.MediaLibrary; import android.content.Context; -import android.content.ContentResolver; -import android.content.ContentUris; import android.content.ContentValues; import android.database.Cursor; + +import java.util.ArrayList; + +import android.content.ContentResolver; +import android.content.ContentUris; import android.net.Uri; import android.provider.MediaStore; - /** * Provides various playlist-related utility functions. */ public class Playlist { /** - * Queries all the playlists known to the MediaStore. + * Queries all the playlists known to the MediaLibrary. * - * @param resolver A ContentResolver to use. + * @param context the context to use * @return The queried cursor. */ - public static Cursor queryPlaylists(ContentResolver resolver) - { - Uri media = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI; - String[] projection = { MediaStore.Audio.Playlists._ID, MediaStore.Audio.Playlists.NAME }; - String sort = MediaStore.Audio.Playlists.NAME; - return MediaUtils.queryResolver(resolver, media, projection, null, null, sort); + public static Cursor queryPlaylists(Context context) { + final String[] projection = { MediaLibrary.PlaylistColumns._ID, MediaLibrary.PlaylistColumns.NAME }; + final String sort = MediaStore.Audio.Playlists.NAME; + return MediaLibrary.queryLibrary(context, MediaLibrary.TABLE_PLAYLISTS, projection, null, null, sort); } /** * Retrieves the id for a playlist with the given name. * - * @param resolver A ContentResolver to use. + * @param context the context to use * @param name The name of the playlist. * @return The id of the playlist, or -1 if there is no playlist with the * given name. */ - public static long getPlaylist(ContentResolver resolver, String name) + public static long getPlaylist(Context context, String name) { long id = -1; - - Cursor cursor = MediaUtils.queryResolver(resolver, MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, - new String[] { MediaStore.Audio.Playlists._ID }, - MediaStore.Audio.Playlists.NAME + "=?", - new String[] { name }, null); + final String[] projection = { MediaLibrary.PlaylistColumns._ID }; + final String selection = MediaLibrary.PlaylistColumns.NAME+"=?"; + final String[] selectionArgs = { name }; + Cursor cursor = MediaLibrary.queryLibrary(context, MediaLibrary.TABLE_PLAYLISTS, projection, selection, selectionArgs, null); if (cursor != null) { if (cursor.moveToNext()) @@ -81,31 +80,17 @@ public class Playlist { * Create a new playlist with the given name. If a playlist with the given * name already exists, it will be overwritten. * - * @param resolver A ContentResolver to use. + * @param context the context to use * @param name The name of the playlist. * @return The id of the new playlist. */ - public static long createPlaylist(ContentResolver resolver, String name) + public static long createPlaylist(Context context, String name) { - long id = getPlaylist(resolver, name); - - if (id == -1) { - // We need to create a new playlist. - ContentValues values = new ContentValues(1); - values.put(MediaStore.Audio.Playlists.NAME, name); - Uri uri = resolver.insert(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, values); - /* Creating the playlist may fail due to race conditions or silly - * android bugs (i am looking at you, kitkat!). In this case, id will stay -1 - */ - if (uri != null) { - id = Long.parseLong(uri.getLastPathSegment()); - } - } else { - // We are overwriting an existing playlist. Clear existing songs. - Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", id); - resolver.delete(uri, null, null); - } + long id = getPlaylist(context, name); + if (id != -1) + deletePlaylist(context, id); + id = MediaLibrary.createPlaylist(context, name); return id; } @@ -113,59 +98,37 @@ public class Playlist { * Run the given query and add the results to the given playlist. Should be * run on a background thread. * - * @param resolver A ContentResolver to use. + * @param context the context to use * @param playlistId The MediaStore.Audio.Playlist id of the playlist to * modify. * @param query The query to run. The audio id should be the first column. * @return The number of songs that were added to the playlist. */ - public static int addToPlaylist(ContentResolver resolver, long playlistId, QueryTask query) { + public static int addToPlaylist(Context context, long playlistId, QueryTask query) { ArrayList result = new ArrayList(); - Cursor cursor = query.runQuery(resolver); + Cursor cursor = query.runQuery(context); if (cursor != null) { while (cursor.moveToNext()) { result.add(cursor.getLong(0)); } } - return addToPlaylist(resolver, playlistId, result); + return addToPlaylist(context, playlistId, result); } /** * Adds a set of audioIds to the given playlist. Should be * run on a background thread. * - * @param resolver A ContentResolver to use. + * @param context the context to use * @param playlistId The MediaStore.Audio.Playlist id of the playlist to * modify. * @param audioIds An ArrayList with all IDs to add * @return The number of songs that were added to the playlist. */ - public static int addToPlaylist(ContentResolver resolver, long playlistId, ArrayList audioIds) { + public static int addToPlaylist(Context context, long playlistId, ArrayList audioIds) { if (playlistId == -1) return 0; - - // Find the greatest PLAY_ORDER in the playlist - Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId); - String[] projection = new String[] { MediaStore.Audio.Playlists.Members.PLAY_ORDER }; - Cursor cursor = MediaUtils.queryResolver(resolver, uri, projection, null, null, null); - int base = 0; - if (cursor.moveToLast()) - base = cursor.getInt(0) + 1; - cursor.close(); - - int count = audioIds.size(); - if (count > 0) { - ContentValues[] values = new ContentValues[count]; - for (int i = 0; i != count; ++i) { - ContentValues value = new ContentValues(2); - value.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(base + i)); - value.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, audioIds.get(i)); - values[i] = value; - } - resolver.bulkInsert(uri, values); - } - - return count; + return MediaLibrary.addToPlaylist(context, playlistId, audioIds); } /** @@ -194,26 +157,13 @@ public class Playlist { /** * Delete the playlist with the given id. * - * @param resolver A ContentResolver to use. - * @param id The Media.Audio.Playlists id of the playlist. + * @param context the context to use + * @param id the id of the playlist. */ - public static void deletePlaylist(ContentResolver resolver, long id) - { - Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, id); - resolver.delete(uri, null, null); + public static void deletePlaylist(Context context, long id) { + MediaLibrary.removePlaylist(context, id); } - /** - * Copy content from one playlist to another - * - * @param resolver A ContentResolver to use. - * @param sourceId The Media.Audio.Playlists id of the source playlist - * @param destinationId The Media.Audio.Playlists id of the destination playlist - */ - private static void _copyToPlaylist(ContentResolver resolver, long sourceId, long destinationId) { - QueryTask query = MediaUtils.buildPlaylistQuery(sourceId, Song.FILLED_PLAYLIST_PROJECTION, null); - addToPlaylist(resolver, destinationId, query); - } /** * Rename the playlist with the given id. @@ -224,11 +174,14 @@ public class Playlist { */ public static void renamePlaylist(ContentResolver resolver, long id, String newName) { + /* + * FIXME: OBSOLETED CODE long newId = createPlaylist(resolver, newName); if (newId != -1) { // new playlist created -> move stuff over _copyToPlaylist(resolver, id, newId); deletePlaylist(resolver, id); } + */ } /** @@ -240,10 +193,10 @@ public class Playlist { */ public static long getFavoritesId(Context context, boolean create) { String playlistName = context.getString(R.string.playlist_favorites); - long playlistId = getPlaylist(context.getContentResolver(), playlistName); + long playlistId = getPlaylist(context, playlistName); if (playlistId == -1 && create == true) - playlistId = createPlaylist(context.getContentResolver(), playlistName); + playlistId = createPlaylist(context, playlistName); return playlistId; } @@ -251,19 +204,20 @@ public class Playlist { /** * Searches for given song in given playlist * - * @param resolver A ContentResolver to use. + * @param context the context to use * @param playlistId The ID of the Playlist to query * @param song The Song to search in given playlistId * @return true if `song' was found in `playlistId' */ - public static boolean isInPlaylist(ContentResolver resolver, long playlistId, Song song) { + public static boolean isInPlaylist(Context context, long playlistId, Song song) { if (playlistId == -1 || song == null) return false; boolean found = false; - String where = MediaStore.Audio.Playlists.Members.AUDIO_ID + "=" + song.id; - QueryTask query = MediaUtils.buildPlaylistQuery(playlistId, Song.EMPTY_PLAYLIST_PROJECTION, where); - Cursor cursor = query.runQuery(resolver); + String selection = MediaLibrary.PlaylistSongColumns.PLAYLIST_ID+"=? AND "+MediaLibrary.PlaylistSongColumns.SONG_ID+"=?"; + String[] selectionArgs = { ""+playlistId, ""+song.id }; + + Cursor cursor = MediaLibrary.queryLibrary(context, MediaLibrary.TABLE_PLAYLISTS_SONGS, Song.EMPTY_PLAYLIST_PROJECTION, selection, selectionArgs, null); if (cursor != null) { found = cursor.getCount() != 0; cursor.close(); diff --git a/src/ch/blinkenlights/android/vanilla/PlaylistActivity.java b/src/ch/blinkenlights/android/vanilla/PlaylistActivity.java index 0cdd28c6..96b73512 100644 --- a/src/ch/blinkenlights/android/vanilla/PlaylistActivity.java +++ b/src/ch/blinkenlights/android/vanilla/PlaylistActivity.java @@ -250,7 +250,7 @@ public class PlaylistActivity extends Activity } case LibraryActivity.ACTION_PLAY_ALL: case LibraryActivity.ACTION_ENQUEUE_ALL: { - QueryTask query = MediaUtils.buildPlaylistQuery(mPlaylistId, Song.FILLED_PLAYLIST_PROJECTION, null); + QueryTask query = MediaUtils.buildPlaylistQuery(mPlaylistId, Song.FILLED_PLAYLIST_PROJECTION); query.mode = MODE_FOR_ACTION[action]; query.data = position - mListView.getHeaderViewsCount(); PlaybackService.get(this).addSongs(query); @@ -275,7 +275,7 @@ public class PlaylistActivity extends Activity public void onClick(DialogInterface dialog, int which) { if (which == DialogInterface.BUTTON_POSITIVE) { - Playlist.deletePlaylist(getContentResolver(), mPlaylistId); + Playlist.deletePlaylist(this, mPlaylistId); finish(); } dialog.dismiss(); diff --git a/src/ch/blinkenlights/android/vanilla/PlaylistAdapter.java b/src/ch/blinkenlights/android/vanilla/PlaylistAdapter.java index 53792d8d..14a8d524 100644 --- a/src/ch/blinkenlights/android/vanilla/PlaylistAdapter.java +++ b/src/ch/blinkenlights/android/vanilla/PlaylistAdapter.java @@ -1,6 +1,6 @@ /* * Copyright (C) 2011 Christopher Eby - * Copyright (C) 2015 Adrian Ulrich + * Copyright (C) 2015-2016 Adrian Ulrich * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,6 +23,8 @@ package ch.blinkenlights.android.vanilla; +import ch.blinkenlights.android.medialibrary.MediaLibrary; + import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; @@ -38,19 +40,19 @@ import android.view.View; import android.view.ViewGroup; import android.widget.CursorAdapter; import android.widget.TextView; -import android.provider.MediaStore.Audio.Playlists.Members; /** * CursorAdapter backed by MediaStore playlists. */ public class PlaylistAdapter extends CursorAdapter implements Handler.Callback { + private static final String[] PROJECTION = new String[] { - MediaStore.Audio.Playlists.Members._ID, - MediaStore.Audio.Playlists.Members.TITLE, - MediaStore.Audio.Playlists.Members.ARTIST, - MediaStore.Audio.Playlists.Members.AUDIO_ID, - MediaStore.Audio.Playlists.Members.ALBUM_ID, - MediaStore.Audio.Playlists.Members.PLAY_ORDER, + MediaLibrary.PlaylistSongColumns._ID, + MediaLibrary.SongColumns.TITLE, + MediaLibrary.ContributorColumns.ARTIST, + MediaLibrary.PlaylistSongColumns.SONG_ID, + MediaLibrary.SongColumns.ALBUM_ID, + MediaLibrary.PlaylistSongColumns.POSITION, }; private final Context mContext; @@ -142,7 +144,7 @@ public class PlaylistAdapter extends CursorAdapter implements Handler.Callback { { switch (message.what) { case MSG_RUN_QUERY: { - Cursor cursor = runQuery(mContext.getContentResolver()); + Cursor cursor = runQuery(); mUiHandler.sendMessage(mUiHandler.obtainMessage(MSG_UPDATE_CURSOR, cursor)); break; } @@ -159,13 +161,12 @@ public class PlaylistAdapter extends CursorAdapter implements Handler.Callback { /** * Query the playlist songs. * - * @param resolver A ContentResolver to query with. * @return The resulting cursor. */ - private Cursor runQuery(ContentResolver resolver) + private Cursor runQuery() { - QueryTask query = MediaUtils.buildPlaylistQuery(mPlaylistId, PROJECTION, null); - return query.runQuery(resolver); + QueryTask query = MediaUtils.buildPlaylistQuery(mPlaylistId, PROJECTION); + return query.runQuery(mContext); } /** @@ -184,10 +185,8 @@ public class PlaylistAdapter extends CursorAdapter implements Handler.Callback { // this can happen when the adapter changes during the drag return; - // The Android API contains a method to move a playlist item, however, - // it has only been available since Froyo and doesn't seem to work - // after a song has been removed from the playlist (I think?). - +/* + * FIXME OBSOLETED ContentResolver resolver = mContext.getContentResolver(); Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", mPlaylistId); Cursor cursor = getCursor(); @@ -222,15 +221,16 @@ public class PlaylistAdapter extends CursorAdapter implements Handler.Callback { // insert the new rows resolver.bulkInsert(uri, values); - - changeCursor(runQuery(resolver)); +*/ + changeCursor(runQuery()); } public void removeItem(int position) { - ContentResolver resolver = mContext.getContentResolver(); - Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", mPlaylistId); - resolver.delete(ContentUris.withAppendedId(uri, getItemId(position)), null, null); + //ContentResolver resolver = mContext.getContentResolver(); + //Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", mPlaylistId); + //resolver.delete(ContentUris.withAppendedId(uri, getItemId(position)), null, null); + // FIXME OBSOLETED mUiHandler.sendEmptyMessage(MSG_RUN_QUERY); } diff --git a/src/ch/blinkenlights/android/vanilla/PlaylistDialog.java b/src/ch/blinkenlights/android/vanilla/PlaylistDialog.java index a37d5001..f4dfaaab 100644 --- a/src/ch/blinkenlights/android/vanilla/PlaylistDialog.java +++ b/src/ch/blinkenlights/android/vanilla/PlaylistDialog.java @@ -83,7 +83,7 @@ public class PlaylistDialog extends DialogFragment @Override public Dialog onCreateDialog(Bundle savedInstanceState) { - Cursor cursor = Playlist.queryPlaylists(getActivity().getContentResolver()); + Cursor cursor = Playlist.queryPlaylists(getActivity()); if (cursor == null) return null; diff --git a/src/ch/blinkenlights/android/vanilla/PlaylistInputDialog.java b/src/ch/blinkenlights/android/vanilla/PlaylistInputDialog.java index 81796c44..fd6604ed 100644 --- a/src/ch/blinkenlights/android/vanilla/PlaylistInputDialog.java +++ b/src/ch/blinkenlights/android/vanilla/PlaylistInputDialog.java @@ -128,8 +128,7 @@ public class PlaylistInputDialog extends DialogFragment mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(false); } else { mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(true); - ContentResolver resolver = getActivity().getContentResolver(); - int res = Playlist.getPlaylist(resolver, string) == -1 ? mActionRes : R.string.overwrite; + int res = Playlist.getPlaylist(getActivity(), string) == -1 ? mActionRes : R.string.overwrite; mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setText(res); } } diff --git a/src/ch/blinkenlights/android/vanilla/QueryTask.java b/src/ch/blinkenlights/android/vanilla/QueryTask.java index b0154573..0bef6f7d 100644 --- a/src/ch/blinkenlights/android/vanilla/QueryTask.java +++ b/src/ch/blinkenlights/android/vanilla/QueryTask.java @@ -1,36 +1,33 @@ /* - * Copyright (C) 2011 Christopher Eby + * Copyright (C) 2016 Adrian Ulrich * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . */ + package ch.blinkenlights.android.vanilla; -import android.content.ContentResolver; +import ch.blinkenlights.android.medialibrary.MediaLibrary; + +import android.content.Context; import android.database.Cursor; -import android.net.Uri; /** * Represents a pending query. */ public class QueryTask { - public Uri uri; + public final String table; public final String[] projection; public final String selection; public final String[] selectionArgs; @@ -57,9 +54,8 @@ public class QueryTask { * Create the tasks. All arguments are passed directly to * ContentResolver.query(). */ - public QueryTask(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) - { - this.uri = uri; + public QueryTask(String table, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + this.table = table; this.projection = projection; this.selection = selection; this.selectionArgs = selectionArgs; @@ -69,10 +65,9 @@ public class QueryTask { /** * Run the query. Should be called on a background thread. * - * @param resolver The ContentResolver to query with. + * @param context The Context to use */ - public Cursor runQuery(ContentResolver resolver) - { - return MediaUtils.queryResolver(resolver, uri, projection, selection, selectionArgs, sortOrder); + public Cursor runQuery(Context context) { + return MediaLibrary.queryLibrary(context, table, projection, selection, selectionArgs, sortOrder); } } diff --git a/src/ch/blinkenlights/android/vanilla/Song.java b/src/ch/blinkenlights/android/vanilla/Song.java index 1b2e7030..dceef747 100644 --- a/src/ch/blinkenlights/android/vanilla/Song.java +++ b/src/ch/blinkenlights/android/vanilla/Song.java @@ -22,6 +22,8 @@ package ch.blinkenlights.android.vanilla; +import ch.blinkenlights.android.medialibrary.MediaLibrary; + import android.content.Context; import android.database.Cursor; import android.graphics.Bitmap; @@ -47,36 +49,27 @@ public class Song implements Comparable { public static final String[] EMPTY_PROJECTION = { - MediaStore.Audio.Media._ID, + MediaLibrary.SongColumns._ID, }; public static final String[] FILLED_PROJECTION = { - MediaStore.Audio.Media._ID, - MediaStore.Audio.Media.DATA, - MediaStore.Audio.Media.TITLE, - MediaStore.Audio.Media.ALBUM, - MediaStore.Audio.Media.ARTIST, - MediaStore.Audio.Media.ALBUM_ID, - MediaStore.Audio.Media.ARTIST_ID, - MediaStore.Audio.Media.DURATION, - MediaStore.Audio.Media.TRACK, + MediaLibrary.SongColumns._ID, + MediaLibrary.SongColumns.PATH, + MediaLibrary.SongColumns.TITLE, + MediaLibrary.AlbumColumns.ALBUM, + MediaLibrary.ContributorColumns.ARTIST, + MediaLibrary.SongColumns.ALBUM_ID, + MediaLibrary.ContributorColumns.ARTIST_ID, + MediaLibrary.SongColumns.DURATION, + MediaLibrary.SongColumns.SONG_NUMBER, }; public static final String[] EMPTY_PLAYLIST_PROJECTION = { - MediaStore.Audio.Playlists.Members.AUDIO_ID, + MediaLibrary.PlaylistSongColumns.SONG_ID, }; - public static final String[] FILLED_PLAYLIST_PROJECTION = { - MediaStore.Audio.Playlists.Members.AUDIO_ID, - MediaStore.Audio.Playlists.Members.DATA, - MediaStore.Audio.Playlists.Members.TITLE, - MediaStore.Audio.Playlists.Members.ALBUM, - MediaStore.Audio.Playlists.Members.ARTIST, - MediaStore.Audio.Playlists.Members.ALBUM_ID, - MediaStore.Audio.Playlists.Members.ARTIST_ID, - MediaStore.Audio.Playlists.Members.DURATION, - MediaStore.Audio.Playlists.Members.TRACK, - }; + public static final String[] FILLED_PLAYLIST_PROJECTION = + FILLED_PROJECTION; // Same, as playlists are just a view of the view /** diff --git a/src/ch/blinkenlights/android/vanilla/SongTimeline.java b/src/ch/blinkenlights/android/vanilla/SongTimeline.java index 1649abbb..1beaedc3 100644 --- a/src/ch/blinkenlights/android/vanilla/SongTimeline.java +++ b/src/ch/blinkenlights/android/vanilla/SongTimeline.java @@ -23,6 +23,8 @@ package ch.blinkenlights.android.vanilla; +import ch.blinkenlights.android.medialibrary.MediaLibrary; + import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; @@ -326,7 +328,7 @@ public final class SongTimeline { // Fill the selection with the ids of all the saved songs // and initialize the timeline with unpopulated songs. - StringBuilder selection = new StringBuilder("_ID IN ("); + StringBuilder selection = new StringBuilder(MediaLibrary.SongColumns._ID+" IN ("); for (int i = 0; i != n; ++i) { long id = in.readLong(); if (id == -1) @@ -346,10 +348,8 @@ public final class SongTimeline { // return its results in. Collections.sort(songs, new IdComparator()); - ContentResolver resolver = mContext.getContentResolver(); - Uri media = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; - - Cursor cursor = MediaUtils.queryResolver(resolver, media, Song.FILLED_PROJECTION, selection.toString(), null, "_id"); + QueryTask query = new QueryTask(MediaLibrary.VIEW_SONGS_ALBUMS_ARTISTS, Song.FILLED_PROJECTION, selection.toString(), null, MediaLibrary.SongColumns._ID); + Cursor cursor = query.runQuery(mContext); if (cursor != null) { if (cursor.getCount() != 0) { cursor.moveToNext(); @@ -576,7 +576,7 @@ public final class SongTimeline { return null; } else if (pos == size) { if (mFinishAction == FINISH_RANDOM) { - song = MediaUtils.getRandomSong(mContext.getContentResolver()); + song = MediaUtils.getRandomSong(mContext); if (song == null) return null; timeline.add(song); @@ -700,7 +700,7 @@ public final class SongTimeline { */ public int addSongs(Context context, QueryTask query) { - Cursor cursor = query.runQuery(context.getContentResolver()); + Cursor cursor = query.runQuery(context); if (cursor == null) { return 0; }