initial import of our own media library database
This commit is contained in:
parent
1e1fe4a375
commit
4e1101b2da
384
src/ch/blinkenlights/android/medialibrary/MediaLibrary.java
Normal file
384
src/ch/blinkenlights/android/medialibrary/MediaLibrary.java
Normal 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";
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
228
src/ch/blinkenlights/android/medialibrary/MediaScanner.java
Normal file
228
src/ch/blinkenlights/android/medialibrary/MediaScanner.java
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
198
src/ch/blinkenlights/android/medialibrary/MediaSchema.java
Normal file
198
src/ch/blinkenlights/android/medialibrary/MediaSchema.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
@ -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()) {
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user