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
|
* 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
|
* 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.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.provider.MediaStore;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.Window;
|
import android.view.Window;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
@ -124,10 +125,24 @@ public class AudioPickerActivity extends PlaybackActivity {
|
|||||||
Song song = new Song(-1);
|
Song song = new Song(-1);
|
||||||
Cursor cursor = null;
|
Cursor cursor = null;
|
||||||
|
|
||||||
if (uri.getScheme().equals("content"))
|
if (uri.getScheme().equals("content")) {
|
||||||
cursor = MediaUtils.queryResolver(getContentResolver(), uri, Song.FILLED_PROJECTION, null, null, null);
|
// check if the native content resolver has a path for this
|
||||||
if (uri.getScheme().equals("file"))
|
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());
|
cursor = MediaUtils.getCursorForFileQuery(uri.getPath());
|
||||||
|
}
|
||||||
|
|
||||||
if (cursor != null) {
|
if (cursor != null) {
|
||||||
if (cursor.moveToNext()) {
|
if (cursor.moveToNext()) {
|
||||||
|
@ -340,7 +340,7 @@ public class FullPlaybackActivity extends SlidingPlaybackActivity
|
|||||||
PlaylistTask playlistTask = new PlaylistTask(playlistId, getString(R.string.playlist_favorites));
|
PlaylistTask playlistTask = new PlaylistTask(playlistId, getString(R.string.playlist_favorites));
|
||||||
playlistTask.audioIds = new ArrayList<Long>();
|
playlistTask.audioIds = new ArrayList<Long>();
|
||||||
playlistTask.audioIds.add(song.id);
|
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));
|
mHandler.sendMessage(mHandler.obtainMessage(action, playlistTask));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -616,7 +616,7 @@ public class FullPlaybackActivity extends SlidingPlaybackActivity
|
|||||||
case MSG_NOTIFY_PLAYLIST_CHANGED: // triggers a fav-refresh
|
case MSG_NOTIFY_PLAYLIST_CHANGED: // triggers a fav-refresh
|
||||||
case MSG_LOAD_FAVOURITE_INFO:
|
case MSG_LOAD_FAVOURITE_INFO:
|
||||||
if (mCurrentSong != null) {
|
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));
|
mUiHandler.sendMessage(mUiHandler.obtainMessage(MSG_COMMIT_FAVOURITE_INFO, found));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -137,7 +137,7 @@ public class LazyCoverView extends ImageView
|
|||||||
if (bitmap == null) {
|
if (bitmap == null) {
|
||||||
if (payload.key.mediaType == MediaUtils.TYPE_ALBUM) {
|
if (payload.key.mediaType == MediaUtils.TYPE_ALBUM) {
|
||||||
// We only display real covers for queries using the album id as key
|
// 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) {
|
if (song != null) {
|
||||||
bitmap = song.getSmallCover(mContext);
|
bitmap = song.getSmallCover(mContext);
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,8 @@
|
|||||||
|
|
||||||
package ch.blinkenlights.android.vanilla;
|
package ch.blinkenlights.android.vanilla;
|
||||||
|
|
||||||
|
import ch.blinkenlights.android.medialibrary.MediaLibrary;
|
||||||
|
|
||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
@ -547,7 +549,7 @@ public class LibraryActivity
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a new limiter of the given type built from the first
|
* 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
|
* @param limiterType The type of limiter to create. Must be either
|
||||||
* MediaUtils.TYPE_ARTIST or MediaUtils.TYPE_ALBUM.
|
* MediaUtils.TYPE_ARTIST or MediaUtils.TYPE_ALBUM.
|
||||||
@ -555,10 +557,10 @@ public class LibraryActivity
|
|||||||
*/
|
*/
|
||||||
private void setLimiter(int limiterType, String selection)
|
private void setLimiter(int limiterType, String selection)
|
||||||
{
|
{
|
||||||
ContentResolver resolver = getContentResolver();
|
String[] projection = new String[] { MediaLibrary.ContributorColumns.ARTIST_ID, MediaLibrary.SongColumns.ALBUM_ID, MediaLibrary.ContributorColumns.ARTIST, MediaLibrary.AlbumColumns.ALBUM };
|
||||||
Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
QueryTask query = new QueryTask(MediaLibrary.VIEW_SONGS_ALBUMS_ARTISTS, projection, selection, null, null);
|
||||||
String[] projection = new String[] { MediaStore.Audio.Media.ARTIST_ID, MediaStore.Audio.Media.ALBUM_ID, MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ALBUM };
|
Cursor cursor = query.runQuery(getApplicationContext());
|
||||||
Cursor cursor = MediaUtils.queryResolver(resolver, uri, projection, selection, null, null);
|
|
||||||
if (cursor != null) {
|
if (cursor != null) {
|
||||||
if (cursor.moveToNext()) {
|
if (cursor.moveToNext()) {
|
||||||
String[] fields;
|
String[] fields;
|
||||||
@ -566,11 +568,11 @@ public class LibraryActivity
|
|||||||
switch (limiterType) {
|
switch (limiterType) {
|
||||||
case MediaUtils.TYPE_ARTIST:
|
case MediaUtils.TYPE_ARTIST:
|
||||||
fields = new String[] { cursor.getString(2) };
|
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;
|
break;
|
||||||
case MediaUtils.TYPE_ALBUM:
|
case MediaUtils.TYPE_ALBUM:
|
||||||
fields = new String[] { cursor.getString(2), cursor.getString(3) };
|
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;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("setLimiter() does not support limiter type " + limiterType);
|
throw new IllegalArgumentException("setLimiter() does not support limiter type " + limiterType);
|
||||||
|
@ -214,7 +214,6 @@ public class LibraryPagerAdapter
|
|||||||
mUiHandler = new Handler(this);
|
mUiHandler = new Handler(this);
|
||||||
mWorkerHandler = new Handler(workerLooper, this);
|
mWorkerHandler = new Handler(workerLooper, this);
|
||||||
mCurrentPage = -1;
|
mCurrentPage = -1;
|
||||||
activity.getContentResolver().registerContentObserver(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, true, mPlaylistObserver);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -23,6 +23,8 @@
|
|||||||
|
|
||||||
package ch.blinkenlights.android.vanilla;
|
package ch.blinkenlights.android.vanilla;
|
||||||
|
|
||||||
|
import ch.blinkenlights.android.medialibrary.MediaLibrary;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
@ -48,6 +50,7 @@ import java.util.regex.Pattern;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.lang.StringBuilder;
|
import java.lang.StringBuilder;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
/**
|
/**
|
||||||
* MediaAdapter provides an adapter backed by a MediaStore content provider.
|
* MediaAdapter provides an adapter backed by a MediaStore content provider.
|
||||||
* It generates simple one- or two-line text views to display each media
|
* It generates simple one- or two-line text views to display each media
|
||||||
@ -66,9 +69,6 @@ public class MediaAdapter
|
|||||||
, SectionIndexer
|
, SectionIndexer
|
||||||
{
|
{
|
||||||
private static final Pattern SPACE_SPLIT = Pattern.compile("\\s+");
|
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
|
* The string to use for length==0 db fields
|
||||||
*/
|
*/
|
||||||
@ -96,9 +96,9 @@ public class MediaAdapter
|
|||||||
*/
|
*/
|
||||||
private final int mType;
|
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
|
* 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
|
* 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.
|
* ASC or DESC as appropriate before being passed to the query.
|
||||||
*/
|
*/
|
||||||
private String[] mAdapterSortValues;
|
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 index of the current of the current sort mode in mSortValues, or
|
||||||
* the inverse of the index (in which case sort should be descending
|
* the inverse of the index (in which case sort should be descending
|
||||||
@ -189,53 +184,51 @@ public class MediaAdapter
|
|||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case MediaUtils.TYPE_ARTIST:
|
case MediaUtils.TYPE_ARTIST:
|
||||||
mStore = MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI;
|
mSource = MediaLibrary.VIEW_ARTISTS;
|
||||||
mFields = new String[] { MediaStore.Audio.Artists.ARTIST };
|
mFields = new String[] { MediaLibrary.ContributorColumns.ARTIST };
|
||||||
mFieldKeys = new String[] { MediaStore.Audio.Artists.ARTIST_KEY };
|
mFieldKeys = new String[] { MediaLibrary.ContributorColumns.ARTIST_SORT };
|
||||||
mSortEntries = new int[] { R.string.name, R.string.number_of_tracks };
|
mSortEntries = new int[] { R.string.name, R.string.date_added };
|
||||||
mAdapterSortValues = new String[] { "artist_key %1$s", "number_of_tracks %1$s,artist_key %1$s" };
|
mAdapterSortValues = new String[] { MediaLibrary.ContributorColumns.ARTIST_SORT+" %1$s", MediaLibrary.ContributorColumns.MTIME+" %1$s" };
|
||||||
mSongSortValues = new String[] { "artist_key %1$s,track", "artist_key %1$s,track" /* cannot sort by number_of_tracks */ };
|
|
||||||
break;
|
break;
|
||||||
case MediaUtils.TYPE_ALBUM:
|
case MediaUtils.TYPE_ALBUM:
|
||||||
mStore = MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI;
|
mSource = MediaLibrary.VIEW_ALBUMS_ARTISTS;
|
||||||
mFields = new String[] { MediaStore.Audio.Albums.ALBUM, MediaStore.Audio.Albums.ARTIST };
|
mFields = new String[] { MediaLibrary.AlbumColumns.ALBUM, MediaLibrary.ContributorColumns.ARTIST };
|
||||||
// Why is there no artist_key column constant in the album MediaStore? The column does seem to exist.
|
mFieldKeys = new String[] { MediaLibrary.AlbumColumns.ALBUM_SORT, MediaLibrary.ContributorColumns.ARTIST_SORT };
|
||||||
mFieldKeys = new String[] { MediaStore.Audio.Albums.ALBUM_KEY, "artist_key" };
|
mSortEntries = new int[] { R.string.name, R.string.artist_album, R.string.year, R.string.date_added };
|
||||||
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[] { MediaLibrary.AlbumColumns.ALBUM_SORT+" %1$s", MediaLibrary.ContributorColumns.ARTIST_SORT+" %1$s,"+MediaLibrary.AlbumColumns.ALBUM_SORT+" %1$s",
|
||||||
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" };
|
MediaLibrary.AlbumColumns.YEAR+" %1$s", MediaLibrary.AlbumColumns.MTIME+" %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" };
|
|
||||||
break;
|
break;
|
||||||
case MediaUtils.TYPE_SONG:
|
case MediaUtils.TYPE_SONG:
|
||||||
mStore = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
mSource = MediaLibrary.VIEW_SONGS_ALBUMS_ARTISTS;
|
||||||
mFields = new String[] { MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.ALBUM, MediaStore.Audio.Media.ARTIST };
|
mFields = new String[] { MediaLibrary.SongColumns.TITLE, MediaLibrary.AlbumColumns.ALBUM, MediaLibrary.ContributorColumns.ARTIST };
|
||||||
mFieldKeys = new String[] { MediaStore.Audio.Media.TITLE_KEY, MediaStore.Audio.Media.ALBUM_KEY, MediaStore.Audio.Media.ARTIST_KEY };
|
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,
|
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 };
|
||||||
R.string.artist_year_album, R.string.album_track,
|
mAdapterSortValues = new String[] { MediaLibrary.SongColumns.TITLE_SORT+" %1$s",
|
||||||
R.string.year, R.string.date_added, R.string.song_playcount };
|
MediaLibrary.ContributorColumns.ARTIST_SORT+" %1$s,"+MediaLibrary.AlbumColumns.ALBUM_SORT+" %1$s,"+MediaLibrary.AlbumColumns.DISC_NUMBER+","+MediaLibrary.SongColumns.SONG_NUMBER,
|
||||||
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",
|
MediaLibrary.ContributorColumns.ARTIST_SORT+" %1$s,"+MediaLibrary.AlbumColumns.ALBUM_SORT+" %1$s,"+MediaLibrary.SongColumns.TITLE_SORT+" %1$s",
|
||||||
"artist_key %1$s,year %1$s,album_key %1$s,track", "album_key %1$s,track",
|
MediaLibrary.AlbumColumns.ALBUM_SORT+" %1$s,"+MediaLibrary.AlbumColumns.DISC_NUMBER+","+MediaLibrary.SongColumns.SONG_NUMBER,
|
||||||
"year %1$s,title_key %1$s","_id %1$s", SORT_MAGIC_PLAYCOUNT };
|
MediaLibrary.AlbumColumns.YEAR+" %1$s,"+MediaLibrary.AlbumColumns.ALBUM_SORT+" %1$s,"+MediaLibrary.AlbumColumns.DISC_NUMBER+","+MediaLibrary.SongColumns.SONG_NUMBER,
|
||||||
mSongSortValues = mAdapterSortValues;
|
MediaLibrary.SongColumns.MTIME+" %1$s",
|
||||||
|
MediaLibrary.SongColumns.PLAYCOUNT+" %1$s",
|
||||||
|
};
|
||||||
// Songs covers are cached per-album
|
// Songs covers are cached per-album
|
||||||
mCoverCacheType = MediaUtils.TYPE_ALBUM;
|
mCoverCacheType = MediaUtils.TYPE_ALBUM;
|
||||||
coverCacheKey = MediaStore.Audio.Albums.ALBUM_ID;
|
coverCacheKey = MediaStore.Audio.Albums.ALBUM_ID;
|
||||||
break;
|
break;
|
||||||
case MediaUtils.TYPE_PLAYLIST:
|
case MediaUtils.TYPE_PLAYLIST:
|
||||||
mStore = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI;
|
mSource = MediaLibrary.TABLE_PLAYLISTS;
|
||||||
mFields = new String[] { MediaStore.Audio.Playlists.NAME };
|
mFields = new String[] { MediaLibrary.PlaylistColumns.NAME };
|
||||||
mFieldKeys = null;
|
mFieldKeys = null;
|
||||||
mSortEntries = new int[] { R.string.name, R.string.date_added };
|
mSortEntries = new int[] { R.string.name, R.string.date_added };
|
||||||
mAdapterSortValues = new String[] { "name %1$s", "date_added %1$s" };
|
mAdapterSortValues = new String[] { MediaLibrary.PlaylistColumns.NAME+" %1$s", MediaLibrary.PlaylistColumns._ID+" %1$s" };
|
||||||
mSongSortValues = null;
|
|
||||||
mExpandable = true;
|
mExpandable = true;
|
||||||
break;
|
break;
|
||||||
case MediaUtils.TYPE_GENRE:
|
case MediaUtils.TYPE_GENRE:
|
||||||
mStore = MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI;
|
mSource = MediaLibrary.TABLE_GENRES;
|
||||||
mFields = new String[] { MediaStore.Audio.Genres.NAME };
|
mFields = new String[] { MediaLibrary.GenreColumns._GENRE };
|
||||||
mFieldKeys = null;
|
mFieldKeys = new String[] { MediaLibrary.GenreColumns._GENRE_SORT };
|
||||||
mSortEntries = new int[] { R.string.name };
|
mSortEntries = new int[] { R.string.name };
|
||||||
mAdapterSortValues = new String[] { "name %1$s" };
|
mAdapterSortValues = new String[] { MediaLibrary.GenreColumns._GENRE_SORT+" %1$s" };
|
||||||
mSongSortValues = null;
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("Invalid value for type: " + type);
|
throw new IllegalArgumentException("Invalid value for type: " + type);
|
||||||
@ -260,8 +253,8 @@ public class MediaAdapter
|
|||||||
private String getFirstSortColumn() {
|
private String getFirstSortColumn() {
|
||||||
int mode = mSortMode < 0 ? ~mSortMode : mSortMode; // get current sort mode
|
int mode = mSortMode < 0 ? ~mSortMode : mSortMode; // get current sort mode
|
||||||
String column = SPACE_SPLIT.split(mAdapterSortValues[mode])[0];
|
String column = SPACE_SPLIT.split(mAdapterSortValues[mode])[0];
|
||||||
if(column.endsWith("_key")) { // we want human-readable string, not machine-composed
|
if(column.endsWith("_sort")) { // we want human-readable string, not machine-composed
|
||||||
column = column.substring(0, column.length() - 4);
|
column = column.substring(0, column.length() - 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
return column;
|
return column;
|
||||||
@ -291,17 +284,23 @@ public class MediaAdapter
|
|||||||
* Build the query to be run with runQuery().
|
* Build the query to be run with runQuery().
|
||||||
*
|
*
|
||||||
* @param projection The columns to query.
|
* @param projection The columns to query.
|
||||||
* @param forceMusicCheck Force the is_music check to be added to the
|
* @param returnSongs return songs instead of mType if true.
|
||||||
* selection.
|
|
||||||
*/
|
*/
|
||||||
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;
|
String constraint = mConstraint;
|
||||||
Limiter limiter = mLimiter;
|
Limiter limiter = mLimiter;
|
||||||
|
|
||||||
StringBuilder selection = new StringBuilder();
|
StringBuilder selection = new StringBuilder();
|
||||||
String[] selectionArgs = null;
|
String[] selectionArgs = null;
|
||||||
|
String[] enrichedProjection = projection;
|
||||||
|
|
||||||
|
// Assemble the sort string as requested by the user
|
||||||
int mode = mSortMode;
|
int mode = mSortMode;
|
||||||
String sortDir;
|
String sortDir;
|
||||||
if (mode < 0) {
|
if (mode < 0) {
|
||||||
@ -311,47 +310,27 @@ public class MediaAdapter
|
|||||||
sortDir = "ASC";
|
sortDir = "ASC";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the song-sort mapping if we are returning songs
|
// Fetch current sorting mode and sort by disc+track if we are going to look up the songs table
|
||||||
String sortStringRaw = (returnSongs ? mSongSortValues[mode] : mAdapterSortValues[mode]);
|
String sortRaw = mAdapterSortValues[mode];
|
||||||
String[] enrichedProjection = projection;
|
if (returnSongs) {
|
||||||
|
sortRaw += ", "+MediaLibrary.AlbumColumns.DISC_NUMBER+", "+MediaLibrary.SongColumns.SONG_NUMBER;
|
||||||
// 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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String sort = String.format(sortStringRaw, sortDir);
|
// ...and assemble the SQL string we are really going to use
|
||||||
|
String sort = String.format(sortRaw, sortDir);
|
||||||
if (returnSongs || mType == MediaUtils.TYPE_SONG)
|
|
||||||
selection.append(MediaStore.Audio.Media.IS_MUSIC+" AND length(_data)");
|
|
||||||
|
|
||||||
|
// include the constraint (aka: search string) if any
|
||||||
if (constraint != null && constraint.length() != 0) {
|
if (constraint != null && constraint.length() != 0) {
|
||||||
String[] needles;
|
String[] needles;
|
||||||
String[] keySource;
|
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) {
|
if (mFieldKeys != null) {
|
||||||
String colKey = MediaStore.Audio.keyFor(constraint);
|
String colKey = MediaLibrary.keyFor(constraint);
|
||||||
String spaceColKey = DatabaseUtils.getCollationKey(" ");
|
String spaceColKey = DatabaseUtils.getCollationKey(" ");
|
||||||
needles = colKey.split(spaceColKey);
|
needles = colKey.split(spaceColKey);
|
||||||
keySource = mFieldKeys;
|
keySource = mFieldKeys;
|
||||||
} else {
|
} else {
|
||||||
|
// only used for playlists, maybe we should just update the schema ?
|
||||||
needles = SPACE_SPLIT.split(constraint);
|
needles = SPACE_SPLIT.split(constraint);
|
||||||
keySource = mFields;
|
keySource = mFields;
|
||||||
}
|
}
|
||||||
@ -379,31 +358,28 @@ public class MediaAdapter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QueryTask query;
|
if (limiter != null) {
|
||||||
if(mType == MediaUtils.TYPE_GENRE && !returnSongs) {
|
if (selection.length() != 0) {
|
||||||
query = MediaUtils.buildGenreExcludeEmptyQuery(enrichedProjection, selection.toString(),
|
selection.append(" AND ");
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
query = new QueryTask(mStore, enrichedProjection, selection.toString(), selectionArgs, sort);
|
selection.append(limiter.data);
|
||||||
if (returnSongs) // force query on song provider as we are requested to return songs
|
|
||||||
query.uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Cursor query()
|
public Cursor query()
|
||||||
{
|
{
|
||||||
return buildQuery(mProjection, false).runQuery(mContext.getContentResolver());
|
return buildQuery(mProjection, false).runQuery(mContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -467,15 +443,15 @@ public class MediaAdapter
|
|||||||
switch (mType) {
|
switch (mType) {
|
||||||
case MediaUtils.TYPE_ARTIST:
|
case MediaUtils.TYPE_ARTIST:
|
||||||
fields = new String[] { cursor.getString(2) };
|
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;
|
break;
|
||||||
case MediaUtils.TYPE_ALBUM:
|
case MediaUtils.TYPE_ALBUM:
|
||||||
fields = new String[] { cursor.getString(3), cursor.getString(2) };
|
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;
|
break;
|
||||||
case MediaUtils.TYPE_GENRE:
|
case MediaUtils.TYPE_GENRE:
|
||||||
fields = new String[] { cursor.getString(2) };
|
fields = new String[] { cursor.getString(2) };
|
||||||
data = id;
|
data = String.format("%s=%d", MediaLibrary.GenreSongColumns._GENRE_ID, id);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IllegalStateException("getLimiter() is not supported for media type: " + mType);
|
throw new IllegalStateException("getLimiter() is not supported for media type: " + mType);
|
||||||
@ -588,7 +564,7 @@ public class MediaAdapter
|
|||||||
*/
|
*/
|
||||||
public void setSortMode(int i)
|
public void setSortMode(int i)
|
||||||
{
|
{
|
||||||
mSortMode = i;
|
mSortMode = (i < mSortEntries.length ? i : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -22,6 +22,8 @@
|
|||||||
|
|
||||||
package ch.blinkenlights.android.vanilla;
|
package ch.blinkenlights.android.vanilla;
|
||||||
|
|
||||||
|
import ch.blinkenlights.android.medialibrary.MediaLibrary;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@ -32,6 +34,8 @@ import java.util.Random;
|
|||||||
import java.util.Vector;
|
import java.util.Vector;
|
||||||
import java.util.zip.CRC32;
|
import java.util.zip.CRC32;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import junit.framework.Assert;
|
import junit.framework.Assert;
|
||||||
|
|
||||||
import android.content.ActivityNotFoundException;
|
import android.content.ActivityNotFoundException;
|
||||||
@ -89,19 +93,19 @@ public class MediaUtils {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The default sort order for media queries. First artist, then album, then
|
* 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
|
* 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.
|
* Cached random instance.
|
||||||
@ -141,35 +145,36 @@ public class MediaUtils {
|
|||||||
*/
|
*/
|
||||||
private static QueryTask buildMediaQuery(int type, long id, String[] projection, String select)
|
private static QueryTask buildMediaQuery(int type, long id, String[] projection, String select)
|
||||||
{
|
{
|
||||||
Uri media = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
|
||||||
StringBuilder selection = new StringBuilder();
|
StringBuilder selection = new StringBuilder();
|
||||||
String sort = DEFAULT_SORT;
|
String sort = DEFAULT_SORT;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case TYPE_SONG:
|
case TYPE_SONG:
|
||||||
selection.append(MediaStore.Audio.Media._ID);
|
selection.append(MediaLibrary.SongColumns._ID);
|
||||||
break;
|
break;
|
||||||
case TYPE_ARTIST:
|
case TYPE_ARTIST:
|
||||||
selection.append(MediaStore.Audio.Media.ARTIST_ID);
|
selection.append(MediaLibrary.ContributorColumns.ARTIST_ID);
|
||||||
break;
|
break;
|
||||||
case TYPE_ALBUM:
|
case TYPE_ALBUM:
|
||||||
selection.append(MediaStore.Audio.Media.ALBUM_ID);
|
selection.append(MediaLibrary.SongColumns.ALBUM_ID);
|
||||||
sort = ALBUM_SORT;
|
sort = ALBUM_SORT;
|
||||||
break;
|
break;
|
||||||
|
case TYPE_GENRE:
|
||||||
|
selection.append(MediaLibrary.GenreSongColumns._GENRE_ID);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("Invalid type specified: " + type);
|
throw new IllegalArgumentException("Invalid type specified: " + type);
|
||||||
}
|
}
|
||||||
|
|
||||||
selection.append('=');
|
selection.append('=');
|
||||||
selection.append(id);
|
selection.append(id);
|
||||||
selection.append(" AND length(_data) AND "+MediaStore.Audio.Media.IS_MUSIC);
|
|
||||||
|
|
||||||
if (select != null) {
|
if (select != null) {
|
||||||
selection.append(" AND ");
|
selection.append(" AND ");
|
||||||
selection.append(select);
|
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;
|
result.type = type;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@ -180,139 +185,16 @@ public class MediaUtils {
|
|||||||
*
|
*
|
||||||
* @param id The id of the playlist in MediaStore.Audio.Playlists.
|
* @param id The id of the playlist in MediaStore.Audio.Playlists.
|
||||||
* @param projection The columns to query.
|
* @param projection The columns to query.
|
||||||
* @param selection The selection to pass to the query, or null.
|
|
||||||
* @return The initialized query.
|
* @return The initialized query.
|
||||||
*/
|
*/
|
||||||
public static QueryTask buildPlaylistQuery(long id, String[] projection, String selection)
|
public static QueryTask buildPlaylistQuery(long id, String[] projection) {
|
||||||
{
|
String sort = MediaLibrary.PlaylistSongColumns.POSITION;
|
||||||
Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", id);
|
String selection = MediaLibrary.PlaylistSongColumns.PLAYLIST_ID+"="+id;
|
||||||
String sort = MediaStore.Audio.Playlists.Members.PLAY_ORDER;
|
QueryTask result = new QueryTask(MediaLibrary.VIEW_PLAYLIST_SONGS, projection, selection, null, sort);
|
||||||
QueryTask result = new QueryTask(uri, projection, selection, null, sort);
|
|
||||||
result.type = TYPE_PLAYLIST;
|
result.type = TYPE_PLAYLIST;
|
||||||
return result;
|
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.
|
* Builds a query with the given information.
|
||||||
*
|
*
|
||||||
@ -329,11 +211,10 @@ public class MediaUtils {
|
|||||||
case TYPE_ARTIST:
|
case TYPE_ARTIST:
|
||||||
case TYPE_ALBUM:
|
case TYPE_ALBUM:
|
||||||
case TYPE_SONG:
|
case TYPE_SONG:
|
||||||
|
case TYPE_GENRE:
|
||||||
return buildMediaQuery(type, id, projection, selection);
|
return buildMediaQuery(type, id, projection, selection);
|
||||||
case TYPE_PLAYLIST:
|
case TYPE_PLAYLIST:
|
||||||
return buildPlaylistQuery(id, projection, selection);
|
return buildPlaylistQuery(id, projection);
|
||||||
case TYPE_GENRE:
|
|
||||||
return buildGenreQuery(id, projection, selection, null, MediaStore.Audio.Genres.Members.TITLE_KEY, TYPE_SONG, true);
|
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("Specified type not valid: " + type);
|
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
|
* Query the MediaStore to determine the id of the genre the song belongs
|
||||||
* to.
|
* 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.
|
* @param id The id of the song to query the genre for.
|
||||||
*/
|
*/
|
||||||
public static long queryGenreForSong(ContentResolver resolver, long id)
|
public static long queryGenreForSong(Context context, long id) {
|
||||||
{
|
String[] projection = { MediaLibrary.GenreSongColumns._GENRE_ID };
|
||||||
String[] projection = { "_id" };
|
String query = MediaLibrary.GenreSongColumns.SONG_ID+"=?";
|
||||||
Uri uri = MediaStore.Audio.Genres.getContentUriForAudioId("external", (int)id);
|
String[] queryArgs = new String[] { id+"" };
|
||||||
Cursor cursor = queryResolver(resolver, uri, projection, null, null, null);
|
|
||||||
|
|
||||||
|
Cursor cursor = MediaLibrary.queryLibrary(context, MediaLibrary.TABLE_GENRES_SONGS, projection, query, queryArgs, null);
|
||||||
if (cursor != null) {
|
if (cursor != null) {
|
||||||
if (cursor.moveToNext())
|
if (cursor.moveToNext())
|
||||||
return cursor.getLong(0);
|
return cursor.getLong(0);
|
||||||
@ -379,7 +260,7 @@ public class MediaUtils {
|
|||||||
/**
|
/**
|
||||||
* Shuffle a Song list using Collections.shuffle().
|
* 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)
|
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.
|
* 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
|
* @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.
|
* 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) {
|
if (sSongCount == -1) {
|
||||||
Uri media = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
QueryTask query = new QueryTask(MediaLibrary.TABLE_SONGS, new String[]{"count(*)"}, null, null, null);
|
||||||
String selection = MediaStore.Audio.Media.IS_MUSIC;
|
Cursor cursor = query.runQuery(context);
|
||||||
selection += " AND length(_data)";
|
|
||||||
Cursor cursor = queryResolver(resolver, media, new String[]{"count(_id)"}, selection, null, null);
|
|
||||||
if (cursor == null) {
|
if (cursor == null) {
|
||||||
sSongCount = 0;
|
sSongCount = 0;
|
||||||
} else {
|
} else {
|
||||||
@ -456,14 +334,11 @@ public class MediaUtils {
|
|||||||
* Returns a shuffled array contaning the ids of all the songs on the
|
* Returns a shuffled array contaning the ids of all the songs on the
|
||||||
* device's library.
|
* device's library.
|
||||||
*
|
*
|
||||||
* @param resolver A ContentResolver to use.
|
* @param context The Context to use
|
||||||
*/
|
*/
|
||||||
private static long[] queryAllSongs(ContentResolver resolver)
|
private static long[] queryAllSongs(Context context) {
|
||||||
{
|
QueryTask query = new QueryTask(MediaLibrary.TABLE_SONGS, Song.EMPTY_PROJECTION, null, null, null);
|
||||||
Uri media = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
Cursor cursor = query.runQuery(context);
|
||||||
String selection = MediaStore.Audio.Media.IS_MUSIC;
|
|
||||||
selection += " AND length(_data)";
|
|
||||||
Cursor cursor = queryResolver(resolver, media, Song.EMPTY_PROJECTION, selection, null, null);
|
|
||||||
if (cursor == null || cursor.getCount() == 0) {
|
if (cursor == null || cursor.getCount() == 0) {
|
||||||
sSongCount = 0;
|
sSongCount = 0;
|
||||||
return null;
|
return null;
|
||||||
@ -485,29 +360,9 @@ public class MediaUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs a query on the passed content resolver.
|
* Called if we detected a medium change
|
||||||
* Catches (and returns null on) SecurityException (= user revoked read permission)
|
* This flushes some cached data
|
||||||
*
|
|
||||||
* @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
|
|
||||||
*/
|
*/
|
||||||
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()
|
public static void onMediaChange()
|
||||||
{
|
{
|
||||||
sSongCount = -1;
|
sSongCount = -1;
|
||||||
@ -526,9 +381,8 @@ public class MediaUtils {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ContentResolver resolver = ctx.getContentResolver();
|
String[] projection = new String [] { MediaLibrary.SongColumns._ID, MediaLibrary.SongColumns.PATH };
|
||||||
String[] projection = new String [] { MediaStore.Audio.Media._ID, MediaStore.Audio.Media.DATA };
|
Cursor cursor = buildQuery(type, id, projection, null).runQuery(ctx);
|
||||||
Cursor cursor = buildQuery(type, id, projection, null).runQuery(resolver);
|
|
||||||
if(cursor == null) {
|
if(cursor == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -555,10 +409,10 @@ public class MediaUtils {
|
|||||||
* @param type The MediaTye to query
|
* @param type The MediaTye to query
|
||||||
* @param id The id of given type 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);
|
Song song = new Song(-1);
|
||||||
QueryTask query = buildQuery(type, id, Song.FILLED_PROJECTION, null);
|
QueryTask query = buildQuery(type, id, Song.FILLED_PROJECTION, null);
|
||||||
Cursor cursor = query.runQuery(resolver);
|
Cursor cursor = query.runQuery(context);
|
||||||
if (cursor != null) {
|
if (cursor != null) {
|
||||||
if (cursor.getCount() > 0) {
|
if (cursor.getCount() > 0) {
|
||||||
cursor.moveToPosition(0);
|
cursor.moveToPosition(0);
|
||||||
@ -573,14 +427,14 @@ public class MediaUtils {
|
|||||||
* Returns a song randomly selected from all the songs in the Android
|
* Returns a song randomly selected from all the songs in the Android
|
||||||
* MediaStore.
|
* 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;
|
long[] songs = sAllSongs;
|
||||||
|
|
||||||
if (songs == null) {
|
if (songs == null) {
|
||||||
songs = queryAllSongs(resolver);
|
songs = queryAllSongs(context);
|
||||||
if (songs == null)
|
if (songs == null)
|
||||||
return null;
|
return null;
|
||||||
sAllSongs = songs;
|
sAllSongs = songs;
|
||||||
@ -590,7 +444,7 @@ public class MediaUtils {
|
|||||||
shuffle(sAllSongs);
|
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;
|
result.flags |= Song.FLAG_RANDOM;
|
||||||
sAllSongsIdx++;
|
sAllSongsIdx++;
|
||||||
return result;
|
return result;
|
||||||
@ -670,11 +524,10 @@ public class MediaUtils {
|
|||||||
-> ended with a % for the LIKE query
|
-> ended with a % for the LIKE query
|
||||||
*/
|
*/
|
||||||
path = addDirEndSlash(sanitizeMediaPath(path)) + "%";
|
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 };
|
String[] qargs = { path };
|
||||||
|
|
||||||
Uri media = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
QueryTask result = new QueryTask(MediaLibrary.VIEW_SONGS_ALBUMS_ARTISTS, projection, query, qargs, FILE_SORT);
|
||||||
QueryTask result = new QueryTask(media, projection, query, qargs, FILE_SORT);
|
|
||||||
result.type = TYPE_FILE;
|
result.type = TYPE_FILE;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -422,7 +422,6 @@ public class MirrorLinkMediaBrowserService extends MediaBrowserService
|
|||||||
try {
|
try {
|
||||||
Cursor cursor = adapter.query();
|
Cursor cursor = adapter.query();
|
||||||
Context context = getApplicationContext();
|
Context context = getApplicationContext();
|
||||||
ContentResolver resolver = context.getContentResolver();
|
|
||||||
|
|
||||||
if (cursor == null) {
|
if (cursor == null) {
|
||||||
return;
|
return;
|
||||||
@ -436,7 +435,7 @@ public class MirrorLinkMediaBrowserService extends MediaBrowserService
|
|||||||
final String label = cursor.getString(2);
|
final String label = cursor.getString(2);
|
||||||
long mediaId = Long.parseLong(id);
|
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(
|
MediaBrowser.MediaItem item = new MediaBrowser.MediaItem(
|
||||||
new MediaDescription.Builder()
|
new MediaDescription.Builder()
|
||||||
.setMediaId(MediaID.toString(mediaType, mediaId, label))
|
.setMediaId(MediaID.toString(mediaType, mediaId, label))
|
||||||
|
@ -17,68 +17,23 @@
|
|||||||
|
|
||||||
package ch.blinkenlights.android.vanilla;
|
package ch.blinkenlights.android.vanilla;
|
||||||
|
|
||||||
|
import ch.blinkenlights.android.medialibrary.MediaLibrary;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.database.sqlite.SQLiteOpenHelper;
|
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.util.Log;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
||||||
public class PlayCountsHelper extends SQLiteOpenHelper {
|
public class PlayCountsHelper {
|
||||||
|
|
||||||
/**
|
public 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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Counts this song object as 'played' or 'skipped'
|
* Counts this song object as 'played' or 'skipped'
|
||||||
*/
|
*/
|
||||||
public void countSong(Song song, boolean played) {
|
public static void countSong(Context context, Song song, boolean played) {
|
||||||
long id = Song.getId(song);
|
final long id = Song.getId(song);
|
||||||
final String column = played ? "playcount" : "skipcount";
|
MediaLibrary.updateSongPlayCounts(context, id, played);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -86,52 +41,15 @@ public class PlayCountsHelper extends SQLiteOpenHelper {
|
|||||||
/**
|
/**
|
||||||
* Returns a sorted array list of most often listen song ids
|
* 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>();
|
ArrayList<Long> payload = new ArrayList<Long>();
|
||||||
SQLiteDatabase dbh = getReadableDatabase();
|
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) {
|
||||||
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()) {
|
|
||||||
payload.add(cursor.getLong(0));
|
payload.add(cursor.getLong(0));
|
||||||
|
limit --;
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor.close();
|
cursor.close();
|
||||||
dbh.close();
|
|
||||||
return payload;
|
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: {
|
case MSG_CREATE_PLAYLIST: {
|
||||||
PlaylistTask playlistTask = (PlaylistTask)message.obj;
|
PlaylistTask playlistTask = (PlaylistTask)message.obj;
|
||||||
int nextAction = message.arg1;
|
int nextAction = message.arg1;
|
||||||
long playlistId = Playlist.createPlaylist(getContentResolver(), playlistTask.name);
|
long playlistId = Playlist.createPlaylist(this, playlistTask.name);
|
||||||
playlistTask.playlistId = playlistId;
|
playlistTask.playlistId = playlistId;
|
||||||
mHandler.sendMessage(mHandler.obtainMessage(nextAction, playlistTask));
|
mHandler.sendMessage(mHandler.obtainMessage(nextAction, playlistTask));
|
||||||
break;
|
break;
|
||||||
@ -498,11 +498,11 @@ public abstract class PlaybackActivity extends Activity
|
|||||||
int count = 0;
|
int count = 0;
|
||||||
|
|
||||||
if (playlistTask.query != null) {
|
if (playlistTask.query != null) {
|
||||||
count += Playlist.addToPlaylist(getContentResolver(), playlistTask.playlistId, playlistTask.query);
|
count += Playlist.addToPlaylist(this, playlistTask.playlistId, playlistTask.query);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playlistTask.audioIds != null) {
|
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);
|
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);
|
message = res.getString(R.string.delete_file_failed, file);
|
||||||
}
|
}
|
||||||
} else if (type == MediaUtils.TYPE_PLAYLIST) {
|
} else if (type == MediaUtils.TYPE_PLAYLIST) {
|
||||||
Playlist.deletePlaylist(getContentResolver(), id);
|
Playlist.deletePlaylist(this, id);
|
||||||
} else {
|
} else {
|
||||||
int count = PlaybackService.get(this).deleteMedia(type, id);
|
int count = PlaybackService.get(this).deleteMedia(type, id);
|
||||||
message = res.getQuantityString(R.plurals.deleted, count, count);
|
message = res.getQuantityString(R.plurals.deleted, count, count);
|
||||||
|
@ -23,6 +23,8 @@
|
|||||||
|
|
||||||
package ch.blinkenlights.android.vanilla;
|
package ch.blinkenlights.android.vanilla;
|
||||||
|
|
||||||
|
import ch.blinkenlights.android.medialibrary.MediaLibrary;
|
||||||
|
|
||||||
import android.app.Notification;
|
import android.app.Notification;
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
@ -420,10 +422,6 @@ public final class PlaybackService extends Service
|
|||||||
* Reference to precreated BASTP Object
|
* Reference to precreated BASTP Object
|
||||||
*/
|
*/
|
||||||
private BastpUtil mBastpUtil;
|
private BastpUtil mBastpUtil;
|
||||||
/**
|
|
||||||
* Reference to Playcounts helper class
|
|
||||||
*/
|
|
||||||
private PlayCountsHelper mPlayCounts;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate()
|
public void onCreate()
|
||||||
@ -435,8 +433,6 @@ public final class PlaybackService extends Service
|
|||||||
mTimeline.setCallback(this);
|
mTimeline.setCallback(this);
|
||||||
int state = loadState();
|
int state = loadState();
|
||||||
|
|
||||||
mPlayCounts = new PlayCountsHelper(this);
|
|
||||||
|
|
||||||
mMediaPlayer = getNewMediaPlayer();
|
mMediaPlayer = getNewMediaPlayer();
|
||||||
mPreparedMediaPlayer = getNewMediaPlayer();
|
mPreparedMediaPlayer = getNewMediaPlayer();
|
||||||
// We only have a single audio session
|
// We only have a single audio session
|
||||||
@ -488,7 +484,7 @@ public final class PlaybackService extends Service
|
|||||||
filter.addAction(Intent.ACTION_SCREEN_ON);
|
filter.addAction(Intent.ACTION_SCREEN_ON);
|
||||||
registerReceiver(mReceiver, filter);
|
registerReceiver(mReceiver, filter);
|
||||||
|
|
||||||
getContentResolver().registerContentObserver(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, true, mObserver);
|
MediaLibrary.registerContentObserver(getApplicationContext(), mObserver);
|
||||||
|
|
||||||
mRemoteControlClient = new RemoteControl().getClient(this);
|
mRemoteControlClient = new RemoteControl().getClient(this);
|
||||||
mRemoteControlClient.initializeRemote();
|
mRemoteControlClient.initializeRemote();
|
||||||
@ -1298,7 +1294,7 @@ public final class PlaybackService extends Service
|
|||||||
Song song = mTimeline.shiftCurrentSong(delta);
|
Song song = mTimeline.shiftCurrentSong(delta);
|
||||||
mCurrentSong = song;
|
mCurrentSong = song;
|
||||||
if (song == null) {
|
if (song == null) {
|
||||||
if (MediaUtils.isSongAvailable(getContentResolver())) {
|
if (MediaUtils.isSongAvailable(getApplicationContext())) {
|
||||||
int flag = finishAction(mState) == SongTimeline.FINISH_RANDOM ? FLAG_ERROR : FLAG_EMPTY_QUEUE;
|
int flag = finishAction(mState) == SongTimeline.FINISH_RANDOM ? FLAG_ERROR : FLAG_EMPTY_QUEUE;
|
||||||
synchronized (mStateLock) {
|
synchronized (mStateLock) {
|
||||||
updateState((mState | flag) & ~FLAG_NO_MEDIA);
|
updateState((mState | flag) & ~FLAG_NO_MEDIA);
|
||||||
@ -1448,7 +1444,7 @@ public final class PlaybackService extends Service
|
|||||||
|
|
||||||
public void onMediaChange()
|
public void onMediaChange()
|
||||||
{
|
{
|
||||||
if (MediaUtils.isSongAvailable(getContentResolver())) {
|
if (MediaUtils.isSongAvailable(getApplicationContext())) {
|
||||||
if ((mState & FLAG_NO_MEDIA) != 0)
|
if ((mState & FLAG_NO_MEDIA) != 0)
|
||||||
setCurrentSong(0);
|
setCurrentSong(0);
|
||||||
} else {
|
} else {
|
||||||
@ -1569,15 +1565,15 @@ public final class PlaybackService extends Service
|
|||||||
case MSG_UPDATE_PLAYCOUNTS:
|
case MSG_UPDATE_PLAYCOUNTS:
|
||||||
Song song = (Song)message.obj;
|
Song song = (Song)message.obj;
|
||||||
boolean played = message.arg1 == 1;
|
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
|
// Update the playcounts playlist in ~20% of all cases if enabled
|
||||||
if (mAutoPlPlaycounts > 0 && Math.random() > 0.8) {
|
if (mAutoPlPlaycounts > 0 && Math.random() > 0.8) {
|
||||||
ContentResolver resolver = getContentResolver();
|
ContentResolver resolver = getContentResolver();
|
||||||
// Add an invisible whitespace to adjust our sorting
|
// Add an invisible whitespace to adjust our sorting
|
||||||
String playlistName = "\u200B"+getString(R.string.autoplaylist_playcounts_name, mAutoPlPlaycounts);
|
String playlistName = "\u200B"+getString(R.string.autoplaylist_playcounts_name, mAutoPlPlaycounts);
|
||||||
long id = Playlist.createPlaylist(resolver, playlistName);
|
long id = Playlist.createPlaylist(getApplicationContext(), playlistName);
|
||||||
ArrayList<Long> items = mPlayCounts.getTopSongs(mAutoPlPlaycounts);
|
ArrayList<Long> items = PlayCountsHelper.getTopSongs(getApplicationContext(), mAutoPlPlaycounts);
|
||||||
Playlist.addToPlaylist(resolver, id, items);
|
Playlist.addToPlaylist(getApplicationContext(), id, items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -1670,25 +1666,20 @@ public final class PlaybackService extends Service
|
|||||||
public int deleteMedia(int type, long id)
|
public int deleteMedia(int type, long id)
|
||||||
{
|
{
|
||||||
int count = 0;
|
int count = 0;
|
||||||
|
String[] projection = new String [] { MediaLibrary.SongColumns._ID, MediaLibrary.SongColumns.PATH };
|
||||||
ContentResolver resolver = getContentResolver();
|
Cursor cursor = MediaUtils.buildQuery(type, id, projection, null).runQuery(getApplicationContext());
|
||||||
String[] projection = new String [] { MediaStore.Audio.Media._ID, MediaStore.Audio.Media.DATA };
|
|
||||||
Cursor cursor = MediaUtils.buildQuery(type, id, projection, null).runQuery(resolver);
|
|
||||||
|
|
||||||
if (cursor != null) {
|
if (cursor != null) {
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
if (new File(cursor.getString(1)).delete()) {
|
if (new File(cursor.getString(1)).delete()) {
|
||||||
long songId = cursor.getLong(0);
|
long songId = cursor.getLong(0);
|
||||||
String where = MediaStore.Audio.Media._ID + '=' + songId;
|
MediaLibrary.removeSong(getApplicationContext(), songId);
|
||||||
resolver.delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, where, null);
|
|
||||||
mTimeline.removeSong(songId);
|
mTimeline.removeSong(songId);
|
||||||
++count;
|
++count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor.close();
|
cursor.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1788,6 +1779,7 @@ public final class PlaybackService extends Service
|
|||||||
mHandler.sendMessage(mHandler.obtainMessage(MSG_QUERY, query));
|
mHandler.sendMessage(mHandler.obtainMessage(MSG_QUERY, query));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enqueues all the songs with the same album/artist/genre as the passed
|
* Enqueues all the songs with the same album/artist/genre as the passed
|
||||||
* song.
|
* song.
|
||||||
@ -1813,7 +1805,7 @@ public final class PlaybackService extends Service
|
|||||||
id = song.albumId;
|
id = song.albumId;
|
||||||
break;
|
break;
|
||||||
case MediaUtils.TYPE_GENRE:
|
case MediaUtils.TYPE_GENRE:
|
||||||
id = MediaUtils.queryGenreForSong(getContentResolver(), song.id);
|
id = MediaUtils.queryGenreForSong(getApplicationContext(), song.id);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("Unsupported media type: " + type);
|
throw new IllegalArgumentException("Unsupported media type: " + type);
|
||||||
|
@ -23,50 +23,49 @@
|
|||||||
|
|
||||||
package ch.blinkenlights.android.vanilla;
|
package ch.blinkenlights.android.vanilla;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import ch.blinkenlights.android.medialibrary.MediaLibrary;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.ContentUris;
|
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.ContentUris;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides various playlist-related utility functions.
|
* Provides various playlist-related utility functions.
|
||||||
*/
|
*/
|
||||||
public class Playlist {
|
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.
|
* @return The queried cursor.
|
||||||
*/
|
*/
|
||||||
public static Cursor queryPlaylists(ContentResolver resolver)
|
public static Cursor queryPlaylists(Context context) {
|
||||||
{
|
final String[] projection = { MediaLibrary.PlaylistColumns._ID, MediaLibrary.PlaylistColumns.NAME };
|
||||||
Uri media = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI;
|
final String sort = MediaStore.Audio.Playlists.NAME;
|
||||||
String[] projection = { MediaStore.Audio.Playlists._ID, MediaStore.Audio.Playlists.NAME };
|
return MediaLibrary.queryLibrary(context, MediaLibrary.TABLE_PLAYLISTS, projection, null, null, sort);
|
||||||
String sort = MediaStore.Audio.Playlists.NAME;
|
|
||||||
return MediaUtils.queryResolver(resolver, media, projection, null, null, sort);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the id for a playlist with the given name.
|
* 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.
|
* @param name The name of the playlist.
|
||||||
* @return The id of the playlist, or -1 if there is no playlist with the
|
* @return The id of the playlist, or -1 if there is no playlist with the
|
||||||
* given name.
|
* given name.
|
||||||
*/
|
*/
|
||||||
public static long getPlaylist(ContentResolver resolver, String name)
|
public static long getPlaylist(Context context, String name)
|
||||||
{
|
{
|
||||||
long id = -1;
|
long id = -1;
|
||||||
|
final String[] projection = { MediaLibrary.PlaylistColumns._ID };
|
||||||
Cursor cursor = MediaUtils.queryResolver(resolver, MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
|
final String selection = MediaLibrary.PlaylistColumns.NAME+"=?";
|
||||||
new String[] { MediaStore.Audio.Playlists._ID },
|
final String[] selectionArgs = { name };
|
||||||
MediaStore.Audio.Playlists.NAME + "=?",
|
Cursor cursor = MediaLibrary.queryLibrary(context, MediaLibrary.TABLE_PLAYLISTS, projection, selection, selectionArgs, null);
|
||||||
new String[] { name }, null);
|
|
||||||
|
|
||||||
if (cursor != null) {
|
if (cursor != null) {
|
||||||
if (cursor.moveToNext())
|
if (cursor.moveToNext())
|
||||||
@ -81,31 +80,17 @@ public class Playlist {
|
|||||||
* Create a new playlist with the given name. If a playlist with the given
|
* Create a new playlist with the given name. If a playlist with the given
|
||||||
* name already exists, it will be overwritten.
|
* 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.
|
* @param name The name of the playlist.
|
||||||
* @return The id of the new 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);
|
long id = getPlaylist(context, name);
|
||||||
|
if (id != -1)
|
||||||
if (id == -1) {
|
deletePlaylist(context, id);
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
id = MediaLibrary.createPlaylist(context, name);
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,59 +98,37 @@ public class Playlist {
|
|||||||
* Run the given query and add the results to the given playlist. Should be
|
* Run the given query and add the results to the given playlist. Should be
|
||||||
* run on a background thread.
|
* 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
|
* @param playlistId The MediaStore.Audio.Playlist id of the playlist to
|
||||||
* modify.
|
* modify.
|
||||||
* @param query The query to run. The audio id should be the first column.
|
* @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.
|
* @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>();
|
ArrayList<Long> result = new ArrayList<Long>();
|
||||||
Cursor cursor = query.runQuery(resolver);
|
Cursor cursor = query.runQuery(context);
|
||||||
if (cursor != null) {
|
if (cursor != null) {
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
result.add(cursor.getLong(0));
|
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
|
* Adds a set of audioIds to the given playlist. Should be
|
||||||
* run on a background thread.
|
* 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
|
* @param playlistId The MediaStore.Audio.Playlist id of the playlist to
|
||||||
* modify.
|
* modify.
|
||||||
* @param audioIds An ArrayList with all IDs to add
|
* @param audioIds An ArrayList with all IDs to add
|
||||||
* @return The number of songs that were added to the playlist.
|
* @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)
|
if (playlistId == -1)
|
||||||
return 0;
|
return 0;
|
||||||
|
return MediaLibrary.addToPlaylist(context, playlistId, audioIds);
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -194,26 +157,13 @@ public class Playlist {
|
|||||||
/**
|
/**
|
||||||
* Delete the playlist with the given id.
|
* Delete the playlist with the given id.
|
||||||
*
|
*
|
||||||
* @param resolver A ContentResolver to use.
|
* @param context the context to use
|
||||||
* @param id The Media.Audio.Playlists id of the playlist.
|
* @param id the id of the playlist.
|
||||||
*/
|
*/
|
||||||
public static void deletePlaylist(ContentResolver resolver, long id)
|
public static void deletePlaylist(Context context, long id) {
|
||||||
{
|
MediaLibrary.removePlaylist(context, id);
|
||||||
Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, id);
|
|
||||||
resolver.delete(uri, null, null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
* Rename the playlist with the given id.
|
||||||
@ -224,11 +174,14 @@ public class Playlist {
|
|||||||
*/
|
*/
|
||||||
public static void renamePlaylist(ContentResolver resolver, long id, String newName)
|
public static void renamePlaylist(ContentResolver resolver, long id, String newName)
|
||||||
{
|
{
|
||||||
|
/*
|
||||||
|
* FIXME: OBSOLETED CODE
|
||||||
long newId = createPlaylist(resolver, newName);
|
long newId = createPlaylist(resolver, newName);
|
||||||
if (newId != -1) { // new playlist created -> move stuff over
|
if (newId != -1) { // new playlist created -> move stuff over
|
||||||
_copyToPlaylist(resolver, id, newId);
|
_copyToPlaylist(resolver, id, newId);
|
||||||
deletePlaylist(resolver, id);
|
deletePlaylist(resolver, id);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -240,10 +193,10 @@ public class Playlist {
|
|||||||
*/
|
*/
|
||||||
public static long getFavoritesId(Context context, boolean create) {
|
public static long getFavoritesId(Context context, boolean create) {
|
||||||
String playlistName = context.getString(R.string.playlist_favorites);
|
String playlistName = context.getString(R.string.playlist_favorites);
|
||||||
long playlistId = getPlaylist(context.getContentResolver(), playlistName);
|
long playlistId = getPlaylist(context, playlistName);
|
||||||
|
|
||||||
if (playlistId == -1 && create == true)
|
if (playlistId == -1 && create == true)
|
||||||
playlistId = createPlaylist(context.getContentResolver(), playlistName);
|
playlistId = createPlaylist(context, playlistName);
|
||||||
|
|
||||||
return playlistId;
|
return playlistId;
|
||||||
}
|
}
|
||||||
@ -251,19 +204,20 @@ public class Playlist {
|
|||||||
/**
|
/**
|
||||||
* Searches for given song in given 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 playlistId The ID of the Playlist to query
|
||||||
* @param song The Song to search in given playlistId
|
* @param song The Song to search in given playlistId
|
||||||
* @return true if `song' was found in `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)
|
if (playlistId == -1 || song == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
boolean found = false;
|
boolean found = false;
|
||||||
String where = MediaStore.Audio.Playlists.Members.AUDIO_ID + "=" + song.id;
|
String selection = MediaLibrary.PlaylistSongColumns.PLAYLIST_ID+"=? AND "+MediaLibrary.PlaylistSongColumns.SONG_ID+"=?";
|
||||||
QueryTask query = MediaUtils.buildPlaylistQuery(playlistId, Song.EMPTY_PLAYLIST_PROJECTION, where);
|
String[] selectionArgs = { ""+playlistId, ""+song.id };
|
||||||
Cursor cursor = query.runQuery(resolver);
|
|
||||||
|
Cursor cursor = MediaLibrary.queryLibrary(context, MediaLibrary.TABLE_PLAYLISTS_SONGS, Song.EMPTY_PLAYLIST_PROJECTION, selection, selectionArgs, null);
|
||||||
if (cursor != null) {
|
if (cursor != null) {
|
||||||
found = cursor.getCount() != 0;
|
found = cursor.getCount() != 0;
|
||||||
cursor.close();
|
cursor.close();
|
||||||
|
@ -250,7 +250,7 @@ public class PlaylistActivity extends Activity
|
|||||||
}
|
}
|
||||||
case LibraryActivity.ACTION_PLAY_ALL:
|
case LibraryActivity.ACTION_PLAY_ALL:
|
||||||
case LibraryActivity.ACTION_ENQUEUE_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.mode = MODE_FOR_ACTION[action];
|
||||||
query.data = position - mListView.getHeaderViewsCount();
|
query.data = position - mListView.getHeaderViewsCount();
|
||||||
PlaybackService.get(this).addSongs(query);
|
PlaybackService.get(this).addSongs(query);
|
||||||
@ -275,7 +275,7 @@ public class PlaylistActivity extends Activity
|
|||||||
public void onClick(DialogInterface dialog, int which)
|
public void onClick(DialogInterface dialog, int which)
|
||||||
{
|
{
|
||||||
if (which == DialogInterface.BUTTON_POSITIVE) {
|
if (which == DialogInterface.BUTTON_POSITIVE) {
|
||||||
Playlist.deletePlaylist(getContentResolver(), mPlaylistId);
|
Playlist.deletePlaylist(this, mPlaylistId);
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2011 Christopher Eby <kreed@kreed.org>
|
* 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
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
@ -23,6 +23,8 @@
|
|||||||
|
|
||||||
package ch.blinkenlights.android.vanilla;
|
package ch.blinkenlights.android.vanilla;
|
||||||
|
|
||||||
|
import ch.blinkenlights.android.medialibrary.MediaLibrary;
|
||||||
|
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.ContentUris;
|
import android.content.ContentUris;
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
@ -38,19 +40,19 @@ import android.view.View;
|
|||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.CursorAdapter;
|
import android.widget.CursorAdapter;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.provider.MediaStore.Audio.Playlists.Members;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CursorAdapter backed by MediaStore playlists.
|
* CursorAdapter backed by MediaStore playlists.
|
||||||
*/
|
*/
|
||||||
public class PlaylistAdapter extends CursorAdapter implements Handler.Callback {
|
public class PlaylistAdapter extends CursorAdapter implements Handler.Callback {
|
||||||
|
|
||||||
private static final String[] PROJECTION = new String[] {
|
private static final String[] PROJECTION = new String[] {
|
||||||
MediaStore.Audio.Playlists.Members._ID,
|
MediaLibrary.PlaylistSongColumns._ID,
|
||||||
MediaStore.Audio.Playlists.Members.TITLE,
|
MediaLibrary.SongColumns.TITLE,
|
||||||
MediaStore.Audio.Playlists.Members.ARTIST,
|
MediaLibrary.ContributorColumns.ARTIST,
|
||||||
MediaStore.Audio.Playlists.Members.AUDIO_ID,
|
MediaLibrary.PlaylistSongColumns.SONG_ID,
|
||||||
MediaStore.Audio.Playlists.Members.ALBUM_ID,
|
MediaLibrary.SongColumns.ALBUM_ID,
|
||||||
MediaStore.Audio.Playlists.Members.PLAY_ORDER,
|
MediaLibrary.PlaylistSongColumns.POSITION,
|
||||||
};
|
};
|
||||||
|
|
||||||
private final Context mContext;
|
private final Context mContext;
|
||||||
@ -142,7 +144,7 @@ public class PlaylistAdapter extends CursorAdapter implements Handler.Callback {
|
|||||||
{
|
{
|
||||||
switch (message.what) {
|
switch (message.what) {
|
||||||
case MSG_RUN_QUERY: {
|
case MSG_RUN_QUERY: {
|
||||||
Cursor cursor = runQuery(mContext.getContentResolver());
|
Cursor cursor = runQuery();
|
||||||
mUiHandler.sendMessage(mUiHandler.obtainMessage(MSG_UPDATE_CURSOR, cursor));
|
mUiHandler.sendMessage(mUiHandler.obtainMessage(MSG_UPDATE_CURSOR, cursor));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -159,13 +161,12 @@ public class PlaylistAdapter extends CursorAdapter implements Handler.Callback {
|
|||||||
/**
|
/**
|
||||||
* Query the playlist songs.
|
* Query the playlist songs.
|
||||||
*
|
*
|
||||||
* @param resolver A ContentResolver to query with.
|
|
||||||
* @return The resulting cursor.
|
* @return The resulting cursor.
|
||||||
*/
|
*/
|
||||||
private Cursor runQuery(ContentResolver resolver)
|
private Cursor runQuery()
|
||||||
{
|
{
|
||||||
QueryTask query = MediaUtils.buildPlaylistQuery(mPlaylistId, PROJECTION, null);
|
QueryTask query = MediaUtils.buildPlaylistQuery(mPlaylistId, PROJECTION);
|
||||||
return query.runQuery(resolver);
|
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
|
// this can happen when the adapter changes during the drag
|
||||||
return;
|
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
|
* FIXME OBSOLETED
|
||||||
// after a song has been removed from the playlist (I think?).
|
|
||||||
|
|
||||||
ContentResolver resolver = mContext.getContentResolver();
|
ContentResolver resolver = mContext.getContentResolver();
|
||||||
Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", mPlaylistId);
|
Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", mPlaylistId);
|
||||||
Cursor cursor = getCursor();
|
Cursor cursor = getCursor();
|
||||||
@ -222,15 +221,16 @@ public class PlaylistAdapter extends CursorAdapter implements Handler.Callback {
|
|||||||
|
|
||||||
// insert the new rows
|
// insert the new rows
|
||||||
resolver.bulkInsert(uri, values);
|
resolver.bulkInsert(uri, values);
|
||||||
|
*/
|
||||||
changeCursor(runQuery(resolver));
|
changeCursor(runQuery());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeItem(int position)
|
public void removeItem(int position)
|
||||||
{
|
{
|
||||||
ContentResolver resolver = mContext.getContentResolver();
|
//ContentResolver resolver = mContext.getContentResolver();
|
||||||
Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", mPlaylistId);
|
//Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", mPlaylistId);
|
||||||
resolver.delete(ContentUris.withAppendedId(uri, getItemId(position)), null, null);
|
//resolver.delete(ContentUris.withAppendedId(uri, getItemId(position)), null, null);
|
||||||
|
// FIXME OBSOLETED
|
||||||
mUiHandler.sendEmptyMessage(MSG_RUN_QUERY);
|
mUiHandler.sendEmptyMessage(MSG_RUN_QUERY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,7 +83,7 @@ public class PlaylistDialog extends DialogFragment
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||||
Cursor cursor = Playlist.queryPlaylists(getActivity().getContentResolver());
|
Cursor cursor = Playlist.queryPlaylists(getActivity());
|
||||||
if (cursor == null)
|
if (cursor == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
@ -128,8 +128,7 @@ public class PlaylistInputDialog extends DialogFragment
|
|||||||
mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(false);
|
mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(false);
|
||||||
} else {
|
} else {
|
||||||
mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(true);
|
mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(true);
|
||||||
ContentResolver resolver = getActivity().getContentResolver();
|
int res = Playlist.getPlaylist(getActivity(), string) == -1 ? mActionRes : R.string.overwrite;
|
||||||
int res = Playlist.getPlaylist(resolver, string) == -1 ? mActionRes : R.string.overwrite;
|
|
||||||
mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setText(res);
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
* it under the terms of the GNU General Public License as published by
|
||||||
* in the Software without restriction, including without limitation the rights
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
* (at your option) any later version.
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
*
|
||||||
* The above copyright notice and this permission notice shall be included in
|
* This program is distributed in the hope that it will be useful,
|
||||||
* all copies or substantial portions of the Software.
|
* 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
|
* You should have received a copy of the GNU General Public License
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
package ch.blinkenlights.android.vanilla;
|
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.database.Cursor;
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a pending query.
|
* Represents a pending query.
|
||||||
*/
|
*/
|
||||||
public class QueryTask {
|
public class QueryTask {
|
||||||
public Uri uri;
|
public final String table;
|
||||||
public final String[] projection;
|
public final String[] projection;
|
||||||
public final String selection;
|
public final String selection;
|
||||||
public final String[] selectionArgs;
|
public final String[] selectionArgs;
|
||||||
@ -57,9 +54,8 @@ public class QueryTask {
|
|||||||
* Create the tasks. All arguments are passed directly to
|
* Create the tasks. All arguments are passed directly to
|
||||||
* ContentResolver.query().
|
* ContentResolver.query().
|
||||||
*/
|
*/
|
||||||
public QueryTask(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
|
public QueryTask(String table, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
|
||||||
{
|
this.table = table;
|
||||||
this.uri = uri;
|
|
||||||
this.projection = projection;
|
this.projection = projection;
|
||||||
this.selection = selection;
|
this.selection = selection;
|
||||||
this.selectionArgs = selectionArgs;
|
this.selectionArgs = selectionArgs;
|
||||||
@ -69,10 +65,9 @@ public class QueryTask {
|
|||||||
/**
|
/**
|
||||||
* Run the query. Should be called on a background thread.
|
* 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)
|
public Cursor runQuery(Context context) {
|
||||||
{
|
return MediaLibrary.queryLibrary(context, table, projection, selection, selectionArgs, sortOrder);
|
||||||
return MediaUtils.queryResolver(resolver, uri, projection, selection, selectionArgs, sortOrder);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,8 @@
|
|||||||
|
|
||||||
package ch.blinkenlights.android.vanilla;
|
package ch.blinkenlights.android.vanilla;
|
||||||
|
|
||||||
|
import ch.blinkenlights.android.medialibrary.MediaLibrary;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
@ -47,36 +49,27 @@ public class Song implements Comparable<Song> {
|
|||||||
|
|
||||||
|
|
||||||
public static final String[] EMPTY_PROJECTION = {
|
public static final String[] EMPTY_PROJECTION = {
|
||||||
MediaStore.Audio.Media._ID,
|
MediaLibrary.SongColumns._ID,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static final String[] FILLED_PROJECTION = {
|
public static final String[] FILLED_PROJECTION = {
|
||||||
MediaStore.Audio.Media._ID,
|
MediaLibrary.SongColumns._ID,
|
||||||
MediaStore.Audio.Media.DATA,
|
MediaLibrary.SongColumns.PATH,
|
||||||
MediaStore.Audio.Media.TITLE,
|
MediaLibrary.SongColumns.TITLE,
|
||||||
MediaStore.Audio.Media.ALBUM,
|
MediaLibrary.AlbumColumns.ALBUM,
|
||||||
MediaStore.Audio.Media.ARTIST,
|
MediaLibrary.ContributorColumns.ARTIST,
|
||||||
MediaStore.Audio.Media.ALBUM_ID,
|
MediaLibrary.SongColumns.ALBUM_ID,
|
||||||
MediaStore.Audio.Media.ARTIST_ID,
|
MediaLibrary.ContributorColumns.ARTIST_ID,
|
||||||
MediaStore.Audio.Media.DURATION,
|
MediaLibrary.SongColumns.DURATION,
|
||||||
MediaStore.Audio.Media.TRACK,
|
MediaLibrary.SongColumns.SONG_NUMBER,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static final String[] EMPTY_PLAYLIST_PROJECTION = {
|
public static final String[] EMPTY_PLAYLIST_PROJECTION = {
|
||||||
MediaStore.Audio.Playlists.Members.AUDIO_ID,
|
MediaLibrary.PlaylistSongColumns.SONG_ID,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static final String[] FILLED_PLAYLIST_PROJECTION = {
|
public static final String[] FILLED_PLAYLIST_PROJECTION =
|
||||||
MediaStore.Audio.Playlists.Members.AUDIO_ID,
|
FILLED_PROJECTION; // Same, as playlists are just a view of the view
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -23,6 +23,8 @@
|
|||||||
|
|
||||||
package ch.blinkenlights.android.vanilla;
|
package ch.blinkenlights.android.vanilla;
|
||||||
|
|
||||||
|
import ch.blinkenlights.android.medialibrary.MediaLibrary;
|
||||||
|
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
@ -326,7 +328,7 @@ public final class SongTimeline {
|
|||||||
|
|
||||||
// Fill the selection with the ids of all the saved songs
|
// Fill the selection with the ids of all the saved songs
|
||||||
// and initialize the timeline with unpopulated 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) {
|
for (int i = 0; i != n; ++i) {
|
||||||
long id = in.readLong();
|
long id = in.readLong();
|
||||||
if (id == -1)
|
if (id == -1)
|
||||||
@ -346,10 +348,8 @@ public final class SongTimeline {
|
|||||||
// return its results in.
|
// return its results in.
|
||||||
Collections.sort(songs, new IdComparator());
|
Collections.sort(songs, new IdComparator());
|
||||||
|
|
||||||
ContentResolver resolver = mContext.getContentResolver();
|
QueryTask query = new QueryTask(MediaLibrary.VIEW_SONGS_ALBUMS_ARTISTS, Song.FILLED_PROJECTION, selection.toString(), null, MediaLibrary.SongColumns._ID);
|
||||||
Uri media = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
Cursor cursor = query.runQuery(mContext);
|
||||||
|
|
||||||
Cursor cursor = MediaUtils.queryResolver(resolver, media, Song.FILLED_PROJECTION, selection.toString(), null, "_id");
|
|
||||||
if (cursor != null) {
|
if (cursor != null) {
|
||||||
if (cursor.getCount() != 0) {
|
if (cursor.getCount() != 0) {
|
||||||
cursor.moveToNext();
|
cursor.moveToNext();
|
||||||
@ -576,7 +576,7 @@ public final class SongTimeline {
|
|||||||
return null;
|
return null;
|
||||||
} else if (pos == size) {
|
} else if (pos == size) {
|
||||||
if (mFinishAction == FINISH_RANDOM) {
|
if (mFinishAction == FINISH_RANDOM) {
|
||||||
song = MediaUtils.getRandomSong(mContext.getContentResolver());
|
song = MediaUtils.getRandomSong(mContext);
|
||||||
if (song == null)
|
if (song == null)
|
||||||
return null;
|
return null;
|
||||||
timeline.add(song);
|
timeline.add(song);
|
||||||
@ -700,7 +700,7 @@ public final class SongTimeline {
|
|||||||
*/
|
*/
|
||||||
public int addSongs(Context context, QueryTask query)
|
public int addSongs(Context context, QueryTask query)
|
||||||
{
|
{
|
||||||
Cursor cursor = query.runQuery(context.getContentResolver());
|
Cursor cursor = query.runQuery(context);
|
||||||
if (cursor == null) {
|
if (cursor == null) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user