initial import of our own media library database

This commit is contained in:
Adrian Ulrich 2016-11-21 11:38:43 +01:00
parent 1e1fe4a375
commit 4e1101b2da
23 changed files with 1456 additions and 604 deletions

View File

@ -0,0 +1,384 @@
/*
* Copyright (C) 2016 Adrian Ulrich <adrian@blinkenlights.ch>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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<Long> 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<ContentValues> bulk = new ArrayList<ContentValues>();
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";
}
}

View File

@ -0,0 +1,347 @@
/*
* Copyright (C) 2016 Adrian Ulrich <adrian@blinkenlights.ch>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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<ContentValues> 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);
}
}

View File

@ -0,0 +1,228 @@
/*
* Copyright (C) 2016 Adrian Ulrich <adrian@blinkenlights.ch>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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<String> 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);
}
}

View File

@ -0,0 +1,198 @@
/*
* Copyright (C) 2016 Adrian Ulrich <adrian@blinkenlights.ch>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2014-2015 Adrian Ulrich <adrian@blinkenlights.ch>
* Copyright (C) 2014-2016 Adrian Ulrich <adrian@blinkenlights.ch>
*
* 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()) {

View File

@ -340,7 +340,7 @@ public class FullPlaybackActivity extends SlidingPlaybackActivity
PlaylistTask playlistTask = new PlaylistTask(playlistId, getString(R.string.playlist_favorites));
playlistTask.audioIds = new ArrayList<Long>();
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;

View File

@ -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);
}

View File

@ -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);

View File

@ -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);
}
/**

View File

@ -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<Long> 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);
}
/**

View File

@ -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<clonedProjection.length; i++) {
if (clonedProjection[i].equals("0") == false) // do not prefix fake rows
clonedProjection[i] = (returnSongs ? "audio" : authority)+"."+clonedProjection[i];
}
sql += TextUtils.join(", ", clonedProjection);
sql += " FROM audio_genres_map_noid, audio" + (authority.equals("audio") ? "" : ", "+authority);
sql += " WHERE(audio._id = audio_id AND genre_id=?)";
if (selection != null && selection.length() > 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<Song> 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;
}

View File

@ -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))

View File

@ -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<Long> getTopSongs(int limit) {
public static ArrayList<Long> getTopSongs(Context context, int limit) {
ArrayList<Long> payload = new ArrayList<Long>();
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<Long> toCheck = new ArrayList<Long>(); // 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;
}
}

View File

@ -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);

View File

@ -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<Long> items = mPlayCounts.getTopSongs(mAutoPlPlaycounts);
Playlist.addToPlaylist(resolver, id, items);
long id = Playlist.createPlaylist(getApplicationContext(), playlistName);
ArrayList<Long> 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);

View File

@ -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<Long> result = new ArrayList<Long>();
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<Long> audioIds) {
public static int addToPlaylist(Context context, long playlistId, ArrayList<Long> 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();

View File

@ -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();

View File

@ -1,6 +1,6 @@
/*
* Copyright (C) 2011 Christopher Eby <kreed@kreed.org>
* Copyright (C) 2015 Adrian Ulrich <adrian@blinkenlights.ch>
* Copyright (C) 2015-2016 Adrian Ulrich <adrian@blinkenlights.ch>
*
* 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);
}

View File

@ -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;

View File

@ -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);
}
}

View File

@ -1,36 +1,33 @@
/*
* Copyright (C) 2011 Christopher Eby <kreed@kreed.org>
* Copyright (C) 2016 Adrian Ulrich <adrian@blinkenlights.ch>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@ -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<Song> {
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
/**

View File

@ -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;
}