diff --git a/src/ch/blinkenlights/android/medialibrary/MediaLibrary.java b/src/ch/blinkenlights/android/medialibrary/MediaLibrary.java index b6408f2f..32b7fa1e 100644 --- a/src/ch/blinkenlights/android/medialibrary/MediaLibrary.java +++ b/src/ch/blinkenlights/android/medialibrary/MediaLibrary.java @@ -45,9 +45,18 @@ public class MediaLibrary { public static final int ROLE_ARTIST = 0; public static final int ROLE_COMPOSER = 1; + /** + * Our static backend instance + */ private static MediaLibraryBackend sBackend; - + /** + * An instance to the created scanner thread during our own creation + */ private static MediaScanner sScanner; + /** + * The observer to call-back during database changes + */ + private static ContentObserver sContentObserver; private static MediaLibraryBackend getBackend(Context context) { if (sBackend == null) { @@ -66,6 +75,28 @@ public class MediaLibrary { return sBackend; } + /** + * 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(ContentObserver observer) { + if (sContentObserver == null) { + sContentObserver = observer; + } else { + throw new IllegalStateException("ContentObserver was already registered"); + } + } + + /** + * Broadcasts a change to the observer, which will queue and dispatch + * the event to any registered observer + */ + static void notifyObserver() { + if (sContentObserver != null) + sContentObserver.onChange(true); + } /** * Perform a media query on the database, returns a cursor @@ -86,9 +117,14 @@ public class MediaLibrary { * * @param context the context to use * @param id the song id to delete + * @return the number of affected rows */ - public static void removeSong(Context context, long id) { - getBackend(context).delete(TABLE_SONGS, SongColumns._ID+"="+id, null); + public static int removeSong(Context context, long id) { + int rows = getBackend(context).delete(TABLE_SONGS, SongColumns._ID+"="+id, null); + + if (rows > 0) + notifyObserver(); + return rows; } /** @@ -115,7 +151,11 @@ public class MediaLibrary { ContentValues v = new ContentValues(); v.put(MediaLibrary.PlaylistColumns._ID, hash63(name)); v.put(MediaLibrary.PlaylistColumns.NAME, name); - return getBackend(context).insert(MediaLibrary.TABLE_PLAYLISTS, null, v); + long id = getBackend(context).insert(MediaLibrary.TABLE_PLAYLISTS, null, v); + + if (id != -1) + notifyObserver(); + return id; } /** @@ -129,7 +169,11 @@ public class MediaLibrary { // first, wipe all songs removeFromPlaylist(context, MediaLibrary.PlaylistSongColumns.PLAYLIST_ID+"="+id, null); int rows = getBackend(context).delete(MediaLibrary.TABLE_PLAYLISTS, MediaLibrary.PlaylistColumns._ID+"="+id, null); - return (rows > 0); + boolean removed = (rows > 0); + + if (removed) + notifyObserver(); + return removed; } /** @@ -163,7 +207,11 @@ public class MediaLibrary { bulk.add(v); pos++; } - return getBackend(context).bulkInsert(MediaLibrary.TABLE_PLAYLISTS_SONGS, null, bulk); + int rows = getBackend(context).bulkInsert(MediaLibrary.TABLE_PLAYLISTS_SONGS, null, bulk); + + if (rows > 0) + notifyObserver(); + return rows; } /** @@ -175,7 +223,11 @@ public class MediaLibrary { * @return the number of deleted rows, -1 on error */ public static int removeFromPlaylist(Context context, String selection, String[] selectionArgs) { - return getBackend(context).delete(MediaLibrary.TABLE_PLAYLISTS_SONGS, selection, selectionArgs); + int rows = getBackend(context).delete(MediaLibrary.TABLE_PLAYLISTS_SONGS, selection, selectionArgs); + + if (rows > 0) + notifyObserver(); + return rows; } /** @@ -195,6 +247,9 @@ public class MediaLibrary { getBackend(context).update(MediaLibrary.TABLE_PLAYLISTS_SONGS, v, selection, null); removePlaylist(context, playlistId); } + + if (newId != -1) + notifyObserver(); return newId; } @@ -238,25 +293,10 @@ public class MediaLibrary { v.put(MediaLibrary.PlaylistSongColumns.POSITION, toPos); selection = MediaLibrary.PlaylistSongColumns._ID+"="+from; getBackend(context).update(MediaLibrary.TABLE_PLAYLISTS_SONGS, v, selection, null); + + notifyObserver(); } - /** - * 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 diff --git a/src/ch/blinkenlights/android/medialibrary/MediaLibraryBackend.java b/src/ch/blinkenlights/android/medialibrary/MediaLibraryBackend.java index c22b186c..c853f499 100644 --- a/src/ch/blinkenlights/android/medialibrary/MediaLibraryBackend.java +++ b/src/ch/blinkenlights/android/medialibrary/MediaLibraryBackend.java @@ -19,7 +19,6 @@ package ch.blinkenlights.android.medialibrary; import android.content.Context; import android.content.ContentValues; -import android.database.ContentObserver; import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; @@ -54,10 +53,6 @@ public class MediaLibraryBackend extends SQLiteOpenHelper { * 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 @@ -101,28 +96,6 @@ public class MediaLibraryBackend extends SQLiteOpenHelper { 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 * @@ -136,7 +109,6 @@ public class MediaLibraryBackend extends SQLiteOpenHelper { int res = dbh.delete(table, whereClause, whereArgs); if (res > 0) { cleanOrphanedEntries(); - notifyObserver(); } return res; } @@ -153,11 +125,6 @@ public class MediaLibraryBackend extends SQLiteOpenHelper { public int update (String table, ContentValues values, String whereClause, String[] whereArgs) { SQLiteDatabase dbh = getWritableDatabase(); int res = dbh.update(table, values, whereClause, whereArgs); - if (res > 0) { - // Note: we are not running notifyObserver for performance reasons here - // Code which changes relations should just delete + re-insert data - notifyObserver(); - } return res; } @@ -185,8 +152,6 @@ public class MediaLibraryBackend extends SQLiteOpenHelper { // avoid logspam as done by insert() } - if (result != -1) - notifyObserver(); return result; } @@ -219,9 +184,6 @@ public class MediaLibraryBackend extends SQLiteOpenHelper { dbh.endTransaction(); } - if (count > 0) - notifyObserver(); - return count; } diff --git a/src/ch/blinkenlights/android/medialibrary/MediaScanner.java b/src/ch/blinkenlights/android/medialibrary/MediaScanner.java index 33718622..80e8ba8a 100644 --- a/src/ch/blinkenlights/android/medialibrary/MediaScanner.java +++ b/src/ch/blinkenlights/android/medialibrary/MediaScanner.java @@ -25,6 +25,7 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Message; import android.os.Process; +import android.os.SystemClock; import java.io.File; import java.util.ArrayList; @@ -32,6 +33,14 @@ import java.util.HashMap; import java.util.regex.Pattern; public class MediaScanner implements Handler.Callback { + /** + * How long to wait until we post an update notification + */ + private final static int SCAN_NOTIFY_DELAY_MS = 1200; + /** + * At which (up-)time we shall trigger the next notification + */ + private long mNextNotification = 0; /** * The backend instance we are acting on */ @@ -66,7 +75,8 @@ public class MediaScanner implements Handler.Callback { } private static final int MSG_SCAN_DIRECTORY = 1; - private static final int MSG_SCAN_FILE = 2; + private static final int MSG_ADD_FILE = 2; + @Override public boolean handleMessage(Message message) { File file = (File)message.obj; @@ -76,14 +86,27 @@ public class MediaScanner implements Handler.Callback { scanDirectory(file, recursive); break; } - case MSG_SCAN_FILE: { - scanFile(file); + case MSG_ADD_FILE: { + long now = SystemClock.uptimeMillis(); + boolean changed = addFile(file); + + // Notify the observer if this was the last message OR if the deadline was reached + if (mHandler.hasMessages(MSG_ADD_FILE) == false || (mNextNotification != 0 && now >= mNextNotification)) { + MediaLibrary.notifyObserver(); + mNextNotification = 0; + } + + // Initiate a new notification trigger if the old one fired and we got a change + if (changed && mNextNotification == 0) + mNextNotification = now + SCAN_NOTIFY_DELAY_MS; + break; } default: { throw new IllegalArgumentException(); } } + return true; } @@ -106,7 +129,7 @@ public class MediaScanner implements Handler.Callback { for (File file : dirents) { if (file.isFile()) { - mHandler.sendMessage(mHandler.obtainMessage(MSG_SCAN_FILE, 0, 0, file)); + mHandler.sendMessage(mHandler.obtainMessage(MSG_ADD_FILE, 0, 0, file)); } else if (file.isDirectory() && recursive) { mHandler.sendMessage(mHandler.obtainMessage(MSG_SCAN_DIRECTORY, 1, 0, file)); } @@ -126,23 +149,24 @@ public class MediaScanner implements Handler.Callback { /** * Scans a single file and adds it to the database * - * @param file the file to scan + * @param file the file to add + * @return true if we modified the database */ - private void scanFile(File file) { + private boolean addFile(File file) { String path = file.getAbsolutePath(); long songId = MediaLibrary.hash63(path); if (isBlacklisted(file)) - return; + return false; if (mBackend.isSongExisting(songId)) { Log.v("VanillaMusic", "Skipping already known song with id "+songId); - return; + return false; } MediaMetadataExtractor tags = new MediaMetadataExtractor(path); if (tags.isEmpty()) - return; // file does not contain audio data + return false; // file does not contain audio data // Get tags which always must be set String title = tags.getFirst(MediaMetadataExtractor.TITLE); @@ -227,8 +251,8 @@ public class MediaScanner implements Handler.Callback { mBackend.insert(MediaLibrary.TABLE_GENRES_SONGS, null, v); } } - Log.v("VanillaMusic", "MediaScanner: inserted "+path); + return true; } } diff --git a/src/ch/blinkenlights/android/vanilla/PlaybackService.java b/src/ch/blinkenlights/android/vanilla/PlaybackService.java index 3a8a96d5..8b3ecefa 100644 --- a/src/ch/blinkenlights/android/vanilla/PlaybackService.java +++ b/src/ch/blinkenlights/android/vanilla/PlaybackService.java @@ -483,7 +483,7 @@ public final class PlaybackService extends Service filter.addAction(Intent.ACTION_SCREEN_ON); registerReceiver(mReceiver, filter); - MediaLibrary.registerContentObserver(getApplicationContext(), mObserver); + MediaLibrary.registerContentObserver(mObserver); mRemoteControlClient = new RemoteControl().getClient(this); mRemoteControlClient.initializeRemote();