Merge branch 'medialibrary'
This commit is contained in:
commit
6c85a11743
@ -158,7 +158,6 @@ function show(n) {
|
||||
<li>Ferenc Nagy (icon)</li>
|
||||
<li>Jean-Baptiste Lab (notication color invert)</li>
|
||||
<li>Jean-François Im (cover art loading)</li>
|
||||
<li>Jeremy Erickson (SD-Scanner code)</li>
|
||||
<li>Jerry Liao (Chinese (Taiwan) translation)</li>
|
||||
<li>Jiri Grönroos (Finnish translation)</li>
|
||||
<li>Magnus Anderssen (headset button)</li>
|
||||
|
@ -38,24 +38,10 @@ Copied from SD Scanner's layout/main.xml with minor changes
|
||||
android:text="@string/button_start">
|
||||
<requestFocus />
|
||||
</Button>
|
||||
<ProgressBar
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:max="100"
|
||||
android:id="@+id/progress_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:gravity="center_vertical"
|
||||
android:progress="0" />
|
||||
<TextView
|
||||
android:id="@+id/progress_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="10000"
|
||||
android:text="@string/progress_unstarted_label" />
|
||||
<TextView
|
||||
android:id="@+id/debug_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:text="@string/empty" />
|
||||
</LinearLayout>
|
||||
|
588
src/ch/blinkenlights/android/medialibrary/MediaLibrary.java
Normal file
588
src/ch/blinkenlights/android/medialibrary/MediaLibrary.java
Normal file
@ -0,0 +1,588 @@
|
||||
/*
|
||||
* 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 android.os.Environment;
|
||||
|
||||
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";
|
||||
|
||||
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;
|
||||
/**
|
||||
* The lock we are using during object creation
|
||||
*/
|
||||
private static final Object[] sWait = new Object[0];
|
||||
|
||||
private static MediaLibraryBackend getBackend(Context context) {
|
||||
if (sBackend == null) {
|
||||
// -> unlikely
|
||||
synchronized(sWait) {
|
||||
if (sBackend == null) {
|
||||
sBackend = new MediaLibraryBackend(context);
|
||||
sScanner = new MediaScanner(context, sBackend);
|
||||
sScanner.startQuickScan();
|
||||
}
|
||||
}
|
||||
}
|
||||
return sBackend;
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers a rescan of the library
|
||||
*
|
||||
* @param context the context to use
|
||||
* @param forceFull starts a full / slow scan if true
|
||||
* @param drop drop the existing library if true
|
||||
*/
|
||||
public static void scanLibrary(Context context, boolean forceFull, boolean drop) {
|
||||
MediaLibraryBackend backend = getBackend(context); // also initialized sScanner
|
||||
if (drop) {
|
||||
backend.execSQL("DELETE FROM "+MediaLibrary.TABLE_SONGS);
|
||||
forceFull = true;
|
||||
// fixme: should clean orphaned AFTER scan finished
|
||||
}
|
||||
|
||||
if (forceFull) {
|
||||
sScanner.startFullScan();
|
||||
} else {
|
||||
sScanner.startNormalScan();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whacky function to get the current scan progress
|
||||
*
|
||||
* @param context the context to use
|
||||
* @return a description of the progress, null if no scan is running
|
||||
*/
|
||||
public static String describeScanProgress(Context context) {
|
||||
MediaLibraryBackend backend = getBackend(context); // also initialized sScanner
|
||||
MediaScanner.MediaScanPlan.Statistics stats = sScanner.getScanStatistics();
|
||||
String msg = null;
|
||||
if (stats.lastFile != null)
|
||||
msg = "seen files = "+stats.seen+", changes made = "+stats.changed+", currently scanning = "+stats.lastFile;
|
||||
return msg;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Registers a new content observer for the media library
|
||||
*
|
||||
* @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
|
||||
*
|
||||
* @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
|
||||
* @return the number of affected rows
|
||||
*/
|
||||
public static int removeSong(Context context, long id) {
|
||||
int rows = getBackend(context).delete(TABLE_SONGS, SongColumns._ID+"="+id, null);
|
||||
|
||||
if (rows > 0) {
|
||||
getBackend(context).cleanOrphanedEntries();
|
||||
notifyObserver();
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the play or skipcount of a song
|
||||
*
|
||||
* @param context the context to use
|
||||
* @param id the song id to update
|
||||
*/
|
||||
public static void updateSongPlayCounts(Context context, long id, boolean played) {
|
||||
final String column = played ? MediaLibrary.SongColumns.PLAYCOUNT : MediaLibrary.SongColumns.SKIPCOUNT;
|
||||
String selection = MediaLibrary.SongColumns._ID+"="+id;
|
||||
getBackend(context).execSQL("UPDATE "+MediaLibrary.TABLE_SONGS+" SET "+column+"="+column+"+1 WHERE "+selection);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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._ID, hash63(name));
|
||||
v.put(MediaLibrary.PlaylistColumns.NAME, name);
|
||||
long id = getBackend(context).insert(MediaLibrary.TABLE_PLAYLISTS, null, v);
|
||||
|
||||
if (id != -1)
|
||||
notifyObserver();
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
removeFromPlaylist(context, MediaLibrary.PlaylistSongColumns.PLAYLIST_ID+"="+id, null);
|
||||
int rows = getBackend(context).delete(MediaLibrary.TABLE_PLAYLISTS, MediaLibrary.PlaylistColumns._ID+"="+id, null);
|
||||
boolean removed = (rows > 0);
|
||||
|
||||
if (removed)
|
||||
notifyObserver();
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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+" DESC";
|
||||
Cursor cursor = queryLibrary(context, MediaLibrary.TABLE_PLAYLISTS_SONGS, projection, selection, null, order);
|
||||
if (cursor.moveToFirst())
|
||||
pos = cursor.getLong(0) + 1;
|
||||
cursor.close();
|
||||
|
||||
ArrayList<ContentValues> bulk = new ArrayList<>();
|
||||
for (Long id : ids) {
|
||||
if (getBackend(context).getSongMtime(id) == 0)
|
||||
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++;
|
||||
}
|
||||
int rows = getBackend(context).bulkInsert(MediaLibrary.TABLE_PLAYLISTS_SONGS, null, bulk);
|
||||
|
||||
if (rows > 0)
|
||||
notifyObserver();
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a set of items from a playlist
|
||||
*
|
||||
* @param context the context to use
|
||||
* @param selection the selection for the items to drop
|
||||
* @param selectionArgs arguments for `selection'
|
||||
* @return the number of deleted rows, -1 on error
|
||||
*/
|
||||
public static int removeFromPlaylist(Context context, String selection, String[] selectionArgs) {
|
||||
int rows = getBackend(context).delete(MediaLibrary.TABLE_PLAYLISTS_SONGS, selection, selectionArgs);
|
||||
|
||||
if (rows > 0)
|
||||
notifyObserver();
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames an existing playlist
|
||||
*
|
||||
* @param context the context to use
|
||||
* @param playlistId the id of the playlist to rename
|
||||
* @param newName the new name of the playlist
|
||||
* @return the id of the new playlist, -1 on error
|
||||
*/
|
||||
public static long renamePlaylist(Context context, long playlistId, String newName) {
|
||||
long newId = createPlaylist(context, newName);
|
||||
if (newId >= 0) {
|
||||
String selection = MediaLibrary.PlaylistSongColumns.PLAYLIST_ID+"="+playlistId;
|
||||
ContentValues v = new ContentValues();
|
||||
v.put(MediaLibrary.PlaylistSongColumns.PLAYLIST_ID, newId);
|
||||
getBackend(context).update(MediaLibrary.TABLE_PLAYLISTS_SONGS, v, selection, null);
|
||||
removePlaylist(context, playlistId);
|
||||
}
|
||||
|
||||
if (newId != -1)
|
||||
notifyObserver();
|
||||
return newId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves an item in a playlist. Note: both items should be in the
|
||||
* same playlist - 'fun things' will happen otherwise.
|
||||
*
|
||||
* @param context the context to use
|
||||
* @param from the _id of the 'dragged' element
|
||||
* @param to the _id of the 'repressed' element
|
||||
*/
|
||||
public static void movePlaylistItem(Context context, long from, long to) {
|
||||
long fromPos, toPos, playlistId;
|
||||
|
||||
String[] projection = { MediaLibrary.PlaylistSongColumns.POSITION, MediaLibrary.PlaylistSongColumns.PLAYLIST_ID };
|
||||
String selection = MediaLibrary.PlaylistSongColumns._ID+"=";
|
||||
|
||||
// Get playlist id and position of the 'from' item
|
||||
Cursor cursor = queryLibrary(context, MediaLibrary.TABLE_PLAYLISTS_SONGS, projection, selection+Long.toString(from), null, null);
|
||||
cursor.moveToFirst();
|
||||
fromPos = cursor.getLong(0);
|
||||
playlistId = cursor.getLong(1);
|
||||
cursor.close();
|
||||
|
||||
// Get position of the target item
|
||||
cursor = queryLibrary(context, MediaLibrary.TABLE_PLAYLISTS_SONGS, projection, selection+Long.toString(to), null, null);
|
||||
cursor.moveToFirst();
|
||||
toPos = cursor.getLong(0);
|
||||
cursor.close();
|
||||
|
||||
// Moving down -> We actually want to be below the target
|
||||
if (toPos > fromPos)
|
||||
toPos++;
|
||||
|
||||
// shift all rows +1
|
||||
String setArg = MediaLibrary.PlaylistSongColumns.POSITION+"="+MediaLibrary.PlaylistSongColumns.POSITION+"+1";
|
||||
selection = MediaLibrary.PlaylistSongColumns.PLAYLIST_ID+"="+playlistId+" AND "+MediaLibrary.PlaylistSongColumns.POSITION+" >= "+toPos;
|
||||
getBackend(context).execSQL("UPDATE "+MediaLibrary.TABLE_PLAYLISTS_SONGS+" SET "+setArg+" WHERE "+selection);
|
||||
|
||||
ContentValues v = new ContentValues();
|
||||
v.put(MediaLibrary.PlaylistSongColumns.POSITION, toPos);
|
||||
selection = MediaLibrary.PlaylistSongColumns._ID+"="+from;
|
||||
getBackend(context).update(MediaLibrary.TABLE_PLAYLISTS_SONGS, v, selection, null);
|
||||
|
||||
notifyObserver();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of songs in the music library
|
||||
*
|
||||
* @param context the context to use
|
||||
* @return the number of songs
|
||||
*/
|
||||
public static int getLibrarySize(Context context) {
|
||||
int count = 0;
|
||||
Cursor cursor = queryLibrary(context, TABLE_SONGS, new String[]{"count(*)"}, null, null, null);
|
||||
if (cursor.moveToFirst())
|
||||
count = cursor.getInt(0);
|
||||
cursor.close();
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple 63 bit hash function for strings
|
||||
*
|
||||
* @param str the string to hash
|
||||
* @return a positive long
|
||||
*/
|
||||
public static 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the guessed media paths for this device
|
||||
*
|
||||
* @return array with guessed directories
|
||||
*/
|
||||
public static File[] discoverMediaPaths() {
|
||||
ArrayList<File> scanTargets = new ArrayList<>();
|
||||
|
||||
// this should always exist
|
||||
scanTargets.add(Environment.getExternalStorageDirectory());
|
||||
|
||||
// this *may* exist
|
||||
File sdCard = new File("/storage/sdcard1");
|
||||
if (sdCard.isDirectory())
|
||||
scanTargets.add(sdCard);
|
||||
|
||||
return scanTargets.toArray(new File[scanTargets.size()]);
|
||||
}
|
||||
|
||||
// Columns of Song entries
|
||||
public interface SongColumns {
|
||||
/**
|
||||
* The id of this song in the database
|
||||
*/
|
||||
String _ID = "_id";
|
||||
/**
|
||||
* The title of this song
|
||||
*/
|
||||
String TITLE = "title";
|
||||
/**
|
||||
* The sortable title of this song
|
||||
*/
|
||||
String TITLE_SORT = "title_sort";
|
||||
/**
|
||||
* The position in the album of this song
|
||||
*/
|
||||
String SONG_NUMBER = "song_num";
|
||||
/**
|
||||
* The album where this song belongs to
|
||||
*/
|
||||
String ALBUM_ID = "album_id";
|
||||
/**
|
||||
* The year of this song
|
||||
*/
|
||||
String YEAR = "year";
|
||||
/**
|
||||
* How often the song was played
|
||||
*/
|
||||
String PLAYCOUNT = "playcount";
|
||||
/**
|
||||
* How often the song was skipped
|
||||
*/
|
||||
String SKIPCOUNT = "skipcount";
|
||||
/**
|
||||
* The duration of this song
|
||||
*/
|
||||
String DURATION = "duration";
|
||||
/**
|
||||
* The path to the music file
|
||||
*/
|
||||
String PATH = "path";
|
||||
/**
|
||||
* The mtime of this item
|
||||
*/
|
||||
String MTIME = "mtime";
|
||||
}
|
||||
|
||||
// Columns of Album entries
|
||||
public interface AlbumColumns {
|
||||
/**
|
||||
* The id of this album in the database
|
||||
*/
|
||||
String _ID = SongColumns._ID;
|
||||
/**
|
||||
* The title of this album
|
||||
*/
|
||||
String ALBUM = "album";
|
||||
/**
|
||||
* The sortable title of this album
|
||||
*/
|
||||
String ALBUM_SORT = "album_sort";
|
||||
/**
|
||||
* The disc number of this album
|
||||
*/
|
||||
String DISC_NUMBER = "disc_num";
|
||||
/**
|
||||
* The primary contributor / artist reference for this album
|
||||
*/
|
||||
String PRIMARY_ARTIST_ID = "primary_artist_id";
|
||||
/**
|
||||
* The year of this album
|
||||
*/
|
||||
String PRIMARY_ALBUM_YEAR = "primary_album_year";
|
||||
/**
|
||||
* The mtime of this item
|
||||
*/
|
||||
String MTIME = "mtime";
|
||||
}
|
||||
|
||||
// Columns of Contributors entries
|
||||
public interface ContributorColumns {
|
||||
/**
|
||||
* The id of this contributor
|
||||
*/
|
||||
String _ID = SongColumns._ID;
|
||||
/**
|
||||
* The name of this contributor
|
||||
*/
|
||||
String _CONTRIBUTOR = "_contributor";
|
||||
/**
|
||||
* The sortable title of this contributor
|
||||
*/
|
||||
String _CONTRIBUTOR_SORT = "_contributor_sort";
|
||||
/**
|
||||
* The mtime of this item
|
||||
*/
|
||||
String MTIME = "mtime";
|
||||
/**
|
||||
* ONLY IN VIEWS - the artist
|
||||
*/
|
||||
String ARTIST = "artist";
|
||||
/**
|
||||
* ONLY IN VIEWS - the artist_sort key
|
||||
*/
|
||||
String ARTIST_SORT = "artist_sort";
|
||||
/**
|
||||
* ONLY IN VIEWS - the artist id
|
||||
*/
|
||||
String ARTIST_ID = "artist_id";
|
||||
}
|
||||
|
||||
// Songs <-> Contributor mapping
|
||||
public interface ContributorSongColumns {
|
||||
/**
|
||||
* The role of this entry
|
||||
*/
|
||||
String ROLE = "role";
|
||||
/**
|
||||
* the contirbutor id this maps to
|
||||
*/
|
||||
String _CONTRIBUTOR_ID = "_contributor_id";
|
||||
/**
|
||||
* the song this maps to
|
||||
*/
|
||||
String SONG_ID = "song_id";
|
||||
}
|
||||
|
||||
// Columns of Genres entries
|
||||
public interface GenreColumns {
|
||||
/**
|
||||
* The id of this genre
|
||||
*/
|
||||
String _ID = SongColumns._ID;
|
||||
/**
|
||||
* The name of this genre
|
||||
*/
|
||||
String _GENRE = "_genre";
|
||||
/**
|
||||
* The sortable title of this genre
|
||||
*/
|
||||
String _GENRE_SORT = "_genre_sort";
|
||||
}
|
||||
|
||||
// Songs <-> Contributor mapping
|
||||
public interface GenreSongColumns {
|
||||
/**
|
||||
* the genre id this maps to
|
||||
*/
|
||||
String _GENRE_ID = "_genre_id";
|
||||
/**
|
||||
* the song this maps to
|
||||
*/
|
||||
String SONG_ID = "song_id";
|
||||
}
|
||||
|
||||
// Playlists
|
||||
public interface PlaylistColumns {
|
||||
/**
|
||||
* The id of this playlist
|
||||
*/
|
||||
String _ID = SongColumns._ID;
|
||||
/**
|
||||
* The name of this playlist
|
||||
*/
|
||||
String NAME = "name";
|
||||
}
|
||||
|
||||
// Song <-> Playlist mapping
|
||||
public interface PlaylistSongColumns {
|
||||
/**
|
||||
* The ID of this entry
|
||||
*/
|
||||
String _ID = SongColumns._ID;
|
||||
/**
|
||||
* The playlist this entry belongs to
|
||||
*/
|
||||
String PLAYLIST_ID = "playlist_id";
|
||||
/**
|
||||
* The song this entry references to
|
||||
*/
|
||||
String SONG_ID = "song_id";
|
||||
/**
|
||||
* The order attribute
|
||||
*/
|
||||
String POSITION = "position";
|
||||
}
|
||||
}
|
@ -0,0 +1,309 @@
|
||||
/*
|
||||
* 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.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";
|
||||
/**
|
||||
* 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+)$");
|
||||
|
||||
/**
|
||||
* 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 the modification time of a song, 0 if the song does not exist
|
||||
*
|
||||
* @param id the song id to query
|
||||
* @return the modification time of this song
|
||||
*/
|
||||
long getSongMtime(long id) {
|
||||
long mtime = 0;
|
||||
Cursor cursor = query(false, MediaLibrary.TABLE_SONGS, new String[]{ MediaLibrary.SongColumns.MTIME }, MediaLibrary.SongColumns._ID+"="+Long.toString(id), null, null, null, null, "1");
|
||||
if (cursor.moveToFirst())
|
||||
mtime = cursor.getLong(0);
|
||||
cursor.close();
|
||||
return mtime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
int delete(String table, String whereClause, String[] whereArgs) {
|
||||
SQLiteDatabase dbh = getWritableDatabase();
|
||||
return dbh.delete(table, whereClause, whereArgs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @return the number of affected rows
|
||||
*/
|
||||
int update (String table, ContentValues values, String whereClause, String[] whereArgs) {
|
||||
SQLiteDatabase dbh = getWritableDatabase();
|
||||
return dbh.update(table, values, whereClause, whereArgs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for SQLiteDatabase.execSQL() function
|
||||
*
|
||||
* @param sql the raw sql string
|
||||
*/
|
||||
void execSQL(String sql) {
|
||||
getWritableDatabase().execSQL(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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()
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Purges orphaned entries from the media library
|
||||
*/
|
||||
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+");");
|
||||
dbh.execSQL("DELETE FROM "+MediaLibrary.TABLE_PLAYLISTS_SONGS+" WHERE "+MediaLibrary.PlaylistSongColumns.SONG_ID+" NOT IN (SELECT "+MediaLibrary.SongColumns._ID+" FROM "+MediaLibrary.TABLE_SONGS+");");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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();
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrappr for SQLiteDatabase.query() function
|
||||
*/
|
||||
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) {
|
||||
return "SELECT "+MediaLibrary.GenreSongColumns.SONG_ID+" FROM "+MediaLibrary.TABLE_GENRES_SONGS+" WHERE "
|
||||
+MediaLibrary.GenreSongColumns._GENRE_ID+"="+genreId+" GROUP BY "+MediaLibrary.GenreSongColumns.SONG_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
return "SELECT "+target+" FROM "+MediaLibrary.VIEW_SONGS_ALBUMS_ARTISTS+" WHERE "
|
||||
+MediaLibrary.SongColumns._ID+" IN ("+genreSelect+") GROUP BY "+target;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,217 @@
|
||||
/*
|
||||
* 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.media.MediaMetadataRetriever;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class MediaMetadataExtractor extends HashMap<String, ArrayList<String>> {
|
||||
// Well known tags
|
||||
public final static String ALBUM = "ALBUM";
|
||||
public final static String ALBUMARTIST = "ALBUM_ARTIST";
|
||||
public final static String ARTIST = "ARTIST";
|
||||
public final static String BITRATE = "BITRATE";
|
||||
public final static String COMPOSER = "COMPOSER";
|
||||
public final static String DISC_COUNT = "DISC_COUNT";
|
||||
public final static String DISC_NUMBER = "DISC_NUMBER";
|
||||
public final static String DURATION = "DURATION";
|
||||
public final static String GENRE = "GENRE";
|
||||
public final static String MIME_TYPE = "MIME";
|
||||
public final static String TRACK_COUNT = "TRACK_COUNT";
|
||||
public final static String TRACK_NUMBER = "TRACK_NUM";
|
||||
public final static String TITLE = "TITLE";
|
||||
public final static String YEAR = "YEAR";
|
||||
|
||||
/**
|
||||
* Regexp used to match a year in a date field
|
||||
*/
|
||||
private static final Pattern sFilterYear = Pattern.compile("(\\d{4})");
|
||||
/**
|
||||
* Regexp matching the first lefthand integer
|
||||
*/
|
||||
private static final Pattern sFilterLeftInt = Pattern.compile("^0*(\\d+)");
|
||||
/**
|
||||
* Regexp matching anything
|
||||
*/
|
||||
private static final Pattern sFilterAny = Pattern.compile("^(.*)$");
|
||||
|
||||
/**
|
||||
* Constructor for MediaMetadataExtractor
|
||||
*
|
||||
* @param path the path to scan
|
||||
*/
|
||||
public MediaMetadataExtractor(String path) {
|
||||
extractMetadata(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first element matching this key, null on if not found
|
||||
*
|
||||
* @param key the key to look up
|
||||
* @return the value of the first entry, null if the key was not found
|
||||
*/
|
||||
public String getFirst(String key) {
|
||||
String result = null;
|
||||
if (containsKey(key))
|
||||
result = get(key).get(0);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this file contains any (interesting) tags
|
||||
* @return true if file is considered to be tagged
|
||||
*/
|
||||
public boolean isTagged() {
|
||||
return (containsKey(TITLE) || containsKey(ALBUM) || containsKey(ARTIST));
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to populate this instance with tags found in given path
|
||||
*
|
||||
* @param path the path to parse
|
||||
*/
|
||||
private void extractMetadata(String path) {
|
||||
if (!isEmpty())
|
||||
throw new IllegalStateException("Expected to be called on a clean HashMap");
|
||||
|
||||
HashMap bastpTags = (new Bastp()).getTags(path);
|
||||
MediaMetadataRetriever mediaTags = new MediaMetadataRetriever();
|
||||
try {
|
||||
mediaTags.setDataSource(path);
|
||||
} catch (Exception e) { /* we will later just check the contents of mediaTags */ }
|
||||
|
||||
// Check if this is an useable audio file
|
||||
if (mediaTags.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO) == null ||
|
||||
mediaTags.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO) != null ||
|
||||
mediaTags.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION) == null) {
|
||||
mediaTags.release();
|
||||
return;
|
||||
}
|
||||
|
||||
// Bastp can not read the duration and bitrates, so we always get it from the system
|
||||
ArrayList<String> duration = new ArrayList<>(1);
|
||||
duration.add(mediaTags.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION));
|
||||
this.put(DURATION, duration);
|
||||
|
||||
ArrayList<String> bitrate = new ArrayList<>(1);
|
||||
bitrate.add(mediaTags.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE));
|
||||
this.put(BITRATE, bitrate);
|
||||
|
||||
ArrayList<String> mime = new ArrayList<>(1);
|
||||
mime.add(mediaTags.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE));
|
||||
this.put(MIME_TYPE, mime);
|
||||
|
||||
|
||||
// ...but we are using bastp for FLAC, OGG and OPUS as it handles them well
|
||||
// Everything else goes to the framework (such as pcm, m4a and mp3)
|
||||
String bastpType = (bastpTags.containsKey("type") ? (String)bastpTags.get("type") : "");
|
||||
switch (bastpType) {
|
||||
case "FLAC":
|
||||
case "OGG":
|
||||
case "OPUS":
|
||||
populateSelf(bastpTags);
|
||||
break;
|
||||
default:
|
||||
populateSelf(mediaTags);
|
||||
}
|
||||
|
||||
mediaTags.release();
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates `this' with tags read from bastp
|
||||
*
|
||||
* @param bastp A hashmap as returned by bastp
|
||||
*/
|
||||
private void populateSelf(HashMap bastp) {
|
||||
// mapping between vorbiscomment -> constant
|
||||
String[] map = new String[]{ "TITLE", TITLE, "ARTIST", ARTIST, "ALBUM", ALBUM, "ALBUMARTIST", ALBUMARTIST, "COMPOSER", COMPOSER, "GENRE", GENRE,
|
||||
"TRACKNUMBER", TRACK_NUMBER, "TRACKTOTAL", TRACK_COUNT, "DISCNUMBER", DISC_NUMBER, "DISCTOTAL", DISC_COUNT,
|
||||
"YEAR", YEAR };
|
||||
// switch to integer filter if i >= x
|
||||
int filterByIntAt = 12;
|
||||
// the filter we are normally using
|
||||
Pattern filter = sFilterAny;
|
||||
|
||||
for (int i=0; i<map.length; i+=2) {
|
||||
if (i >= filterByIntAt)
|
||||
filter = sFilterLeftInt;
|
||||
|
||||
if (bastp.containsKey(map[i])) {
|
||||
addFiltered(filter, map[i+1], (ArrayList<String>)bastp.get(map[i]));
|
||||
}
|
||||
}
|
||||
|
||||
// Try to guess YEAR from date field if only DATE was specified
|
||||
// We expect it to match \d{4}
|
||||
if (!containsKey(YEAR) && bastp.containsKey("DATE")) {
|
||||
addFiltered(sFilterYear, YEAR, (ArrayList<String>)bastp.get("DATE"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates `this' with tags read from the MediaMetadataRetriever
|
||||
*
|
||||
* @param tags a MediaMetadataRetriever object
|
||||
*/
|
||||
private void populateSelf(MediaMetadataRetriever tags) {
|
||||
int[] mediaMap = new int[] { MediaMetadataRetriever.METADATA_KEY_TITLE, MediaMetadataRetriever.METADATA_KEY_ARTIST, MediaMetadataRetriever.METADATA_KEY_ALBUM,
|
||||
MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST, MediaMetadataRetriever.METADATA_KEY_COMPOSER, MediaMetadataRetriever.METADATA_KEY_GENRE,
|
||||
MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER, MediaMetadataRetriever.METADATA_KEY_YEAR };
|
||||
String[] selfMap = new String[]{ TITLE, ARTIST, ALBUM, ALBUMARTIST, COMPOSER, GENRE, TRACK_NUMBER, YEAR };
|
||||
int filterByIntAt = 6;
|
||||
Pattern filter = sFilterAny;
|
||||
|
||||
for (int i=0; i<selfMap.length; i++) {
|
||||
String data = tags.extractMetadata(mediaMap[i]);
|
||||
if (i >= filterByIntAt)
|
||||
filter = sFilterLeftInt;
|
||||
|
||||
if (data != null) {
|
||||
ArrayList<String> md = new ArrayList<String>(1);
|
||||
md.add(data);
|
||||
addFiltered(filter, selfMap[i], md);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches all elements of `data' with `filter' and adds the result as `key'
|
||||
*
|
||||
* @param filter the pattern to use, result is expected to be in capture group 1
|
||||
* @param key the key to use for the data to put
|
||||
* @param data the array list to inspect
|
||||
*/
|
||||
private void addFiltered(Pattern filter, String key, ArrayList<String> data) {
|
||||
ArrayList<String> list = new ArrayList<>();
|
||||
for (String s : data) {
|
||||
Matcher matcher = filter.matcher(s);
|
||||
if (matcher.matches()) {
|
||||
list.add(matcher.group(1));
|
||||
}
|
||||
}
|
||||
if (list.size() > 0)
|
||||
put(key, list);
|
||||
}
|
||||
|
||||
}
|
511
src/ch/blinkenlights/android/medialibrary/MediaScanner.java
Normal file
511
src/ch/blinkenlights/android/medialibrary/MediaScanner.java
Normal file
@ -0,0 +1,511 @@
|
||||
/*
|
||||
* 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.content.SharedPreferences;
|
||||
import android.database.Cursor;
|
||||
import android.database.ContentObserver;
|
||||
import android.util.Log;
|
||||
import android.provider.MediaStore;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.Message;
|
||||
import android.os.Process;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class MediaScanner implements Handler.Callback {
|
||||
/**
|
||||
* Our scan plan
|
||||
*/
|
||||
private MediaScanPlan mScanPlan;
|
||||
/**
|
||||
* Our message handler
|
||||
*/
|
||||
private Handler mHandler;
|
||||
/**
|
||||
* The context to use for native library queries
|
||||
*/
|
||||
private Context mContext;
|
||||
/**
|
||||
* Instance of a media backend
|
||||
*/
|
||||
private MediaLibraryBackend mBackend;
|
||||
|
||||
|
||||
MediaScanner(Context context, MediaLibraryBackend backend) {
|
||||
mContext = context;
|
||||
mBackend = backend;
|
||||
mScanPlan = new MediaScanPlan();
|
||||
HandlerThread handlerThread = new HandlerThread("MediaScannerThread", Process.THREAD_PRIORITY_LOWEST);
|
||||
handlerThread.start();
|
||||
mHandler = new Handler(handlerThread.getLooper(), this);
|
||||
|
||||
// the content observer to use
|
||||
ContentObserver mObserver = new ContentObserver(null) {
|
||||
@Override
|
||||
public void onChange(boolean self) {
|
||||
startQuickScan();
|
||||
}
|
||||
};
|
||||
context.getContentResolver().registerContentObserver(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, false, mObserver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a 'fast' scan by checking the native and our own
|
||||
* library for new and changed files
|
||||
*/
|
||||
public void startNormalScan() {
|
||||
mScanPlan.addNextStep(RPC_NATIVE_VRFY, null)
|
||||
.addNextStep(RPC_LIBRARY_VRFY, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a 'slow' scan by inspecting all files on the device
|
||||
*/
|
||||
public void startFullScan() {
|
||||
for (File dir : MediaLibrary.discoverMediaPaths()) {
|
||||
mScanPlan.addNextStep(RPC_READ_DIR, dir);
|
||||
}
|
||||
mScanPlan.addNextStep(RPC_LIBRARY_VRFY, null);
|
||||
mScanPlan.addNextStep(RPC_NATIVE_VRFY, null);
|
||||
mHandler.sendMessage(mHandler.obtainMessage(MSG_SCAN_RPC, RPC_NOOP, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the content observer if a change in the media library
|
||||
* has been detected
|
||||
*/
|
||||
public void startQuickScan() {
|
||||
if (!mHandler.hasMessages(MSG_SCAN_RPC)) {
|
||||
mScanPlan.addNextStep(RPC_NATIVE_VRFY, null)
|
||||
.addOptionalStep(RPC_LIBRARY_VRFY, null); // only runs if previous scan found no change
|
||||
mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SCAN_RPC, RPC_NOOP, 0), 1400);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns some scan statistics
|
||||
*
|
||||
* @return a stats object
|
||||
*/
|
||||
MediaScanPlan.Statistics getScanStatistics() {
|
||||
return mScanPlan.getStatistics();
|
||||
}
|
||||
|
||||
private static final int MSG_SCAN_RPC = 0;
|
||||
private static final int MSG_NOTIFY_CHANGE = 1;
|
||||
private static final int RPC_NOOP = 100;
|
||||
private static final int RPC_READ_DIR = 101;
|
||||
private static final int RPC_INSPECT_FILE = 102;
|
||||
private static final int RPC_LIBRARY_VRFY = 103;
|
||||
private static final int RPC_NATIVE_VRFY = 104;
|
||||
|
||||
@Override
|
||||
public boolean handleMessage(Message message) {
|
||||
int rpc = (message.what == MSG_SCAN_RPC ? message.arg1 : message.what);
|
||||
|
||||
switch (rpc) {
|
||||
case MSG_NOTIFY_CHANGE: {
|
||||
MediaLibrary.notifyObserver();
|
||||
break;
|
||||
}
|
||||
case RPC_NOOP: {
|
||||
// just used to trigger the initial scan
|
||||
break;
|
||||
}
|
||||
case RPC_INSPECT_FILE: {
|
||||
final File file = (File)message.obj;
|
||||
boolean changed = rpcInspectFile(file);
|
||||
mScanPlan.registerProgress(file.toString(), changed);
|
||||
if (changed && !mHandler.hasMessages(MSG_NOTIFY_CHANGE)) {
|
||||
mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_NOTIFY_CHANGE), 500);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case RPC_READ_DIR: {
|
||||
rpcReadDirectory((File)message.obj);
|
||||
break;
|
||||
}
|
||||
case RPC_LIBRARY_VRFY: {
|
||||
rpcLibraryVerify((Cursor)message.obj);
|
||||
break;
|
||||
}
|
||||
case RPC_NATIVE_VRFY: {
|
||||
rpcNativeVerify((Cursor)message.obj, message.arg2);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
if (message.what == MSG_SCAN_RPC && !mHandler.hasMessages(MSG_SCAN_RPC)) {
|
||||
MediaScanPlan.Step step = mScanPlan.getNextStep();
|
||||
if (step == null) {
|
||||
Log.v("VanillaMusic", "--- all scanners finished ---");
|
||||
} else {
|
||||
Log.v("VanillaMusic", "--- starting scan of type "+step.msg);
|
||||
mHandler.sendMessage(mHandler.obtainMessage(MSG_SCAN_RPC, step.msg, 0, step.arg));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans the android library, inspecting every found file
|
||||
*
|
||||
* @param cursor the cursor we are using
|
||||
* @param mtime the mtime to carry over, ignored if cursor is null
|
||||
*/
|
||||
private void rpcNativeVerify(Cursor cursor, int mtime) {
|
||||
if (cursor == null) {
|
||||
mtime = getSetScanMark(-1); // starting a new scan -> read stored mtime from preferences
|
||||
String selection = MediaStore.Audio.Media.IS_MUSIC + "!= 0 AND "+ MediaStore.MediaColumns.DATE_MODIFIED +" > " + mtime;
|
||||
String sort = MediaStore.MediaColumns.DATE_MODIFIED;
|
||||
String[] projection = { MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.DATE_MODIFIED };
|
||||
try {
|
||||
cursor = mContext.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection, null, sort);
|
||||
} catch(SecurityException e) {
|
||||
Log.e("VanillaMusic", "rpcNativeVerify failed: "+e);
|
||||
}
|
||||
}
|
||||
|
||||
if (cursor == null)
|
||||
return; // still null.. fixme: handle me better
|
||||
|
||||
if (cursor.moveToNext()) {
|
||||
String path = cursor.getString(0);
|
||||
mtime = cursor.getInt(1);
|
||||
if (path != null) { // this seems to be a thing...
|
||||
File entry = new File(path);
|
||||
mHandler.sendMessage(mHandler.obtainMessage(MSG_SCAN_RPC, RPC_INSPECT_FILE, 0, entry));
|
||||
mHandler.sendMessage(mHandler.obtainMessage(MSG_SCAN_RPC, RPC_NATIVE_VRFY, mtime, cursor));
|
||||
}
|
||||
} else {
|
||||
cursor.close();
|
||||
getSetScanMark(mtime);
|
||||
Log.v("VanillaMusic", "NativeLibraryScanner finished, mtime mark is now at "+mtime);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Scans every file in our own library and checks for changes
|
||||
*
|
||||
* @param cursor the cursor we are using
|
||||
*/
|
||||
private void rpcLibraryVerify(Cursor cursor) {
|
||||
if (cursor == null)
|
||||
cursor = mBackend.query(false, MediaLibrary.TABLE_SONGS, new String[]{MediaLibrary.SongColumns.PATH}, null, null, null, null, null, null);
|
||||
|
||||
if (cursor.moveToNext()) {
|
||||
File entry = new File(cursor.getString(0));
|
||||
mHandler.sendMessage(mHandler.obtainMessage(MSG_SCAN_RPC, RPC_INSPECT_FILE, 0, entry));
|
||||
mHandler.sendMessage(mHandler.obtainMessage(MSG_SCAN_RPC, RPC_LIBRARY_VRFY, 0, cursor));
|
||||
} else {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loops trough given directory and adds all found
|
||||
* files to the scan queue
|
||||
*
|
||||
* @param dir the directory to scan
|
||||
*/
|
||||
private void rpcReadDirectory(File dir) {
|
||||
if (!dir.isDirectory())
|
||||
return;
|
||||
|
||||
if (new File(dir, ".nomedia").exists())
|
||||
return;
|
||||
|
||||
File[] dirents = dir.listFiles();
|
||||
if (dirents == null)
|
||||
return;
|
||||
|
||||
for (File file : dirents) {
|
||||
int rpc = (file.isFile() ? RPC_INSPECT_FILE : RPC_READ_DIR);
|
||||
mHandler.sendMessage(mHandler.obtainMessage(MSG_SCAN_RPC, rpc, 0, file));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspects a single file and adds it to the database or removes it. maybe.
|
||||
*
|
||||
* @param file the file to add
|
||||
* @return true if we modified the database
|
||||
*/
|
||||
private boolean rpcInspectFile(File file) {
|
||||
String path = file.getAbsolutePath();
|
||||
long songId = MediaLibrary.hash63(path);
|
||||
|
||||
if (isBlacklisted(file))
|
||||
return false;
|
||||
|
||||
long dbEntryMtime = mBackend.getSongMtime(songId) * 1000; // this is in unixtime -> convert to 'ms'
|
||||
long fileMtime = file.lastModified();
|
||||
boolean needsInsert = true;
|
||||
boolean needsCleanup = false;
|
||||
|
||||
if (fileMtime > 0 && dbEntryMtime >= fileMtime) {
|
||||
return false; // on-disk mtime is older than db mtime and it still exists -> nothing to do
|
||||
}
|
||||
|
||||
if (dbEntryMtime != 0) {
|
||||
// DB entry exists but is outdated - drop current entry and maybe re-insert it
|
||||
// fixme: drops play counts :-(
|
||||
mBackend.delete(MediaLibrary.TABLE_SONGS, MediaLibrary.SongColumns._ID+"="+songId, null);
|
||||
needsCleanup = true;
|
||||
}
|
||||
|
||||
MediaMetadataExtractor tags = new MediaMetadataExtractor(path);
|
||||
if (!tags.isTagged()) {
|
||||
needsInsert = false; // does not have any useable metadata: wont insert even if it is a playable file
|
||||
}
|
||||
|
||||
if (needsInsert) {
|
||||
// Get tags which always must be set
|
||||
String title = tags.getFirst(MediaMetadataExtractor.TITLE);
|
||||
if (title == null)
|
||||
title = "Untitled";
|
||||
|
||||
String album = tags.getFirst(MediaMetadataExtractor.ALBUM);
|
||||
if (album == null)
|
||||
album = "No Album";
|
||||
|
||||
String artist = tags.getFirst(MediaMetadataExtractor.ARTIST);
|
||||
if (artist == null)
|
||||
artist = "No Artist";
|
||||
|
||||
|
||||
long albumId = MediaLibrary.hash63(album);
|
||||
long artistId = MediaLibrary.hash63(artist);
|
||||
|
||||
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, tags.getFirst(MediaMetadataExtractor.DURATION));
|
||||
v.put(MediaLibrary.SongColumns.SONG_NUMBER, tags.getFirst(MediaMetadataExtractor.TRACK_NUMBER));
|
||||
v.put(MediaLibrary.SongColumns.YEAR, tags.getFirst(MediaMetadataExtractor.YEAR));
|
||||
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);
|
||||
v.put(MediaLibrary.AlbumColumns.PRIMARY_ALBUM_YEAR,tags.getFirst(MediaMetadataExtractor.YEAR));
|
||||
v.put(MediaLibrary.AlbumColumns.DISC_NUMBER, tags.getFirst(MediaMetadataExtractor.DISC_NUMBER));
|
||||
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, MediaLibrary.ROLE_ARTIST);
|
||||
mBackend.insert(MediaLibrary.TABLE_CONTRIBUTORS_SONGS, null, v);
|
||||
|
||||
// Composers are optional: only add if we found it
|
||||
String composer = tags.getFirst(MediaMetadataExtractor.COMPOSER);
|
||||
if (composer != null) {
|
||||
long composerId = MediaLibrary.hash63(composer);
|
||||
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, MediaLibrary.ROLE_COMPOSER);
|
||||
mBackend.insert(MediaLibrary.TABLE_CONTRIBUTORS_SONGS, null, v);
|
||||
}
|
||||
|
||||
// A song might be in multiple genres
|
||||
if (tags.containsKey(MediaMetadataExtractor.GENRE)) {
|
||||
ArrayList<String> genres = tags.get(MediaMetadataExtractor.GENRE);
|
||||
for (String genre : genres) {
|
||||
long genreId = MediaLibrary.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);
|
||||
}
|
||||
}
|
||||
} // end if (needsInsert)
|
||||
|
||||
|
||||
if (needsCleanup)
|
||||
mBackend.cleanOrphanedEntries();
|
||||
|
||||
Log.v("VanillaMusic", "MediaScanner: inserted "+path);
|
||||
return (needsInsert || needsCleanup);
|
||||
}
|
||||
|
||||
private static final Pattern sIgnoredNames = Pattern.compile("^([^\\.]+|.+\\.(jpe?g|gif|png|bmp|webm|txt|pdf|avi|mp4|mkv|zip|tgz|xml))$", Pattern.CASE_INSENSITIVE);
|
||||
/**
|
||||
* Returns true if the file should not be scanned
|
||||
*
|
||||
* @param file the file to inspect
|
||||
* @return boolean
|
||||
*/
|
||||
private boolean isBlacklisted(File file) {
|
||||
return sIgnoredNames.matcher(file.getName()).matches();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clunky shortcut to preferences editor
|
||||
*
|
||||
* @param newVal the new value to store, ignored if < 0
|
||||
* @return the value previously set, or 0 as a default
|
||||
*/
|
||||
private int getSetScanMark(int newVal) {
|
||||
final String prefKey = "native_last_mtime";
|
||||
SharedPreferences sharedPref = mContext.getSharedPreferences("scanner_preferences", Context.MODE_PRIVATE);
|
||||
int oldVal = sharedPref.getInt(prefKey, 0);
|
||||
|
||||
if (newVal >= 0) {
|
||||
SharedPreferences.Editor editor = sharedPref.edit();
|
||||
editor.putInt(prefKey, newVal);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
return oldVal;
|
||||
}
|
||||
|
||||
// MediaScanPlan describes how we are going to perform the media scan
|
||||
class MediaScanPlan {
|
||||
class Step {
|
||||
int msg;
|
||||
Object arg;
|
||||
boolean optional;
|
||||
Step (int msg, Object arg, boolean optional) {
|
||||
this.msg = msg;
|
||||
this.arg = arg;
|
||||
this.optional = optional;
|
||||
}
|
||||
}
|
||||
|
||||
class Statistics {
|
||||
String lastFile;
|
||||
int seen = 0;
|
||||
int changed = 0;
|
||||
void reset() {
|
||||
this.seen = 0;
|
||||
this.changed = 0;
|
||||
this.lastFile = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* All steps in this plan
|
||||
*/
|
||||
private ArrayList<Step> mSteps;
|
||||
/**
|
||||
* Statistics of the currently running step
|
||||
*/
|
||||
private Statistics mStats;
|
||||
|
||||
MediaScanPlan() {
|
||||
mSteps = new ArrayList<>();
|
||||
mStats = new Statistics();
|
||||
}
|
||||
|
||||
Statistics getStatistics() {
|
||||
return mStats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the scanner to signal that a file was handled
|
||||
*
|
||||
* @param path the file we scanned
|
||||
* @param changed true if this triggered a database update
|
||||
*/
|
||||
void registerProgress(String path, boolean changed) {
|
||||
mStats.lastFile = path;
|
||||
mStats.seen++;
|
||||
if (changed) {
|
||||
mStats.changed++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the next step in our plan
|
||||
*
|
||||
* @param msg the message to add
|
||||
* @param arg the argument to msg
|
||||
*/
|
||||
MediaScanPlan addNextStep(int msg, Object arg) {
|
||||
mSteps.add(new Step(msg, arg, false));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an optional step to our plan. This will NOT
|
||||
* run if the previous step caused database changes
|
||||
*
|
||||
* @param msg the message to add
|
||||
* @param arg the argument to msg
|
||||
*/
|
||||
MediaScanPlan addOptionalStep(int msg, Object arg) {
|
||||
mSteps.add(new Step(msg, arg, true));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next step of our scan plan
|
||||
*
|
||||
* @return a new step object, null if we hit the end
|
||||
*/
|
||||
Step getNextStep() {
|
||||
Step next = (mSteps.size() != 0 ? mSteps.remove(0) : null);
|
||||
if (next != null) {
|
||||
if (next.optional && mStats.changed != 0) {
|
||||
next = null;
|
||||
mSteps.clear();
|
||||
}
|
||||
}
|
||||
mStats.reset();
|
||||
return next;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
205
src/ch/blinkenlights/android/medialibrary/MediaSchema.java
Normal file
205
src/ch/blinkenlights/android/medialibrary/MediaSchema.java
Normal file
@ -0,0 +1,205 @@
|
||||
/*
|
||||
* 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.YEAR +" 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 (strftime('%s', 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.DISC_NUMBER +" INTEGER, "
|
||||
+ MediaLibrary.AlbumColumns.PRIMARY_ALBUM_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+")"
|
||||
+";";
|
||||
|
||||
/**
|
||||
* Index to select a song from a playlist quickly
|
||||
*/
|
||||
private static final String INDEX_IDX_PLAYLIST_ID_SONG = "CREATE INDEX idx_playlist_id_song ON "+MediaLibrary.TABLE_PLAYLISTS_SONGS
|
||||
+" ("+MediaLibrary.PlaylistSongColumns.PLAYLIST_ID+", "+MediaLibrary.PlaylistSongColumns.SONG_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+"="+MediaLibrary.ROLE_ARTIST
|
||||
+" 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+"="+MediaLibrary.ROLE_ARTIST+" 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+"="+MediaLibrary.ROLE_ARTIST
|
||||
+" 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(INDEX_IDX_PLAYLIST_ID_SONG);
|
||||
dbh.execSQL(VIEW_CREATE_SONGS_ALBUMS_ARTISTS);
|
||||
dbh.execSQL(VIEW_CREATE_ALBUMS_ARTISTS);
|
||||
dbh.execSQL(VIEW_CREATE_ARTISTS);
|
||||
dbh.execSQL(VIEW_CREATE_PLAYLIST_SONGS);
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2014-2015 Adrian Ulrich <adrian@blinkenlights.ch>
|
||||
* Copyright (C) 2014-2016 Adrian Ulrich <adrian@blinkenlights.ch>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@ -22,6 +22,7 @@ import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.provider.MediaStore;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.widget.TextView;
|
||||
@ -124,10 +125,24 @@ public class AudioPickerActivity extends PlaybackActivity {
|
||||
Song song = new Song(-1);
|
||||
Cursor cursor = null;
|
||||
|
||||
if (uri.getScheme().equals("content"))
|
||||
cursor = MediaUtils.queryResolver(getContentResolver(), uri, Song.FILLED_PROJECTION, null, null, null);
|
||||
if (uri.getScheme().equals("file"))
|
||||
if (uri.getScheme().equals("content")) {
|
||||
// check if the native content resolver has a path for this
|
||||
Cursor pathCursor = getContentResolver().query(uri, new String[]{ MediaStore.Audio.Media.DATA }, null, null, null);
|
||||
if (pathCursor != null) {
|
||||
if (pathCursor.moveToNext()) {
|
||||
String mediaPath = pathCursor.getString(0);
|
||||
if (mediaPath != null) { // this happens on android 4.x sometimes?!
|
||||
QueryTask query = MediaUtils.buildFileQuery(mediaPath, Song.FILLED_PROJECTION);
|
||||
cursor = query.runQuery(this);
|
||||
}
|
||||
}
|
||||
pathCursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
if (uri.getScheme().equals("file")) {
|
||||
cursor = MediaUtils.getCursorForFileQuery(uri.getPath());
|
||||
}
|
||||
|
||||
if (cursor != null) {
|
||||
if (cursor.moveToNext()) {
|
||||
|
@ -28,6 +28,7 @@ import android.graphics.Bitmap.CompressFormat;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
import android.provider.MediaStore;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
@ -255,7 +256,7 @@ public class CoverCache {
|
||||
dbh.delete(TABLE_NAME, "1", null);
|
||||
} else if (availableSpace < 0) {
|
||||
// Try to evict all expired entries first
|
||||
int affected = dbh.delete(TABLE_NAME, "expires < ?", new String[] {""+getUnixTime()});
|
||||
int affected = dbh.delete(TABLE_NAME, "expires < ?", new String[] { Long.toString(getUnixTime())});
|
||||
if (affected > 0)
|
||||
availableSpace = maxCacheSize - getUsedSpace();
|
||||
|
||||
@ -266,7 +267,7 @@ public class CoverCache {
|
||||
while (cursor.moveToNext() && availableSpace < 0) {
|
||||
int id = cursor.getInt(0);
|
||||
int size = cursor.getInt(1);
|
||||
dbh.delete(TABLE_NAME, "id=?", new String[] {""+id});
|
||||
dbh.delete(TABLE_NAME, "id=?", new String[] { Long.toString(id) });
|
||||
availableSpace += size;
|
||||
}
|
||||
cursor.close();
|
||||
@ -361,7 +362,7 @@ public class CoverCache {
|
||||
|
||||
SQLiteDatabase dbh = getWritableDatabase(); // may also delete
|
||||
String selection = "id=?";
|
||||
String[] selectionArgs = { ""+key.hashCode() };
|
||||
String[] selectionArgs = { Long.toString(key.hashCode()) };
|
||||
Cursor cursor = dbh.query(TABLE_NAME, FULL_PROJECTION, selection, selectionArgs, null, null, null);
|
||||
if (cursor != null) {
|
||||
if (cursor.moveToFirst()) {
|
||||
@ -444,12 +445,24 @@ public class CoverCache {
|
||||
}
|
||||
|
||||
if (inputStream == null && (CoverCache.mCoverLoadMode & CoverCache.COVER_MODE_ANDROID) != 0) {
|
||||
Uri uri = Uri.parse("content://media/external/audio/albumart/"+song.albumId);
|
||||
|
||||
long albumId = -1;
|
||||
ContentResolver res = mContext.getContentResolver();
|
||||
sampleInputStream = res.openInputStream(uri);
|
||||
if (sampleInputStream != null) // cache misses are VERY expensive here, so we check if the first open worked
|
||||
inputStream = res.openInputStream(uri);
|
||||
Uri contentUri = MediaStore.Audio.Media.getContentUriForPath(song.path);
|
||||
|
||||
// Lookup the album id assigned to this path in the android media store
|
||||
Cursor cursor = res.query(contentUri, new String[]{ MediaStore.Audio.Media.ALBUM_ID }, MediaStore.Audio.Media.DATA+"=?", new String[] { song.path }, null);
|
||||
if (cursor.moveToFirst()) {
|
||||
albumId = cursor.getLong(0);
|
||||
}
|
||||
cursor.close();
|
||||
|
||||
if (albumId != -1) {
|
||||
// now we can query for the album art path if we found an album id
|
||||
Uri uri = Uri.parse("content://media/external/audio/albumart/"+albumId);
|
||||
sampleInputStream = res.openInputStream(uri);
|
||||
if (sampleInputStream != null) // cache misses are VERY expensive here, so we check if the first open worked
|
||||
inputStream = res.openInputStream(uri);
|
||||
}
|
||||
}
|
||||
|
||||
if (inputStream != null) {
|
||||
|
@ -23,17 +23,17 @@
|
||||
|
||||
package ch.blinkenlights.android.vanilla;
|
||||
|
||||
import ch.blinkenlights.android.medialibrary.MediaMetadataExtractor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Color;
|
||||
import android.media.MediaMetadataRetriever;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Message;
|
||||
import android.util.Log;
|
||||
import android.content.ContentResolver;
|
||||
import android.view.Gravity;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
@ -340,7 +340,7 @@ public class FullPlaybackActivity extends SlidingPlaybackActivity
|
||||
PlaylistTask playlistTask = new PlaylistTask(playlistId, getString(R.string.playlist_favorites));
|
||||
playlistTask.audioIds = new ArrayList<Long>();
|
||||
playlistTask.audioIds.add(song.id);
|
||||
int action = Playlist.isInPlaylist(getContentResolver(), playlistId, song) ? MSG_REMOVE_FROM_PLAYLIST : MSG_ADD_TO_PLAYLIST;
|
||||
int action = Playlist.isInPlaylist(this, playlistId, song) ? MSG_REMOVE_FROM_PLAYLIST : MSG_ADD_TO_PLAYLIST;
|
||||
mHandler.sendMessage(mHandler.obtainMessage(action, playlistTask));
|
||||
}
|
||||
break;
|
||||
@ -496,36 +496,17 @@ public class FullPlaybackActivity extends SlidingPlaybackActivity
|
||||
mReplayGain = null;
|
||||
|
||||
if(song != null) {
|
||||
MediaMetadataExtractor data = new MediaMetadataExtractor(song.path);
|
||||
|
||||
MediaMetadataRetriever data = new MediaMetadataRetriever();
|
||||
|
||||
try {
|
||||
data.setDataSource(song.path);
|
||||
} catch (Exception e) {
|
||||
Log.w("VanillaMusic", "Failed to extract metadata from " + song.path);
|
||||
}
|
||||
|
||||
mGenre = data.extractMetadata(MediaMetadataRetriever.METADATA_KEY_GENRE);
|
||||
mTrack = data.extractMetadata(MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER);
|
||||
String composer = data.extractMetadata(MediaMetadataRetriever.METADATA_KEY_COMPOSER);
|
||||
if (composer == null)
|
||||
composer = data.extractMetadata(MediaMetadataRetriever.METADATA_KEY_WRITER);
|
||||
mComposer = composer;
|
||||
|
||||
String year = data.extractMetadata(MediaMetadataRetriever.METADATA_KEY_YEAR);
|
||||
if (year == null || "0".equals(year)) {
|
||||
year = null;
|
||||
} else {
|
||||
int dash = year.indexOf('-');
|
||||
if (dash != -1)
|
||||
year = year.substring(0, dash);
|
||||
}
|
||||
mYear = year;
|
||||
|
||||
mGenre = data.getFirst(MediaMetadataExtractor.GENRE);
|
||||
mTrack = data.getFirst(MediaMetadataExtractor.TRACK_NUMBER);
|
||||
mComposer = data.getFirst(MediaMetadataExtractor.COMPOSER);
|
||||
mYear = data.getFirst(MediaMetadataExtractor.YEAR);
|
||||
mPath = song.path;
|
||||
|
||||
StringBuilder sb = new StringBuilder(12);
|
||||
sb.append(decodeMimeType(data.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE)));
|
||||
String bitrate = data.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE);
|
||||
sb.append(decodeMimeType(data.getFirst(MediaMetadataExtractor.MIME_TYPE)));
|
||||
String bitrate = data.getFirst(MediaMetadataExtractor.BITRATE);
|
||||
if (bitrate != null && bitrate.length() > 3) {
|
||||
sb.append(' ');
|
||||
sb.append(bitrate.substring(0, bitrate.length() - 3));
|
||||
@ -535,8 +516,6 @@ public class FullPlaybackActivity extends SlidingPlaybackActivity
|
||||
|
||||
BastpUtil.GainValues rg = PlaybackService.get(this).getReplayGainValues(song.path);
|
||||
mReplayGain = String.format("base=%.2f, track=%.2f, album=%.2f", rg.base, rg.track, rg.album);
|
||||
|
||||
data.release();
|
||||
}
|
||||
|
||||
mUiHandler.sendEmptyMessage(MSG_COMMIT_INFO);
|
||||
@ -616,7 +595,7 @@ public class FullPlaybackActivity extends SlidingPlaybackActivity
|
||||
case MSG_NOTIFY_PLAYLIST_CHANGED: // triggers a fav-refresh
|
||||
case MSG_LOAD_FAVOURITE_INFO:
|
||||
if (mCurrentSong != null) {
|
||||
boolean found = Playlist.isInPlaylist(getContentResolver(), Playlist.getFavoritesId(this, false), mCurrentSong);
|
||||
boolean found = Playlist.isInPlaylist(this, Playlist.getFavoritesId(this, false), mCurrentSong);
|
||||
mUiHandler.sendMessage(mUiHandler.obtainMessage(MSG_COMMIT_FAVOURITE_INFO, found));
|
||||
}
|
||||
break;
|
||||
|
@ -137,7 +137,7 @@ public class LazyCoverView extends ImageView
|
||||
if (bitmap == null) {
|
||||
if (payload.key.mediaType == MediaUtils.TYPE_ALBUM) {
|
||||
// We only display real covers for queries using the album id as key
|
||||
Song song = MediaUtils.getSongByTypeId(mContext.getContentResolver(), payload.key.mediaType, payload.key.mediaId);
|
||||
Song song = MediaUtils.getSongByTypeId(mContext, payload.key.mediaType, payload.key.mediaId);
|
||||
if (song != null) {
|
||||
bitmap = song.getSmallCover(mContext);
|
||||
}
|
||||
|
@ -23,8 +23,9 @@
|
||||
|
||||
package ch.blinkenlights.android.vanilla;
|
||||
|
||||
import ch.blinkenlights.android.medialibrary.MediaLibrary;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
@ -547,7 +548,7 @@ public class LibraryActivity
|
||||
|
||||
/**
|
||||
* Set a new limiter of the given type built from the first
|
||||
* MediaStore.Audio.Media row that matches the selection.
|
||||
* MediaLibrary.VIEW_SONGS_ALBUMS_ARTISTS row that matches the selection.
|
||||
*
|
||||
* @param limiterType The type of limiter to create. Must be either
|
||||
* MediaUtils.TYPE_ARTIST or MediaUtils.TYPE_ALBUM.
|
||||
@ -555,10 +556,10 @@ public class LibraryActivity
|
||||
*/
|
||||
private void setLimiter(int limiterType, String selection)
|
||||
{
|
||||
ContentResolver resolver = getContentResolver();
|
||||
Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
||||
String[] projection = new String[] { MediaStore.Audio.Media.ARTIST_ID, MediaStore.Audio.Media.ALBUM_ID, MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ALBUM };
|
||||
Cursor cursor = MediaUtils.queryResolver(resolver, uri, projection, selection, null, null);
|
||||
String[] projection = new String[] { MediaLibrary.ContributorColumns.ARTIST_ID, MediaLibrary.SongColumns.ALBUM_ID, MediaLibrary.ContributorColumns.ARTIST, MediaLibrary.AlbumColumns.ALBUM };
|
||||
QueryTask query = new QueryTask(MediaLibrary.VIEW_SONGS_ALBUMS_ARTISTS, projection, selection, null, null);
|
||||
Cursor cursor = query.runQuery(getApplicationContext());
|
||||
|
||||
if (cursor != null) {
|
||||
if (cursor.moveToNext()) {
|
||||
String[] fields;
|
||||
@ -566,11 +567,11 @@ public class LibraryActivity
|
||||
switch (limiterType) {
|
||||
case MediaUtils.TYPE_ARTIST:
|
||||
fields = new String[] { cursor.getString(2) };
|
||||
data = String.format("artist_id=%d", cursor.getLong(0));
|
||||
data = String.format(MediaLibrary.ContributorColumns.ARTIST_ID+"=%d", cursor.getLong(0));
|
||||
break;
|
||||
case MediaUtils.TYPE_ALBUM:
|
||||
fields = new String[] { cursor.getString(2), cursor.getString(3) };
|
||||
data = String.format("album_id=%d", cursor.getLong(1));
|
||||
data = String.format(MediaLibrary.SongColumns.ALBUM_ID+"=%d", cursor.getLong(1));
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("setLimiter() does not support limiter type " + limiterType);
|
||||
|
@ -214,7 +214,6 @@ public class LibraryPagerAdapter
|
||||
mUiHandler = new Handler(this);
|
||||
mWorkerHandler = new Handler(workerLooper, this);
|
||||
mCurrentPage = -1;
|
||||
activity.getContentResolver().registerContentObserver(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, true, mPlaylistObserver);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -23,6 +23,8 @@
|
||||
|
||||
package ch.blinkenlights.android.vanilla;
|
||||
|
||||
import ch.blinkenlights.android.medialibrary.MediaLibrary;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
@ -48,6 +50,7 @@ import java.util.regex.Pattern;
|
||||
import java.util.ArrayList;
|
||||
import java.lang.StringBuilder;
|
||||
|
||||
import android.util.Log;
|
||||
/**
|
||||
* MediaAdapter provides an adapter backed by a MediaStore content provider.
|
||||
* It generates simple one- or two-line text views to display each media
|
||||
@ -66,9 +69,6 @@ public class MediaAdapter
|
||||
, SectionIndexer
|
||||
{
|
||||
private static final Pattern SPACE_SPLIT = Pattern.compile("\\s+");
|
||||
|
||||
private static final String SORT_MAGIC_PLAYCOUNT = "__PLAYCOUNT_SORT";
|
||||
|
||||
/**
|
||||
* The string to use for length==0 db fields
|
||||
*/
|
||||
@ -96,9 +96,9 @@ public class MediaAdapter
|
||||
*/
|
||||
private final int mType;
|
||||
/**
|
||||
* The URI of the content provider backing this adapter.
|
||||
* The table / view to use for this query
|
||||
*/
|
||||
private Uri mStore;
|
||||
private String mSource;
|
||||
/**
|
||||
* The fields to use from the content provider. The last field will be
|
||||
* displayed in the MediaView, as will the first field if there are
|
||||
@ -133,11 +133,6 @@ public class MediaAdapter
|
||||
* ASC or DESC as appropriate before being passed to the query.
|
||||
*/
|
||||
private String[] mAdapterSortValues;
|
||||
/**
|
||||
* Same as mAdapterSortValues, but describes the query to do if we
|
||||
* are returning songs for a `foreign' adapter (which migt have different column names)
|
||||
*/
|
||||
private String[] mSongSortValues;
|
||||
/**
|
||||
* The index of the current of the current sort mode in mSortValues, or
|
||||
* the inverse of the index (in which case sort should be descending
|
||||
@ -189,53 +184,51 @@ public class MediaAdapter
|
||||
|
||||
switch (type) {
|
||||
case MediaUtils.TYPE_ARTIST:
|
||||
mStore = MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI;
|
||||
mFields = new String[] { MediaStore.Audio.Artists.ARTIST };
|
||||
mFieldKeys = new String[] { MediaStore.Audio.Artists.ARTIST_KEY };
|
||||
mSortEntries = new int[] { R.string.name, R.string.number_of_tracks };
|
||||
mAdapterSortValues = new String[] { "artist_key %1$s", "number_of_tracks %1$s,artist_key %1$s" };
|
||||
mSongSortValues = new String[] { "artist_key %1$s,track", "artist_key %1$s,track" /* cannot sort by number_of_tracks */ };
|
||||
mSource = MediaLibrary.VIEW_ARTISTS;
|
||||
mFields = new String[] { MediaLibrary.ContributorColumns.ARTIST };
|
||||
mFieldKeys = new String[] { MediaLibrary.ContributorColumns.ARTIST_SORT };
|
||||
mSortEntries = new int[] { R.string.name, R.string.date_added };
|
||||
mAdapterSortValues = new String[] { MediaLibrary.ContributorColumns.ARTIST_SORT+" %1$s", MediaLibrary.ContributorColumns.MTIME+" %1$s" };
|
||||
break;
|
||||
case MediaUtils.TYPE_ALBUM:
|
||||
mStore = MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI;
|
||||
mFields = new String[] { MediaStore.Audio.Albums.ALBUM, MediaStore.Audio.Albums.ARTIST };
|
||||
// Why is there no artist_key column constant in the album MediaStore? The column does seem to exist.
|
||||
mFieldKeys = new String[] { MediaStore.Audio.Albums.ALBUM_KEY, "artist_key" };
|
||||
mSortEntries = new int[] { R.string.name, R.string.artist_album, R.string.artist_year_album, R.string.number_of_tracks, R.string.date_added };
|
||||
mAdapterSortValues = new String[] { "album_key %1$s", "artist_key %1$s,album_key %1$s", "artist_key %1$s,minyear %1$s,album_key %1$s", "numsongs %1$s,album_key %1$s", "_id %1$s" };
|
||||
mSongSortValues = new String[] { "album_key %1$s,track", "artist_key %1$s,album_key %1$s,track", "artist_key %1$s,year %1$s,album_key %1$s,track", "album_key %1$s,track", "album_id %1$s,track" };
|
||||
mSource = MediaLibrary.VIEW_ALBUMS_ARTISTS;
|
||||
mFields = new String[] { MediaLibrary.AlbumColumns.ALBUM, MediaLibrary.ContributorColumns.ARTIST };
|
||||
mFieldKeys = new String[] { MediaLibrary.AlbumColumns.ALBUM_SORT, MediaLibrary.ContributorColumns.ARTIST_SORT };
|
||||
mSortEntries = new int[] { R.string.name, R.string.artist_album, R.string.year, R.string.date_added };
|
||||
mAdapterSortValues = new String[] { MediaLibrary.AlbumColumns.ALBUM_SORT+" %1$s", MediaLibrary.ContributorColumns.ARTIST_SORT+" %1$s,"+MediaLibrary.AlbumColumns.ALBUM_SORT+" %1$s",
|
||||
MediaLibrary.AlbumColumns.PRIMARY_ALBUM_YEAR+" %1$s", MediaLibrary.AlbumColumns.MTIME+" %1$s" };
|
||||
break;
|
||||
case MediaUtils.TYPE_SONG:
|
||||
mStore = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
||||
mFields = new String[] { MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.ALBUM, MediaStore.Audio.Media.ARTIST };
|
||||
mFieldKeys = new String[] { MediaStore.Audio.Media.TITLE_KEY, MediaStore.Audio.Media.ALBUM_KEY, MediaStore.Audio.Media.ARTIST_KEY };
|
||||
mSortEntries = new int[] { R.string.name, R.string.artist_album_track, R.string.artist_album_title,
|
||||
R.string.artist_year_album, R.string.album_track,
|
||||
R.string.year, R.string.date_added, R.string.song_playcount };
|
||||
mAdapterSortValues = new String[] { "title_key %1$s", "artist_key %1$s,album_key %1$s,track", "artist_key %1$s,album_key %1$s,title_key %1$s",
|
||||
"artist_key %1$s,year %1$s,album_key %1$s,track", "album_key %1$s,track",
|
||||
"year %1$s,title_key %1$s","_id %1$s", SORT_MAGIC_PLAYCOUNT };
|
||||
mSongSortValues = mAdapterSortValues;
|
||||
mSource = MediaLibrary.VIEW_SONGS_ALBUMS_ARTISTS;
|
||||
mFields = new String[] { MediaLibrary.SongColumns.TITLE, MediaLibrary.AlbumColumns.ALBUM, MediaLibrary.ContributorColumns.ARTIST };
|
||||
mFieldKeys = new String[] { MediaLibrary.SongColumns.TITLE_SORT, MediaLibrary.AlbumColumns.ALBUM_SORT, MediaLibrary.ContributorColumns.ARTIST_SORT };
|
||||
mSortEntries = new int[] { R.string.name, R.string.artist_album_track, R.string.artist_album_title, R.string.album_track, R.string.year, R.string.date_added, R.string.song_playcount };
|
||||
mAdapterSortValues = new String[] { MediaLibrary.SongColumns.TITLE_SORT+" %1$s",
|
||||
MediaLibrary.ContributorColumns.ARTIST_SORT+" %1$s,"+MediaLibrary.AlbumColumns.ALBUM_SORT+" %1$s,"+MediaLibrary.AlbumColumns.DISC_NUMBER+","+MediaLibrary.SongColumns.SONG_NUMBER,
|
||||
MediaLibrary.ContributorColumns.ARTIST_SORT+" %1$s,"+MediaLibrary.AlbumColumns.ALBUM_SORT+" %1$s,"+MediaLibrary.SongColumns.TITLE_SORT+" %1$s",
|
||||
MediaLibrary.AlbumColumns.ALBUM_SORT+" %1$s,"+MediaLibrary.AlbumColumns.DISC_NUMBER+","+MediaLibrary.SongColumns.SONG_NUMBER,
|
||||
MediaLibrary.SongColumns.YEAR+" %1$s,"+MediaLibrary.AlbumColumns.ALBUM_SORT+" %1$s,"+MediaLibrary.AlbumColumns.DISC_NUMBER+","+MediaLibrary.SongColumns.SONG_NUMBER,
|
||||
MediaLibrary.SongColumns.MTIME+" %1$s",
|
||||
MediaLibrary.SongColumns.PLAYCOUNT+" %1$s",
|
||||
};
|
||||
// Songs covers are cached per-album
|
||||
mCoverCacheType = MediaUtils.TYPE_ALBUM;
|
||||
coverCacheKey = MediaStore.Audio.Albums.ALBUM_ID;
|
||||
break;
|
||||
case MediaUtils.TYPE_PLAYLIST:
|
||||
mStore = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI;
|
||||
mFields = new String[] { MediaStore.Audio.Playlists.NAME };
|
||||
mSource = MediaLibrary.TABLE_PLAYLISTS;
|
||||
mFields = new String[] { MediaLibrary.PlaylistColumns.NAME };
|
||||
mFieldKeys = null;
|
||||
mSortEntries = new int[] { R.string.name, R.string.date_added };
|
||||
mAdapterSortValues = new String[] { "name %1$s", "date_added %1$s" };
|
||||
mSongSortValues = null;
|
||||
mAdapterSortValues = new String[] { MediaLibrary.PlaylistColumns.NAME+" %1$s", MediaLibrary.PlaylistColumns._ID+" %1$s" };
|
||||
mExpandable = true;
|
||||
break;
|
||||
case MediaUtils.TYPE_GENRE:
|
||||
mStore = MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI;
|
||||
mFields = new String[] { MediaStore.Audio.Genres.NAME };
|
||||
mFieldKeys = null;
|
||||
mSource = MediaLibrary.TABLE_GENRES;
|
||||
mFields = new String[] { MediaLibrary.GenreColumns._GENRE };
|
||||
mFieldKeys = new String[] { MediaLibrary.GenreColumns._GENRE_SORT };
|
||||
mSortEntries = new int[] { R.string.name };
|
||||
mAdapterSortValues = new String[] { "name %1$s" };
|
||||
mSongSortValues = null;
|
||||
mAdapterSortValues = new String[] { MediaLibrary.GenreColumns._GENRE_SORT+" %1$s" };
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Invalid value for type: " + type);
|
||||
@ -260,8 +253,8 @@ public class MediaAdapter
|
||||
private String getFirstSortColumn() {
|
||||
int mode = mSortMode < 0 ? ~mSortMode : mSortMode; // get current sort mode
|
||||
String column = SPACE_SPLIT.split(mAdapterSortValues[mode])[0];
|
||||
if(column.endsWith("_key")) { // we want human-readable string, not machine-composed
|
||||
column = column.substring(0, column.length() - 4);
|
||||
if(column.endsWith("_sort")) { // we want human-readable string, not machine-composed
|
||||
column = column.substring(0, column.length() - 5);
|
||||
}
|
||||
|
||||
return column;
|
||||
@ -291,17 +284,23 @@ public class MediaAdapter
|
||||
* Build the query to be run with runQuery().
|
||||
*
|
||||
* @param projection The columns to query.
|
||||
* @param forceMusicCheck Force the is_music check to be added to the
|
||||
* selection.
|
||||
* @param returnSongs return songs instead of mType if true.
|
||||
*/
|
||||
private QueryTask buildQuery(String[] projection, boolean returnSongs)
|
||||
{
|
||||
private QueryTask buildQuery(String[] projection, boolean returnSongs) {
|
||||
Log.v("VanillaMusic", "constrain = "+mConstraint);
|
||||
Log.v("VanillaMusic", "limiter = "+ (mLimiter == null ? "NULL" : mLimiter.data));
|
||||
Log.v("VanillaMusic", "sortMode = "+mSortMode);
|
||||
|
||||
|
||||
String source = mSource;
|
||||
String constraint = mConstraint;
|
||||
Limiter limiter = mLimiter;
|
||||
|
||||
StringBuilder selection = new StringBuilder();
|
||||
String[] selectionArgs = null;
|
||||
String[] enrichedProjection = projection;
|
||||
|
||||
// Assemble the sort string as requested by the user
|
||||
int mode = mSortMode;
|
||||
String sortDir;
|
||||
if (mode < 0) {
|
||||
@ -311,47 +310,31 @@ public class MediaAdapter
|
||||
sortDir = "ASC";
|
||||
}
|
||||
|
||||
// Use the song-sort mapping if we are returning songs
|
||||
String sortStringRaw = (returnSongs ? mSongSortValues[mode] : mAdapterSortValues[mode]);
|
||||
String[] enrichedProjection = projection;
|
||||
|
||||
// Magic sort mode: sort by playcount
|
||||
if (sortStringRaw == SORT_MAGIC_PLAYCOUNT) {
|
||||
ArrayList<Long> topSongs = (new PlayCountsHelper(mContext)).getTopSongs(4096);
|
||||
int sortWeight = -1 * topSongs.size(); // Sort mode is actually reversed (default: mostplayed -> leastplayed)
|
||||
|
||||
StringBuilder sb = new StringBuilder("CASE WHEN _id=0 THEN 0"); // include dummy statement in initial string -> topSongs may be empty
|
||||
for (Long id : topSongs) {
|
||||
sb.append(" WHEN _id="+id+" THEN "+sortWeight);
|
||||
sortWeight++;
|
||||
}
|
||||
sb.append(" ELSE 0 END %1s");
|
||||
sortStringRaw = sb.toString();
|
||||
} else if (returnSongs == false) {
|
||||
// This is an 'adapter native' query: include the first sorting column
|
||||
// in the projection to make it useable for the fast-scroller
|
||||
enrichedProjection = Arrays.copyOf(projection, projection.length + 1);
|
||||
enrichedProjection[projection.length] = getFirstSortColumn();
|
||||
// Fetch current sorting mode and sort by disc+track if we are going to look up the songs table
|
||||
String sortRaw = mAdapterSortValues[mode];
|
||||
if (returnSongs) {
|
||||
// songs returned from the artist tab should also sort by album
|
||||
if (mType == MediaUtils.TYPE_ARTIST)
|
||||
sortRaw += ", "+MediaLibrary.AlbumColumns.ALBUM_SORT+" %1$s";
|
||||
// and this is for all types:
|
||||
sortRaw += ", "+MediaLibrary.AlbumColumns.DISC_NUMBER+", "+MediaLibrary.SongColumns.SONG_NUMBER;
|
||||
}
|
||||
|
||||
String sort = String.format(sortStringRaw, sortDir);
|
||||
|
||||
if (returnSongs || mType == MediaUtils.TYPE_SONG)
|
||||
selection.append(MediaStore.Audio.Media.IS_MUSIC+" AND length(_data)");
|
||||
// ...and assemble the SQL string we are really going to use
|
||||
String sort = String.format(sortRaw, sortDir);
|
||||
|
||||
// include the constraint (aka: search string) if any
|
||||
if (constraint != null && constraint.length() != 0) {
|
||||
String[] needles;
|
||||
String[] keySource;
|
||||
|
||||
// If we are using sorting keys, we need to change our constraint
|
||||
// into a list of collation keys. Otherwise, just split the
|
||||
// constraint with no modification.
|
||||
if (mFieldKeys != null) {
|
||||
String colKey = MediaStore.Audio.keyFor(constraint);
|
||||
String colKey = MediaLibrary.keyFor(constraint);
|
||||
String spaceColKey = DatabaseUtils.getCollationKey(" ");
|
||||
needles = colKey.split(spaceColKey);
|
||||
keySource = mFieldKeys;
|
||||
} else {
|
||||
// only used for playlists, maybe we should just update the schema ?
|
||||
needles = SPACE_SPLIT.split(constraint);
|
||||
keySource = mFields;
|
||||
}
|
||||
@ -379,31 +362,28 @@ public class MediaAdapter
|
||||
}
|
||||
}
|
||||
|
||||
QueryTask query;
|
||||
if(mType == MediaUtils.TYPE_GENRE && !returnSongs) {
|
||||
query = MediaUtils.buildGenreExcludeEmptyQuery(enrichedProjection, selection.toString(),
|
||||
selectionArgs, sort);
|
||||
} else if (limiter != null && limiter.type == MediaUtils.TYPE_GENRE) {
|
||||
// Genre is not standard metadata for MediaStore.Audio.Media.
|
||||
// We have to query it through a separate provider. : /
|
||||
query = MediaUtils.buildGenreQuery((Long)limiter.data, enrichedProjection, selection.toString(), selectionArgs, sort, mType, returnSongs);
|
||||
} else {
|
||||
if (limiter != null) {
|
||||
if (selection.length() != 0)
|
||||
selection.append(" AND ");
|
||||
selection.append(limiter.data);
|
||||
if (limiter != null) {
|
||||
if (selection.length() != 0) {
|
||||
selection.append(" AND ");
|
||||
}
|
||||
query = new QueryTask(mStore, enrichedProjection, selection.toString(), selectionArgs, sort);
|
||||
if (returnSongs) // force query on song provider as we are requested to return songs
|
||||
query.uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
||||
selection.append(limiter.data);
|
||||
}
|
||||
|
||||
if (returnSongs == true) {
|
||||
source = MediaLibrary.VIEW_SONGS_ALBUMS_ARTISTS;
|
||||
} else {
|
||||
enrichedProjection = Arrays.copyOf(projection, projection.length + 1);
|
||||
enrichedProjection[projection.length] = getFirstSortColumn();
|
||||
}
|
||||
|
||||
QueryTask query = new QueryTask(source, enrichedProjection, selection.toString(), selectionArgs, sort);
|
||||
return query;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query()
|
||||
{
|
||||
return buildQuery(mProjection, false).runQuery(mContext.getContentResolver());
|
||||
return buildQuery(mProjection, false).runQuery(mContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -467,15 +447,15 @@ public class MediaAdapter
|
||||
switch (mType) {
|
||||
case MediaUtils.TYPE_ARTIST:
|
||||
fields = new String[] { cursor.getString(2) };
|
||||
data = String.format("%s=%d", MediaStore.Audio.Media.ARTIST_ID, id);
|
||||
data = String.format("%s=%d", MediaLibrary.ContributorColumns.ARTIST_ID, id);
|
||||
break;
|
||||
case MediaUtils.TYPE_ALBUM:
|
||||
fields = new String[] { cursor.getString(3), cursor.getString(2) };
|
||||
data = String.format("%s=%d", MediaStore.Audio.Media.ALBUM_ID, id);
|
||||
data = String.format("%s=%d", MediaLibrary.SongColumns.ALBUM_ID, id);
|
||||
break;
|
||||
case MediaUtils.TYPE_GENRE:
|
||||
fields = new String[] { cursor.getString(2) };
|
||||
data = id;
|
||||
data = String.format("%s=%d", MediaLibrary.GenreSongColumns._GENRE_ID, id);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("getLimiter() is not supported for media type: " + mType);
|
||||
@ -588,7 +568,7 @@ public class MediaAdapter
|
||||
*/
|
||||
public void setSortMode(int i)
|
||||
{
|
||||
mSortMode = i;
|
||||
mSortMode = (i < mSortEntries.length ? i : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -22,6 +22,9 @@
|
||||
|
||||
package ch.blinkenlights.android.vanilla;
|
||||
|
||||
import ch.blinkenlights.android.medialibrary.MediaLibrary;
|
||||
import ch.blinkenlights.android.medialibrary.MediaMetadataExtractor;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
@ -29,12 +32,12 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.zip.CRC32;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import junit.framework.Assert;
|
||||
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
@ -43,7 +46,6 @@ import android.os.Environment;
|
||||
import android.provider.MediaStore;
|
||||
import android.text.TextUtils;
|
||||
import android.database.MatrixCursor;
|
||||
import android.media.MediaMetadataRetriever;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
@ -88,19 +90,19 @@ public class MediaUtils {
|
||||
|
||||
/**
|
||||
* The default sort order for media queries. First artist, then album, then
|
||||
* track number.
|
||||
* song number.
|
||||
*/
|
||||
private static final String DEFAULT_SORT = "artist_key,album_key,track";
|
||||
private static final String DEFAULT_SORT = "artist_sort,album_sort,disc_num,song_num";
|
||||
|
||||
/**
|
||||
* The default sort order for albums. First the album, then tracknumber
|
||||
* The default sort order for albums. First the album, then songnumber
|
||||
*/
|
||||
private static final String ALBUM_SORT = "album_key,track";
|
||||
private static final String ALBUM_SORT = "album_sort,disc_num,song_num";
|
||||
|
||||
/**
|
||||
* The default sort order for files. Simply use the path
|
||||
*/
|
||||
private static final String FILE_SORT = "_data";
|
||||
private static final String FILE_SORT = "path";
|
||||
|
||||
/**
|
||||
* Cached random instance.
|
||||
@ -140,35 +142,36 @@ public class MediaUtils {
|
||||
*/
|
||||
private static QueryTask buildMediaQuery(int type, long id, String[] projection, String select)
|
||||
{
|
||||
Uri media = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
||||
StringBuilder selection = new StringBuilder();
|
||||
String sort = DEFAULT_SORT;
|
||||
|
||||
if (select != null) {
|
||||
selection.append(select);
|
||||
selection.append(" AND ");
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case TYPE_SONG:
|
||||
selection.append(MediaStore.Audio.Media._ID);
|
||||
selection.append(MediaLibrary.SongColumns._ID);
|
||||
break;
|
||||
case TYPE_ARTIST:
|
||||
selection.append(MediaStore.Audio.Media.ARTIST_ID);
|
||||
selection.append(MediaLibrary.ContributorColumns.ARTIST_ID);
|
||||
break;
|
||||
case TYPE_ALBUM:
|
||||
selection.append(MediaStore.Audio.Media.ALBUM_ID);
|
||||
selection.append(MediaLibrary.SongColumns.ALBUM_ID);
|
||||
sort = ALBUM_SORT;
|
||||
break;
|
||||
case TYPE_GENRE:
|
||||
selection.append(MediaLibrary.GenreSongColumns._GENRE_ID);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Invalid type specified: " + type);
|
||||
}
|
||||
|
||||
selection.append('=');
|
||||
selection.append(id);
|
||||
selection.append(" AND length(_data) AND "+MediaStore.Audio.Media.IS_MUSIC);
|
||||
|
||||
if (select != null) {
|
||||
selection.append(" AND ");
|
||||
selection.append(select);
|
||||
}
|
||||
|
||||
QueryTask result = new QueryTask(media, projection, selection.toString(), null, sort);
|
||||
QueryTask result = new QueryTask(MediaLibrary.VIEW_SONGS_ALBUMS_ARTISTS, projection, selection.toString(), null, sort);
|
||||
result.type = type;
|
||||
return result;
|
||||
}
|
||||
@ -179,139 +182,16 @@ public class MediaUtils {
|
||||
*
|
||||
* @param id The id of the playlist in MediaStore.Audio.Playlists.
|
||||
* @param projection The columns to query.
|
||||
* @param selection The selection to pass to the query, or null.
|
||||
* @return The initialized query.
|
||||
*/
|
||||
public static QueryTask buildPlaylistQuery(long id, String[] projection, String selection)
|
||||
{
|
||||
Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", id);
|
||||
String sort = MediaStore.Audio.Playlists.Members.PLAY_ORDER;
|
||||
QueryTask result = new QueryTask(uri, projection, selection, null, sort);
|
||||
public static QueryTask buildPlaylistQuery(long id, String[] projection) {
|
||||
String sort = MediaLibrary.PlaylistSongColumns.POSITION;
|
||||
String selection = MediaLibrary.PlaylistSongColumns.PLAYLIST_ID+"="+id;
|
||||
QueryTask result = new QueryTask(MediaLibrary.VIEW_PLAYLIST_SONGS, projection, selection, null, sort);
|
||||
result.type = TYPE_PLAYLIST;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a query that will return all the songs in the genre with the
|
||||
* given id.
|
||||
*
|
||||
* @param id The id of the genre in MediaStore.Audio.Genres.
|
||||
* @param projection The columns to query.
|
||||
* @param selection The selection to pass to the query, or null.
|
||||
* @param selectionArgs The arguments to substitute into the selection.
|
||||
* @param sort The sort order.
|
||||
* @param type The media type to query and return
|
||||
* @param returnSongs returns matching songs instead of `type' if true
|
||||
*/
|
||||
public static QueryTask buildGenreQuery(long id, String[] projection, String selection, String[] selectionArgs, String sort, int type, boolean returnSongs)
|
||||
{
|
||||
// Note: This function works on a raw sql query with way too much internal
|
||||
// knowledge about the mediaProvider SQL table layout. Yes: it's ugly.
|
||||
// The reason for this mess is that android has a very crippled genre implementation
|
||||
// and does, for example, not allow us to query the albumbs beloging to a genre.
|
||||
|
||||
Uri uri = MediaStore.Audio.Genres.Members.getContentUri("external", id);
|
||||
String[] clonedProjection = projection.clone(); // we modify the projection, but this should not be visible to the caller
|
||||
String sql = "";
|
||||
String authority = "audio";
|
||||
|
||||
if (type == TYPE_ARTIST)
|
||||
authority = "artist_info";
|
||||
if (type == TYPE_ALBUM)
|
||||
authority = "album_info";
|
||||
|
||||
// Our raw SQL query includes the album_info table (well: it's actually a view)
|
||||
// which shares some columns with audio.
|
||||
// This regexp should matche duplicate column names and forces them to use
|
||||
// the audio table as a source
|
||||
final String _FORCE_AUDIO_SRC = "(^|[ |,\\(])(_id|album(_\\w+)?|artist(_\\w+)?)";
|
||||
|
||||
// Prefix the SELECTed rows with the current table authority name
|
||||
for (int i=0 ;i<clonedProjection.length; i++) {
|
||||
if (clonedProjection[i].equals("0") == false) // do not prefix fake rows
|
||||
clonedProjection[i] = (returnSongs ? "audio" : authority)+"."+clonedProjection[i];
|
||||
}
|
||||
|
||||
sql += TextUtils.join(", ", clonedProjection);
|
||||
sql += " FROM audio_genres_map_noid, audio" + (authority.equals("audio") ? "" : ", "+authority);
|
||||
sql += " WHERE(audio._id = audio_id AND genre_id=?)";
|
||||
|
||||
if (selection != null && selection.length() > 0)
|
||||
sql += " AND("+selection.replaceAll(_FORCE_AUDIO_SRC, "$1audio.$2")+")";
|
||||
|
||||
if (type == TYPE_ARTIST)
|
||||
sql += " AND(artist_info._id = audio.artist_id)" + (returnSongs ? "" : " GROUP BY artist_info._id");
|
||||
|
||||
if (type == TYPE_ALBUM)
|
||||
sql += " AND(album_info._id = audio.album_id)" + (returnSongs ? "" : " GROUP BY album_info._id");
|
||||
|
||||
if (sort != null && sort.length() > 0)
|
||||
sql += " ORDER BY "+sort.replaceAll(_FORCE_AUDIO_SRC, "$1audio.$2");
|
||||
|
||||
// We are now turning this into an sql injection. Fun times.
|
||||
clonedProjection[0] = sql +" --";
|
||||
|
||||
QueryTask result = new QueryTask(uri, clonedProjection, selection, selectionArgs, sort);
|
||||
result.type = TYPE_GENRE;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link QueryTask} for genres. The query will select only genres that have at least
|
||||
* one song associated with them.
|
||||
*
|
||||
* @param projection The fields of the genre table that should be returned.
|
||||
* @param selection Additional constraints for the query (added to the WHERE section). '?'s
|
||||
* will be replaced by values in {@code selectionArgs}. Can be null.
|
||||
* @param selectionArgs Arguments for {@code selection}. Can be null. See
|
||||
* {@link android.content.ContentProvider#query(Uri, String[], String, String[], String)}
|
||||
* @param sort How the returned genres should be sorted (added to the ORDER BY section)
|
||||
* @return The QueryTask for the genres
|
||||
*/
|
||||
public static QueryTask buildGenreExcludeEmptyQuery(String[] projection, String selection, String[] selectionArgs, String sort) {
|
||||
/*
|
||||
* An example SQLite query that we're building in this function
|
||||
SELECT DISTINCT _id, name
|
||||
FROM audio_genres
|
||||
WHERE
|
||||
EXISTS(
|
||||
SELECT audio_id, genre_id, audio._id
|
||||
FROM audio_genres_map, audio
|
||||
WHERE (genre_id == audio_genres._id)
|
||||
AND (audio_id == audio._id))
|
||||
ORDER BY name DESC
|
||||
*/
|
||||
Uri uri = MediaStore.Audio.Genres.getContentUri("external");
|
||||
StringBuilder sql = new StringBuilder();
|
||||
// Don't want multiple identical genres
|
||||
sql.append("DISTINCT ");
|
||||
|
||||
// Add the projection fields to the query
|
||||
sql.append(TextUtils.join(", ", projection)).append(' ');
|
||||
|
||||
sql.append("FROM audio_genres ");
|
||||
// Limit to genres that contain at least one valid song
|
||||
sql.append("WHERE EXISTS( ")
|
||||
.append("SELECT audio_id, genre_id, audio._id ")
|
||||
.append("FROM audio_genres_map, audio ")
|
||||
.append("WHERE (genre_id == audio_genres._id) AND (audio_id == audio._id) ")
|
||||
.append(") ");
|
||||
|
||||
if (!TextUtils.isEmpty(selection))
|
||||
sql.append(" AND(" + selection + ") ");
|
||||
|
||||
if(!TextUtils.isEmpty(sort))
|
||||
sql.append(" ORDER BY ").append(sort);
|
||||
|
||||
// Ignore the framework generated query
|
||||
sql.append(" -- ");
|
||||
String[] injectedProjection = new String[1];
|
||||
injectedProjection[0] = sql.toString();
|
||||
|
||||
// Don't pass the selection/sort as we've already added it to the query
|
||||
return new QueryTask(uri, injectedProjection, null, selectionArgs, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a query with the given information.
|
||||
*
|
||||
@ -328,11 +208,10 @@ public class MediaUtils {
|
||||
case TYPE_ARTIST:
|
||||
case TYPE_ALBUM:
|
||||
case TYPE_SONG:
|
||||
case TYPE_GENRE:
|
||||
return buildMediaQuery(type, id, projection, selection);
|
||||
case TYPE_PLAYLIST:
|
||||
return buildPlaylistQuery(id, projection, selection);
|
||||
case TYPE_GENRE:
|
||||
return buildGenreQuery(id, projection, selection, null, MediaStore.Audio.Genres.Members.TITLE_KEY, TYPE_SONG, true);
|
||||
return buildPlaylistQuery(id, projection);
|
||||
default:
|
||||
throw new IllegalArgumentException("Specified type not valid: " + type);
|
||||
}
|
||||
@ -342,15 +221,15 @@ public class MediaUtils {
|
||||
* Query the MediaStore to determine the id of the genre the song belongs
|
||||
* to.
|
||||
*
|
||||
* @param resolver A ContentResolver to use.
|
||||
* @param context The context to use
|
||||
* @param id The id of the song to query the genre for.
|
||||
*/
|
||||
public static long queryGenreForSong(ContentResolver resolver, long id)
|
||||
{
|
||||
String[] projection = { "_id" };
|
||||
Uri uri = MediaStore.Audio.Genres.getContentUriForAudioId("external", (int)id);
|
||||
Cursor cursor = queryResolver(resolver, uri, projection, null, null, null);
|
||||
public static long queryGenreForSong(Context context, long id) {
|
||||
String[] projection = { MediaLibrary.GenreSongColumns._GENRE_ID };
|
||||
String query = MediaLibrary.GenreSongColumns.SONG_ID+"=?";
|
||||
String[] queryArgs = new String[] { Long.toString(id) };
|
||||
|
||||
Cursor cursor = MediaLibrary.queryLibrary(context, MediaLibrary.TABLE_GENRES_SONGS, projection, query, queryArgs, null);
|
||||
if (cursor != null) {
|
||||
if (cursor.moveToNext())
|
||||
return cursor.getLong(0);
|
||||
@ -378,7 +257,7 @@ public class MediaUtils {
|
||||
/**
|
||||
* Shuffle a Song list using Collections.shuffle().
|
||||
*
|
||||
* @param albumShuffle If true, preserve the order of tracks inside albums.
|
||||
* @param albumShuffle If true, preserve the order of songs inside albums.
|
||||
*/
|
||||
public static void shuffle(List<Song> list, boolean albumShuffle)
|
||||
{
|
||||
@ -428,24 +307,13 @@ public class MediaUtils {
|
||||
/**
|
||||
* Determine if any songs are available from the library.
|
||||
*
|
||||
* @param resolver A ContentResolver to use.
|
||||
* @param context The Context to use
|
||||
* @return True if it's possible to retrieve any songs, false otherwise. For
|
||||
* example, false could be returned if there are no songs in the library.
|
||||
*/
|
||||
public static boolean isSongAvailable(ContentResolver resolver)
|
||||
{
|
||||
public static boolean isSongAvailable(Context context) {
|
||||
if (sSongCount == -1) {
|
||||
Uri media = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
||||
String selection = MediaStore.Audio.Media.IS_MUSIC;
|
||||
selection += " AND length(_data)";
|
||||
Cursor cursor = queryResolver(resolver, media, new String[]{"count(_id)"}, selection, null, null);
|
||||
if (cursor == null) {
|
||||
sSongCount = 0;
|
||||
} else {
|
||||
cursor.moveToFirst();
|
||||
sSongCount = cursor.getInt(0);
|
||||
cursor.close();
|
||||
}
|
||||
sSongCount = MediaLibrary.getLibrarySize(context);
|
||||
}
|
||||
|
||||
return sSongCount != 0;
|
||||
@ -455,14 +323,11 @@ public class MediaUtils {
|
||||
* Returns a shuffled array contaning the ids of all the songs on the
|
||||
* device's library.
|
||||
*
|
||||
* @param resolver A ContentResolver to use.
|
||||
* @param context The Context to use
|
||||
*/
|
||||
private static long[] queryAllSongs(ContentResolver resolver)
|
||||
{
|
||||
Uri media = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
||||
String selection = MediaStore.Audio.Media.IS_MUSIC;
|
||||
selection += " AND length(_data)";
|
||||
Cursor cursor = queryResolver(resolver, media, Song.EMPTY_PROJECTION, selection, null, null);
|
||||
private static long[] queryAllSongs(Context context) {
|
||||
QueryTask query = new QueryTask(MediaLibrary.TABLE_SONGS, Song.EMPTY_PROJECTION, null, null, null);
|
||||
Cursor cursor = query.runQuery(context);
|
||||
if (cursor == null || cursor.getCount() == 0) {
|
||||
sSongCount = 0;
|
||||
return null;
|
||||
@ -484,29 +349,9 @@ public class MediaUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a query on the passed content resolver.
|
||||
* Catches (and returns null on) SecurityException (= user revoked read permission)
|
||||
*
|
||||
* @param resolver The content resolver to use
|
||||
* @param uri the uri to query
|
||||
* @param projection the projection to use
|
||||
* @param selection the selection to use
|
||||
* @param selectionArgs arguments for the selection
|
||||
* @param sortOrder sort order of the returned result
|
||||
*
|
||||
* @return a cursor or null
|
||||
* Called if we detected a medium change
|
||||
* This flushes some cached data
|
||||
*/
|
||||
public static Cursor queryResolver(ContentResolver resolver, Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
|
||||
{
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = resolver.query(uri, projection, selection, selectionArgs, sortOrder);
|
||||
} catch(java.lang.SecurityException e) {
|
||||
// we do not have read permission - just return a null cursor
|
||||
}
|
||||
return cursor;
|
||||
}
|
||||
|
||||
public static void onMediaChange()
|
||||
{
|
||||
sSongCount = -1;
|
||||
@ -525,9 +370,8 @@ public class MediaUtils {
|
||||
return;
|
||||
}
|
||||
|
||||
ContentResolver resolver = ctx.getContentResolver();
|
||||
String[] projection = new String [] { MediaStore.Audio.Media._ID, MediaStore.Audio.Media.DATA };
|
||||
Cursor cursor = buildQuery(type, id, projection, null).runQuery(resolver);
|
||||
String[] projection = new String [] { MediaLibrary.SongColumns._ID, MediaLibrary.SongColumns.PATH };
|
||||
Cursor cursor = buildQuery(type, id, projection, null).runQuery(ctx);
|
||||
if(cursor == null) {
|
||||
return;
|
||||
}
|
||||
@ -550,14 +394,14 @@ public class MediaUtils {
|
||||
/**
|
||||
* Returns the first matching song (or NULL) of given type + id combination
|
||||
*
|
||||
* @param resolver A ContentResolver to use.
|
||||
* @param context A Context to use.
|
||||
* @param type The MediaTye to query
|
||||
* @param id The id of given type to query
|
||||
*/
|
||||
public static Song getSongByTypeId(ContentResolver resolver, int type, long id) {
|
||||
public static Song getSongByTypeId(Context context, int type, long id) {
|
||||
Song song = new Song(-1);
|
||||
QueryTask query = buildQuery(type, id, Song.FILLED_PROJECTION, null);
|
||||
Cursor cursor = query.runQuery(resolver);
|
||||
Cursor cursor = query.runQuery(context);
|
||||
if (cursor != null) {
|
||||
if (cursor.getCount() > 0) {
|
||||
cursor.moveToPosition(0);
|
||||
@ -572,14 +416,14 @@ public class MediaUtils {
|
||||
* Returns a song randomly selected from all the songs in the Android
|
||||
* MediaStore.
|
||||
*
|
||||
* @param resolver A ContentResolver to use.
|
||||
* @param context The Context to use
|
||||
*/
|
||||
public static Song getRandomSong(ContentResolver resolver)
|
||||
public static Song getRandomSong(Context context)
|
||||
{
|
||||
long[] songs = sAllSongs;
|
||||
|
||||
if (songs == null) {
|
||||
songs = queryAllSongs(resolver);
|
||||
songs = queryAllSongs(context);
|
||||
if (songs == null)
|
||||
return null;
|
||||
sAllSongs = songs;
|
||||
@ -589,7 +433,7 @@ public class MediaUtils {
|
||||
shuffle(sAllSongs);
|
||||
}
|
||||
|
||||
Song result = getSongByTypeId(resolver, MediaUtils.TYPE_SONG, sAllSongs[sAllSongsIdx]);
|
||||
Song result = getSongByTypeId(context, MediaUtils.TYPE_SONG, sAllSongs[sAllSongsIdx]);
|
||||
result.flags |= Song.FLAG_RANDOM;
|
||||
sAllSongsIdx++;
|
||||
return result;
|
||||
@ -669,11 +513,10 @@ public class MediaUtils {
|
||||
-> ended with a % for the LIKE query
|
||||
*/
|
||||
path = addDirEndSlash(sanitizeMediaPath(path)) + "%";
|
||||
final String query = "_data LIKE ? AND "+MediaStore.Audio.Media.IS_MUSIC;
|
||||
final String query = MediaLibrary.SongColumns.PATH+" LIKE ?";
|
||||
String[] qargs = { path };
|
||||
|
||||
Uri media = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
||||
QueryTask result = new QueryTask(media, projection, query, qargs, FILE_SORT);
|
||||
QueryTask result = new QueryTask(MediaLibrary.VIEW_SONGS_ALBUMS_ARTISTS, projection, query, qargs, FILE_SORT);
|
||||
result.type = TYPE_FILE;
|
||||
return result;
|
||||
}
|
||||
@ -685,18 +528,11 @@ public class MediaUtils {
|
||||
* */
|
||||
public static Cursor getCursorForFileQuery(String path) {
|
||||
MatrixCursor matrixCursor = new MatrixCursor(Song.FILLED_PROJECTION);
|
||||
MediaMetadataRetriever data = new MediaMetadataRetriever();
|
||||
|
||||
try {
|
||||
data.setDataSource(path);
|
||||
} catch (Exception e) {
|
||||
Log.w("VanillaMusic", "Failed to extract metadata from " + path);
|
||||
}
|
||||
|
||||
String title = data.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
|
||||
String album = data.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM);
|
||||
String artist = data.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST);
|
||||
String duration = data.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
|
||||
MediaMetadataExtractor tags = new MediaMetadataExtractor(path);
|
||||
String title = tags.getFirst(MediaMetadataExtractor.TITLE);
|
||||
String album = tags.getFirst(MediaMetadataExtractor.ALBUM);
|
||||
String artist = tags.getFirst(MediaMetadataExtractor.ARTIST);
|
||||
String duration = tags.getFirst(MediaMetadataExtractor.DURATION);
|
||||
|
||||
if (duration != null) { // looks like we will be able to play this file
|
||||
// Vanilla requires each file to be identified by its unique id in the media database.
|
||||
@ -704,9 +540,9 @@ public class MediaUtils {
|
||||
// using the negative crc32 sum of the path value. While this is not perfect
|
||||
// (the same file may be accessed using various paths) it's the fastest method
|
||||
// and far good enough.
|
||||
CRC32 crc = new CRC32();
|
||||
crc.update(path.getBytes());
|
||||
Long songId = (Long)(2+crc.getValue())*-1; // must at least be -2 (-1 defines Song-Object to be empty)
|
||||
long songId = MediaLibrary.hash63(path) * -1;
|
||||
if (songId > -2)
|
||||
songId = -2; // must be less than -1 (-1 defines an empty song object)
|
||||
|
||||
// Build minimal fake-database entry for this file
|
||||
Object[] objData = new Object[] { songId, path, "", "", "", 0, 0, 0, 0 };
|
||||
|
@ -422,7 +422,6 @@ public class MirrorLinkMediaBrowserService extends MediaBrowserService
|
||||
try {
|
||||
Cursor cursor = adapter.query();
|
||||
Context context = getApplicationContext();
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
|
||||
if (cursor == null) {
|
||||
return;
|
||||
@ -436,7 +435,7 @@ public class MirrorLinkMediaBrowserService extends MediaBrowserService
|
||||
final String label = cursor.getString(2);
|
||||
long mediaId = Long.parseLong(id);
|
||||
|
||||
Song song = MediaUtils.getSongByTypeId(resolver, mediaType, mediaId);
|
||||
Song song = MediaUtils.getSongByTypeId(context, mediaType, mediaId);
|
||||
MediaBrowser.MediaItem item = new MediaBrowser.MediaItem(
|
||||
new MediaDescription.Builder()
|
||||
.setMediaId(MediaID.toString(mediaType, mediaId, label))
|
||||
|
@ -17,68 +17,23 @@
|
||||
|
||||
package ch.blinkenlights.android.vanilla;
|
||||
|
||||
import ch.blinkenlights.android.medialibrary.MediaLibrary;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.ContentResolver;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.database.Cursor;
|
||||
import android.util.Log;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class PlayCountsHelper extends SQLiteOpenHelper {
|
||||
public class PlayCountsHelper {
|
||||
|
||||
/**
|
||||
* SQL constants and CREATE TABLE statements used by
|
||||
* this java class
|
||||
*/
|
||||
private static final int DATABASE_VERSION = 2;
|
||||
private static final String DATABASE_NAME = "playcounts.db";
|
||||
private static final String TABLE_PLAYCOUNTS = "playcounts";
|
||||
private static final String DATABASE_CREATE = "CREATE TABLE "+TABLE_PLAYCOUNTS + " ("
|
||||
+ "type INTEGER, "
|
||||
+ "type_id BIGINT, "
|
||||
+ "playcount INTEGER, "
|
||||
+ "skipcount INTEGER);";
|
||||
private static final String INDEX_UNIQUE_CREATE = "CREATE UNIQUE INDEX idx_uniq ON "+TABLE_PLAYCOUNTS
|
||||
+ " (type, type_id);";
|
||||
private static final String INDEX_TYPE_CREATE = "CREATE INDEX idx_type ON "+TABLE_PLAYCOUNTS
|
||||
+ " (type);";
|
||||
|
||||
private Context ctx;
|
||||
|
||||
public PlayCountsHelper(Context context) {
|
||||
super(context, DATABASE_NAME, null, DATABASE_VERSION);
|
||||
ctx = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase dbh) {
|
||||
dbh.execSQL(DATABASE_CREATE);
|
||||
dbh.execSQL(INDEX_UNIQUE_CREATE);
|
||||
dbh.execSQL(INDEX_TYPE_CREATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase dbh, int oldVersion, int newVersion) {
|
||||
if (oldVersion < 2) {
|
||||
dbh.execSQL("ALTER TABLE "+TABLE_PLAYCOUNTS+" ADD COLUMN skipcount INTEGER");
|
||||
dbh.execSQL("UPDATE "+TABLE_PLAYCOUNTS+" SET skipcount=0");
|
||||
}
|
||||
public PlayCountsHelper() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts this song object as 'played' or 'skipped'
|
||||
*/
|
||||
public void countSong(Song song, boolean played) {
|
||||
long id = Song.getId(song);
|
||||
final String column = played ? "playcount" : "skipcount";
|
||||
|
||||
SQLiteDatabase dbh = getWritableDatabase();
|
||||
dbh.execSQL("INSERT OR IGNORE INTO "+TABLE_PLAYCOUNTS+" (type, type_id, playcount, skipcount) VALUES ("+MediaUtils.TYPE_SONG+", "+id+", 0, 0);"); // Creates row if not exists
|
||||
dbh.execSQL("UPDATE "+TABLE_PLAYCOUNTS+" SET "+column+"="+column+"+1 WHERE type="+MediaUtils.TYPE_SONG+" AND type_id="+id+";");
|
||||
dbh.close();
|
||||
|
||||
performGC(MediaUtils.TYPE_SONG);
|
||||
public static void countSong(Context context, Song song, boolean played) {
|
||||
final long id = Song.getId(song);
|
||||
MediaLibrary.updateSongPlayCounts(context, id, played);
|
||||
}
|
||||
|
||||
|
||||
@ -86,52 +41,15 @@ public class PlayCountsHelper extends SQLiteOpenHelper {
|
||||
/**
|
||||
* Returns a sorted array list of most often listen song ids
|
||||
*/
|
||||
public ArrayList<Long> getTopSongs(int limit) {
|
||||
public static ArrayList<Long> getTopSongs(Context context, int limit) {
|
||||
ArrayList<Long> payload = new ArrayList<Long>();
|
||||
SQLiteDatabase dbh = getReadableDatabase();
|
||||
|
||||
Cursor cursor = dbh.rawQuery("SELECT type_id FROM "+TABLE_PLAYCOUNTS+" WHERE type="+MediaUtils.TYPE_SONG+" AND playcount != 0 ORDER BY playcount DESC limit "+limit, null);
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
Cursor cursor = MediaLibrary.queryLibrary(context, MediaLibrary.TABLE_SONGS, new String[]{ MediaLibrary.SongColumns._ID }, MediaLibrary.SongColumns.PLAYCOUNT+" > 0", null, MediaLibrary.SongColumns.PLAYCOUNT+" DESC");
|
||||
while (cursor.moveToNext() && limit > 0) {
|
||||
payload.add(cursor.getLong(0));
|
||||
limit--;
|
||||
}
|
||||
|
||||
cursor.close();
|
||||
dbh.close();
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Picks a random amount of 'type' items from the provided DBH
|
||||
* and checks them against Androids media database.
|
||||
* Items not found in the media library are removed from the DBH's database
|
||||
*/
|
||||
private int performGC(int type) {
|
||||
SQLiteDatabase dbh = getWritableDatabase();
|
||||
ArrayList<Long> toCheck = new ArrayList<Long>(); // List of songs we are going to check
|
||||
QueryTask query; // Reused query object
|
||||
Cursor cursor; // recycled cursor
|
||||
int removed = 0; // Amount of removed items
|
||||
|
||||
// We are just grabbing a bunch of random IDs
|
||||
cursor = dbh.rawQuery("SELECT type_id FROM "+TABLE_PLAYCOUNTS+" WHERE type="+type+" ORDER BY RANDOM() LIMIT 10", null);
|
||||
while (cursor.moveToNext()) {
|
||||
toCheck.add(cursor.getLong(0));
|
||||
}
|
||||
cursor.close();
|
||||
|
||||
for (Long id : toCheck) {
|
||||
query = MediaUtils.buildQuery(type, id, null, null);
|
||||
cursor = query.runQuery(ctx.getContentResolver());
|
||||
if(cursor.getCount() == 0) {
|
||||
dbh.execSQL("DELETE FROM "+TABLE_PLAYCOUNTS+" WHERE type="+type+" AND type_id="+id);
|
||||
removed++;
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
Log.v("VanillaMusic", "performGC: items removed="+removed);
|
||||
dbh.close();
|
||||
return removed;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -440,7 +440,7 @@ public abstract class PlaybackActivity extends Activity
|
||||
case MSG_CREATE_PLAYLIST: {
|
||||
PlaylistTask playlistTask = (PlaylistTask)message.obj;
|
||||
int nextAction = message.arg1;
|
||||
long playlistId = Playlist.createPlaylist(getContentResolver(), playlistTask.name);
|
||||
long playlistId = Playlist.createPlaylist(this, playlistTask.name);
|
||||
playlistTask.playlistId = playlistId;
|
||||
mHandler.sendMessage(mHandler.obtainMessage(nextAction, playlistTask));
|
||||
break;
|
||||
@ -471,7 +471,7 @@ public abstract class PlaybackActivity extends Activity
|
||||
}
|
||||
case MSG_RENAME_PLAYLIST: {
|
||||
PlaylistTask playlistTask = (PlaylistTask)message.obj;
|
||||
Playlist.renamePlaylist(getContentResolver(), playlistTask.playlistId, playlistTask.name);
|
||||
Playlist.renamePlaylist(getApplicationContext(), playlistTask.playlistId, playlistTask.name);
|
||||
break;
|
||||
}
|
||||
case MSG_DELETE: {
|
||||
@ -498,11 +498,11 @@ public abstract class PlaybackActivity extends Activity
|
||||
int count = 0;
|
||||
|
||||
if (playlistTask.query != null) {
|
||||
count += Playlist.addToPlaylist(getContentResolver(), playlistTask.playlistId, playlistTask.query);
|
||||
count += Playlist.addToPlaylist(this, playlistTask.playlistId, playlistTask.query);
|
||||
}
|
||||
|
||||
if (playlistTask.audioIds != null) {
|
||||
count += Playlist.addToPlaylist(getContentResolver(), playlistTask.playlistId, playlistTask.audioIds);
|
||||
count += Playlist.addToPlaylist(this, playlistTask.playlistId, playlistTask.audioIds);
|
||||
}
|
||||
|
||||
String message = getResources().getQuantityString(R.plurals.added_to_playlist, count, count, playlistTask.name);
|
||||
@ -524,7 +524,7 @@ public abstract class PlaybackActivity extends Activity
|
||||
}
|
||||
|
||||
if (playlistTask.audioIds != null) {
|
||||
count += Playlist.removeFromPlaylist(getContentResolver(), playlistTask.playlistId, playlistTask.audioIds);
|
||||
count += Playlist.removeFromPlaylist(getApplicationContext(), playlistTask.playlistId, playlistTask.audioIds);
|
||||
}
|
||||
|
||||
String message = getResources().getQuantityString(R.plurals.removed_from_playlist, count, count, playlistTask.name);
|
||||
@ -553,7 +553,7 @@ public abstract class PlaybackActivity extends Activity
|
||||
message = res.getString(R.string.delete_file_failed, file);
|
||||
}
|
||||
} else if (type == MediaUtils.TYPE_PLAYLIST) {
|
||||
Playlist.deletePlaylist(getContentResolver(), id);
|
||||
Playlist.deletePlaylist(this, id);
|
||||
} else {
|
||||
int count = PlaybackService.get(this).deleteMedia(type, id);
|
||||
message = res.getQuantityString(R.plurals.deleted, count, count);
|
||||
|
@ -23,6 +23,8 @@
|
||||
|
||||
package ch.blinkenlights.android.vanilla;
|
||||
|
||||
import ch.blinkenlights.android.medialibrary.MediaLibrary;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
@ -31,7 +33,6 @@ import android.app.backup.BackupManager;
|
||||
import android.appwidget.AppWidgetManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ComponentName;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
@ -420,10 +421,6 @@ public final class PlaybackService extends Service
|
||||
* Reference to precreated BASTP Object
|
||||
*/
|
||||
private BastpUtil mBastpUtil;
|
||||
/**
|
||||
* Reference to Playcounts helper class
|
||||
*/
|
||||
private PlayCountsHelper mPlayCounts;
|
||||
|
||||
@Override
|
||||
public void onCreate()
|
||||
@ -435,8 +432,6 @@ public final class PlaybackService extends Service
|
||||
mTimeline.setCallback(this);
|
||||
int state = loadState();
|
||||
|
||||
mPlayCounts = new PlayCountsHelper(this);
|
||||
|
||||
mMediaPlayer = getNewMediaPlayer();
|
||||
mPreparedMediaPlayer = getNewMediaPlayer();
|
||||
// We only have a single audio session
|
||||
@ -488,7 +483,7 @@ public final class PlaybackService extends Service
|
||||
filter.addAction(Intent.ACTION_SCREEN_ON);
|
||||
registerReceiver(mReceiver, filter);
|
||||
|
||||
getContentResolver().registerContentObserver(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, true, mObserver);
|
||||
MediaLibrary.registerContentObserver(mObserver);
|
||||
|
||||
mRemoteControlClient = new RemoteControl().getClient(this);
|
||||
mRemoteControlClient.initializeRemote();
|
||||
@ -1297,7 +1292,7 @@ public final class PlaybackService extends Service
|
||||
Song song = mTimeline.shiftCurrentSong(delta);
|
||||
mCurrentSong = song;
|
||||
if (song == null) {
|
||||
if (MediaUtils.isSongAvailable(getContentResolver())) {
|
||||
if (MediaUtils.isSongAvailable(getApplicationContext())) {
|
||||
int flag = finishAction(mState) == SongTimeline.FINISH_RANDOM ? FLAG_ERROR : FLAG_EMPTY_QUEUE;
|
||||
synchronized (mStateLock) {
|
||||
updateState((mState | flag) & ~FLAG_NO_MEDIA);
|
||||
@ -1447,7 +1442,7 @@ public final class PlaybackService extends Service
|
||||
|
||||
public void onMediaChange()
|
||||
{
|
||||
if (MediaUtils.isSongAvailable(getContentResolver())) {
|
||||
if (MediaUtils.isSongAvailable(getApplicationContext())) {
|
||||
if ((mState & FLAG_NO_MEDIA) != 0)
|
||||
setCurrentSong(0);
|
||||
} else {
|
||||
@ -1568,15 +1563,15 @@ public final class PlaybackService extends Service
|
||||
case MSG_UPDATE_PLAYCOUNTS:
|
||||
Song song = (Song)message.obj;
|
||||
boolean played = message.arg1 == 1;
|
||||
mPlayCounts.countSong(song, played);
|
||||
PlayCountsHelper.countSong(getApplicationContext(), song, played);
|
||||
// Update the playcounts playlist in ~20% of all cases if enabled
|
||||
if (mAutoPlPlaycounts > 0 && Math.random() > 0.8) {
|
||||
ContentResolver resolver = getContentResolver();
|
||||
Context context = getApplicationContext();
|
||||
// Add an invisible whitespace to adjust our sorting
|
||||
String playlistName = "\u200B"+getString(R.string.autoplaylist_playcounts_name, mAutoPlPlaycounts);
|
||||
long id = Playlist.createPlaylist(resolver, playlistName);
|
||||
ArrayList<Long> items = mPlayCounts.getTopSongs(mAutoPlPlaycounts);
|
||||
Playlist.addToPlaylist(resolver, id, items);
|
||||
String playlistName = getString(R.string.autoplaylist_playcounts_name, mAutoPlPlaycounts);
|
||||
long id = Playlist.createPlaylist(context, playlistName);
|
||||
ArrayList<Long> items = PlayCountsHelper.getTopSongs(context, mAutoPlPlaycounts);
|
||||
Playlist.addToPlaylist(context, id, items);
|
||||
}
|
||||
|
||||
|
||||
@ -1669,25 +1664,20 @@ public final class PlaybackService extends Service
|
||||
public int deleteMedia(int type, long id)
|
||||
{
|
||||
int count = 0;
|
||||
|
||||
ContentResolver resolver = getContentResolver();
|
||||
String[] projection = new String [] { MediaStore.Audio.Media._ID, MediaStore.Audio.Media.DATA };
|
||||
Cursor cursor = MediaUtils.buildQuery(type, id, projection, null).runQuery(resolver);
|
||||
String[] projection = new String [] { MediaLibrary.SongColumns._ID, MediaLibrary.SongColumns.PATH };
|
||||
Cursor cursor = MediaUtils.buildQuery(type, id, projection, null).runQuery(getApplicationContext());
|
||||
|
||||
if (cursor != null) {
|
||||
while (cursor.moveToNext()) {
|
||||
if (new File(cursor.getString(1)).delete()) {
|
||||
long songId = cursor.getLong(0);
|
||||
String where = MediaStore.Audio.Media._ID + '=' + songId;
|
||||
resolver.delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, where, null);
|
||||
MediaLibrary.removeSong(getApplicationContext(), songId);
|
||||
mTimeline.removeSong(songId);
|
||||
++count;
|
||||
}
|
||||
}
|
||||
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
@ -1787,6 +1777,7 @@ public final class PlaybackService extends Service
|
||||
mHandler.sendMessage(mHandler.obtainMessage(MSG_QUERY, query));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Enqueues all the songs with the same album/artist/genre as the passed
|
||||
* song.
|
||||
@ -1812,7 +1803,7 @@ public final class PlaybackService extends Service
|
||||
id = song.albumId;
|
||||
break;
|
||||
case MediaUtils.TYPE_GENRE:
|
||||
id = MediaUtils.queryGenreForSong(getContentResolver(), song.id);
|
||||
id = MediaUtils.queryGenreForSong(getApplicationContext(), song.id);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported media type: " + type);
|
||||
|
@ -23,50 +23,45 @@
|
||||
|
||||
package ch.blinkenlights.android.vanilla;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import ch.blinkenlights.android.medialibrary.MediaLibrary;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.MediaStore;
|
||||
import android.text.TextUtils;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Provides various playlist-related utility functions.
|
||||
*/
|
||||
public class Playlist {
|
||||
/**
|
||||
* Queries all the playlists known to the MediaStore.
|
||||
* Queries all the playlists known to the MediaLibrary.
|
||||
*
|
||||
* @param resolver A ContentResolver to use.
|
||||
* @param context the context to use
|
||||
* @return The queried cursor.
|
||||
*/
|
||||
public static Cursor queryPlaylists(ContentResolver resolver)
|
||||
{
|
||||
Uri media = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI;
|
||||
String[] projection = { MediaStore.Audio.Playlists._ID, MediaStore.Audio.Playlists.NAME };
|
||||
String sort = MediaStore.Audio.Playlists.NAME;
|
||||
return MediaUtils.queryResolver(resolver, media, projection, null, null, sort);
|
||||
public static Cursor queryPlaylists(Context context) {
|
||||
final String[] projection = { MediaLibrary.PlaylistColumns._ID, MediaLibrary.PlaylistColumns.NAME };
|
||||
final String sort = MediaLibrary.PlaylistColumns.NAME;
|
||||
return MediaLibrary.queryLibrary(context, MediaLibrary.TABLE_PLAYLISTS, projection, null, null, sort);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the id for a playlist with the given name.
|
||||
*
|
||||
* @param resolver A ContentResolver to use.
|
||||
* @param context the context to use
|
||||
* @param name The name of the playlist.
|
||||
* @return The id of the playlist, or -1 if there is no playlist with the
|
||||
* given name.
|
||||
*/
|
||||
public static long getPlaylist(ContentResolver resolver, String name)
|
||||
public static long getPlaylist(Context context, String name)
|
||||
{
|
||||
long id = -1;
|
||||
|
||||
Cursor cursor = MediaUtils.queryResolver(resolver, MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
|
||||
new String[] { MediaStore.Audio.Playlists._ID },
|
||||
MediaStore.Audio.Playlists.NAME + "=?",
|
||||
new String[] { name }, null);
|
||||
final String[] projection = { MediaLibrary.PlaylistColumns._ID };
|
||||
final String selection = MediaLibrary.PlaylistColumns.NAME+"=?";
|
||||
final String[] selectionArgs = { name };
|
||||
Cursor cursor = MediaLibrary.queryLibrary(context, MediaLibrary.TABLE_PLAYLISTS, projection, selection, selectionArgs, null);
|
||||
|
||||
if (cursor != null) {
|
||||
if (cursor.moveToNext())
|
||||
@ -81,31 +76,17 @@ public class Playlist {
|
||||
* Create a new playlist with the given name. If a playlist with the given
|
||||
* name already exists, it will be overwritten.
|
||||
*
|
||||
* @param resolver A ContentResolver to use.
|
||||
* @param context the context to use
|
||||
* @param name The name of the playlist.
|
||||
* @return The id of the new playlist.
|
||||
*/
|
||||
public static long createPlaylist(ContentResolver resolver, String name)
|
||||
public static long createPlaylist(Context context, String name)
|
||||
{
|
||||
long id = getPlaylist(resolver, name);
|
||||
|
||||
if (id == -1) {
|
||||
// We need to create a new playlist.
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(MediaStore.Audio.Playlists.NAME, name);
|
||||
Uri uri = resolver.insert(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, values);
|
||||
/* Creating the playlist may fail due to race conditions or silly
|
||||
* android bugs (i am looking at you, kitkat!). In this case, id will stay -1
|
||||
*/
|
||||
if (uri != null) {
|
||||
id = Long.parseLong(uri.getLastPathSegment());
|
||||
}
|
||||
} else {
|
||||
// We are overwriting an existing playlist. Clear existing songs.
|
||||
Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", id);
|
||||
resolver.delete(uri, null, null);
|
||||
}
|
||||
long id = getPlaylist(context, name);
|
||||
if (id != -1)
|
||||
deletePlaylist(context, id);
|
||||
|
||||
id = MediaLibrary.createPlaylist(context, name);
|
||||
return id;
|
||||
}
|
||||
|
||||
@ -113,122 +94,78 @@ public class Playlist {
|
||||
* Run the given query and add the results to the given playlist. Should be
|
||||
* run on a background thread.
|
||||
*
|
||||
* @param resolver A ContentResolver to use.
|
||||
* @param playlistId The MediaStore.Audio.Playlist id of the playlist to
|
||||
* @param context the context to use
|
||||
* @param playlistId The playlist id of the playlist to
|
||||
* modify.
|
||||
* @param query The query to run. The audio id should be the first column.
|
||||
* @return The number of songs that were added to the playlist.
|
||||
*/
|
||||
public static int addToPlaylist(ContentResolver resolver, long playlistId, QueryTask query) {
|
||||
public static int addToPlaylist(Context context, long playlistId, QueryTask query) {
|
||||
ArrayList<Long> result = new ArrayList<Long>();
|
||||
Cursor cursor = query.runQuery(resolver);
|
||||
Cursor cursor = query.runQuery(context);
|
||||
if (cursor != null) {
|
||||
while (cursor.moveToNext()) {
|
||||
result.add(cursor.getLong(0));
|
||||
}
|
||||
}
|
||||
return addToPlaylist(resolver, playlistId, result);
|
||||
return addToPlaylist(context, playlistId, result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a set of audioIds to the given playlist. Should be
|
||||
* run on a background thread.
|
||||
*
|
||||
* @param resolver A ContentResolver to use.
|
||||
* @param playlistId The MediaStore.Audio.Playlist id of the playlist to
|
||||
* @param context the context to use
|
||||
* @param playlistId The playlist id of the playlist to
|
||||
* modify.
|
||||
* @param audioIds An ArrayList with all IDs to add
|
||||
* @return The number of songs that were added to the playlist.
|
||||
*/
|
||||
public static int addToPlaylist(ContentResolver resolver, long playlistId, ArrayList<Long> audioIds) {
|
||||
public static int addToPlaylist(Context context, long playlistId, ArrayList<Long> audioIds) {
|
||||
if (playlistId == -1)
|
||||
return 0;
|
||||
|
||||
// Find the greatest PLAY_ORDER in the playlist
|
||||
Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId);
|
||||
String[] projection = new String[] { MediaStore.Audio.Playlists.Members.PLAY_ORDER };
|
||||
Cursor cursor = MediaUtils.queryResolver(resolver, uri, projection, null, null, null);
|
||||
int base = 0;
|
||||
if (cursor.moveToLast())
|
||||
base = cursor.getInt(0) + 1;
|
||||
cursor.close();
|
||||
|
||||
int count = audioIds.size();
|
||||
if (count > 0) {
|
||||
ContentValues[] values = new ContentValues[count];
|
||||
for (int i = 0; i != count; ++i) {
|
||||
ContentValues value = new ContentValues(2);
|
||||
value.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(base + i));
|
||||
value.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, audioIds.get(i));
|
||||
values[i] = value;
|
||||
}
|
||||
resolver.bulkInsert(uri, values);
|
||||
}
|
||||
|
||||
return count;
|
||||
return MediaLibrary.addToPlaylist(context, playlistId, audioIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a set of audioIds from the given playlist. Should be
|
||||
* run on a background thread.
|
||||
*
|
||||
* @param resolver A ContentResolver to use.
|
||||
* @param playlistId The MediaStore.Audio.Playlist id of the playlist to
|
||||
* @param context the context to use
|
||||
* @param playlistId id of the playlist to
|
||||
* modify.
|
||||
* @param audioIds An ArrayList with all IDs to add
|
||||
* @return The number of songs that were added to the playlist.
|
||||
* @param audioIds An ArrayList with all IDs to drop
|
||||
* @return The number of songs that were removed from the playlist
|
||||
*/
|
||||
public static int removeFromPlaylist(ContentResolver resolver, long playlistId, ArrayList<Long> audioIds) {
|
||||
public static int removeFromPlaylist(Context context, long playlistId, ArrayList<Long> audioIds) {
|
||||
if (playlistId == -1)
|
||||
return 0;
|
||||
|
||||
int count = 0;
|
||||
Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId);
|
||||
for (long id : audioIds) {
|
||||
String where = MediaStore.Audio.Playlists.Members.AUDIO_ID + "=" + id;
|
||||
count += resolver.delete(uri, where, null);
|
||||
}
|
||||
return count;
|
||||
String idList = TextUtils.join(", ", audioIds);
|
||||
String selection = MediaLibrary.PlaylistSongColumns.SONG_ID+" IN ("+idList+") AND "+MediaLibrary.PlaylistSongColumns.PLAYLIST_ID+"="+playlistId;
|
||||
return MediaLibrary.removeFromPlaylist(context, selection, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the playlist with the given id.
|
||||
*
|
||||
* @param resolver A ContentResolver to use.
|
||||
* @param id The Media.Audio.Playlists id of the playlist.
|
||||
* @param context the context to use
|
||||
* @param id the id of the playlist.
|
||||
*/
|
||||
public static void deletePlaylist(ContentResolver resolver, long id)
|
||||
{
|
||||
Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, id);
|
||||
resolver.delete(uri, null, null);
|
||||
public static void deletePlaylist(Context context, long id) {
|
||||
MediaLibrary.removePlaylist(context, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy content from one playlist to another
|
||||
*
|
||||
* @param resolver A ContentResolver to use.
|
||||
* @param sourceId The Media.Audio.Playlists id of the source playlist
|
||||
* @param destinationId The Media.Audio.Playlists id of the destination playlist
|
||||
*/
|
||||
private static void _copyToPlaylist(ContentResolver resolver, long sourceId, long destinationId) {
|
||||
QueryTask query = MediaUtils.buildPlaylistQuery(sourceId, Song.FILLED_PLAYLIST_PROJECTION, null);
|
||||
addToPlaylist(resolver, destinationId, query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename the playlist with the given id.
|
||||
*
|
||||
* @param resolver A ContentResolver to use.
|
||||
* @param context the context to use
|
||||
* @param id The Media.Audio.Playlists id of the playlist.
|
||||
* @param newName The new name for the playlist.
|
||||
*/
|
||||
public static void renamePlaylist(ContentResolver resolver, long id, String newName)
|
||||
{
|
||||
long newId = createPlaylist(resolver, newName);
|
||||
if (newId != -1) { // new playlist created -> move stuff over
|
||||
_copyToPlaylist(resolver, id, newId);
|
||||
deletePlaylist(resolver, id);
|
||||
}
|
||||
public static void renamePlaylist(Context context, long id, String newName) {
|
||||
MediaLibrary.renamePlaylist(context, id, newName);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -240,10 +177,10 @@ public class Playlist {
|
||||
*/
|
||||
public static long getFavoritesId(Context context, boolean create) {
|
||||
String playlistName = context.getString(R.string.playlist_favorites);
|
||||
long playlistId = getPlaylist(context.getContentResolver(), playlistName);
|
||||
long playlistId = getPlaylist(context, playlistName);
|
||||
|
||||
if (playlistId == -1 && create == true)
|
||||
playlistId = createPlaylist(context.getContentResolver(), playlistName);
|
||||
playlistId = createPlaylist(context, playlistName);
|
||||
|
||||
return playlistId;
|
||||
}
|
||||
@ -251,19 +188,20 @@ public class Playlist {
|
||||
/**
|
||||
* Searches for given song in given playlist
|
||||
*
|
||||
* @param resolver A ContentResolver to use.
|
||||
* @param context the context to use
|
||||
* @param playlistId The ID of the Playlist to query
|
||||
* @param song The Song to search in given playlistId
|
||||
* @return true if `song' was found in `playlistId'
|
||||
*/
|
||||
public static boolean isInPlaylist(ContentResolver resolver, long playlistId, Song song) {
|
||||
public static boolean isInPlaylist(Context context, long playlistId, Song song) {
|
||||
if (playlistId == -1 || song == null)
|
||||
return false;
|
||||
|
||||
boolean found = false;
|
||||
String where = MediaStore.Audio.Playlists.Members.AUDIO_ID + "=" + song.id;
|
||||
QueryTask query = MediaUtils.buildPlaylistQuery(playlistId, Song.EMPTY_PLAYLIST_PROJECTION, where);
|
||||
Cursor cursor = query.runQuery(resolver);
|
||||
String selection = MediaLibrary.PlaylistSongColumns.PLAYLIST_ID+"=? AND "+MediaLibrary.PlaylistSongColumns.SONG_ID+"=?";
|
||||
String[] selectionArgs = { Long.toString(playlistId), Long.toString(song.id) };
|
||||
|
||||
Cursor cursor = MediaLibrary.queryLibrary(context, MediaLibrary.TABLE_PLAYLISTS_SONGS, Song.EMPTY_PLAYLIST_PROJECTION, selection, selectionArgs, null);
|
||||
if (cursor != null) {
|
||||
found = cursor.getCount() != 0;
|
||||
cursor.close();
|
||||
|
@ -250,7 +250,7 @@ public class PlaylistActivity extends Activity
|
||||
}
|
||||
case LibraryActivity.ACTION_PLAY_ALL:
|
||||
case LibraryActivity.ACTION_ENQUEUE_ALL: {
|
||||
QueryTask query = MediaUtils.buildPlaylistQuery(mPlaylistId, Song.FILLED_PLAYLIST_PROJECTION, null);
|
||||
QueryTask query = MediaUtils.buildPlaylistQuery(mPlaylistId, Song.FILLED_PLAYLIST_PROJECTION);
|
||||
query.mode = MODE_FOR_ACTION[action];
|
||||
query.data = position - mListView.getHeaderViewsCount();
|
||||
PlaybackService.get(this).addSongs(query);
|
||||
@ -275,7 +275,7 @@ public class PlaylistActivity extends Activity
|
||||
public void onClick(DialogInterface dialog, int which)
|
||||
{
|
||||
if (which == DialogInterface.BUTTON_POSITIVE) {
|
||||
Playlist.deletePlaylist(getContentResolver(), mPlaylistId);
|
||||
Playlist.deletePlaylist(this, mPlaylistId);
|
||||
finish();
|
||||
}
|
||||
dialog.dismiss();
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Copyright (C) 2011 Christopher Eby <kreed@kreed.org>
|
||||
* Copyright (C) 2015 Adrian Ulrich <adrian@blinkenlights.ch>
|
||||
* Copyright (C) 2015-2016 Adrian Ulrich <adrian@blinkenlights.ch>
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
@ -23,8 +23,8 @@
|
||||
|
||||
package ch.blinkenlights.android.vanilla;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentUris;
|
||||
import ch.blinkenlights.android.medialibrary.MediaLibrary;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
@ -32,25 +32,24 @@ import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.provider.MediaStore;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CursorAdapter;
|
||||
import android.widget.TextView;
|
||||
import android.provider.MediaStore.Audio.Playlists.Members;
|
||||
|
||||
/**
|
||||
* CursorAdapter backed by MediaStore playlists.
|
||||
* CursorAdapter backed by MediaLibrary playlists.
|
||||
*/
|
||||
public class PlaylistAdapter extends CursorAdapter implements Handler.Callback {
|
||||
|
||||
private static final String[] PROJECTION = new String[] {
|
||||
MediaStore.Audio.Playlists.Members._ID,
|
||||
MediaStore.Audio.Playlists.Members.TITLE,
|
||||
MediaStore.Audio.Playlists.Members.ARTIST,
|
||||
MediaStore.Audio.Playlists.Members.AUDIO_ID,
|
||||
MediaStore.Audio.Playlists.Members.ALBUM_ID,
|
||||
MediaStore.Audio.Playlists.Members.PLAY_ORDER,
|
||||
MediaLibrary.PlaylistSongColumns._ID,
|
||||
MediaLibrary.SongColumns.TITLE,
|
||||
MediaLibrary.ContributorColumns.ARTIST,
|
||||
MediaLibrary.PlaylistSongColumns.SONG_ID,
|
||||
MediaLibrary.SongColumns.ALBUM_ID,
|
||||
MediaLibrary.PlaylistSongColumns.POSITION,
|
||||
};
|
||||
|
||||
private final Context mContext;
|
||||
@ -81,7 +80,7 @@ public class PlaylistAdapter extends CursorAdapter implements Handler.Callback {
|
||||
/**
|
||||
* Set the id of the backing playlist.
|
||||
*
|
||||
* @param id The MediaStore id of a playlist.
|
||||
* @param id The id of a playlist.
|
||||
*/
|
||||
public void setPlaylistId(long id)
|
||||
{
|
||||
@ -142,7 +141,7 @@ public class PlaylistAdapter extends CursorAdapter implements Handler.Callback {
|
||||
{
|
||||
switch (message.what) {
|
||||
case MSG_RUN_QUERY: {
|
||||
Cursor cursor = runQuery(mContext.getContentResolver());
|
||||
Cursor cursor = runQuery();
|
||||
mUiHandler.sendMessage(mUiHandler.obtainMessage(MSG_UPDATE_CURSOR, cursor));
|
||||
break;
|
||||
}
|
||||
@ -159,13 +158,12 @@ public class PlaylistAdapter extends CursorAdapter implements Handler.Callback {
|
||||
/**
|
||||
* Query the playlist songs.
|
||||
*
|
||||
* @param resolver A ContentResolver to query with.
|
||||
* @return The resulting cursor.
|
||||
*/
|
||||
private Cursor runQuery(ContentResolver resolver)
|
||||
private Cursor runQuery()
|
||||
{
|
||||
QueryTask query = MediaUtils.buildPlaylistQuery(mPlaylistId, PROJECTION, null);
|
||||
return query.runQuery(resolver);
|
||||
QueryTask query = MediaUtils.buildPlaylistQuery(mPlaylistId, PROJECTION);
|
||||
return query.runQuery(mContext);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -184,53 +182,12 @@ public class PlaylistAdapter extends CursorAdapter implements Handler.Callback {
|
||||
// this can happen when the adapter changes during the drag
|
||||
return;
|
||||
|
||||
// The Android API contains a method to move a playlist item, however,
|
||||
// it has only been available since Froyo and doesn't seem to work
|
||||
// after a song has been removed from the playlist (I think?).
|
||||
|
||||
ContentResolver resolver = mContext.getContentResolver();
|
||||
Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", mPlaylistId);
|
||||
Cursor cursor = getCursor();
|
||||
|
||||
int start = Math.min(from, to);
|
||||
int end = Math.max(from, to);
|
||||
|
||||
long order;
|
||||
if (start == 0) {
|
||||
order = 0;
|
||||
} else {
|
||||
cursor.moveToPosition(start - 1);
|
||||
order = cursor.getLong(5) + 1;
|
||||
}
|
||||
|
||||
cursor.moveToPosition(end);
|
||||
long endOrder = cursor.getLong(5);
|
||||
|
||||
// clear the rows we are replacing
|
||||
String[] args = new String[] { Long.toString(order), Long.toString(endOrder) };
|
||||
resolver.delete(uri, "play_order >= ? AND play_order <= ?", args);
|
||||
|
||||
// create the new rows
|
||||
ContentValues[] values = new ContentValues[end - start + 1];
|
||||
for (int i = start, j = 0; i <= end; ++i, ++j, ++order) {
|
||||
cursor.moveToPosition(i == to ? from : i > to ? i - 1 : i + 1);
|
||||
ContentValues value = new ContentValues(2);
|
||||
value.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Long.valueOf(order));
|
||||
value.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, cursor.getLong(3));
|
||||
values[j] = value;
|
||||
}
|
||||
|
||||
// insert the new rows
|
||||
resolver.bulkInsert(uri, values);
|
||||
|
||||
changeCursor(runQuery(resolver));
|
||||
MediaLibrary.movePlaylistItem(mContext, getItemId(from), getItemId(to));
|
||||
changeCursor(runQuery());
|
||||
}
|
||||
|
||||
public void removeItem(int position)
|
||||
{
|
||||
ContentResolver resolver = mContext.getContentResolver();
|
||||
Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", mPlaylistId);
|
||||
resolver.delete(ContentUris.withAppendedId(uri, getItemId(position)), null, null);
|
||||
public void removeItem(int position) {
|
||||
MediaLibrary.removeFromPlaylist(mContext, MediaLibrary.PlaylistSongColumns._ID+"="+getItemId(position), null);
|
||||
mUiHandler.sendEmptyMessage(MSG_RUN_QUERY);
|
||||
}
|
||||
|
||||
|
@ -83,7 +83,7 @@ public class PlaylistDialog extends DialogFragment
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
Cursor cursor = Playlist.queryPlaylists(getActivity().getContentResolver());
|
||||
Cursor cursor = Playlist.queryPlaylists(getActivity());
|
||||
if (cursor == null)
|
||||
return null;
|
||||
|
||||
|
@ -25,7 +25,6 @@ package ch.blinkenlights.android.vanilla;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.app.DialogFragment;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
@ -128,8 +127,7 @@ public class PlaylistInputDialog extends DialogFragment
|
||||
mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(false);
|
||||
} else {
|
||||
mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(true);
|
||||
ContentResolver resolver = getActivity().getContentResolver();
|
||||
int res = Playlist.getPlaylist(resolver, string) == -1 ? mActionRes : R.string.overwrite;
|
||||
int res = Playlist.getPlaylist(getActivity(), string) == -1 ? mActionRes : R.string.overwrite;
|
||||
mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setText(res);
|
||||
}
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ public class PreferencesTheme extends PreferenceFragment
|
||||
pref.setPersistent(false);
|
||||
pref.setOnPreferenceClickListener(this);
|
||||
pref.setTitle(entries[i]);
|
||||
pref.setKey(""+attrs[0]); // that's actually our value
|
||||
pref.setKey(Long.toString(attrs[0])); // that's actually our value
|
||||
pref.setIcon(generateThemePreview(attrs));
|
||||
screen.addPreference(pref);
|
||||
}
|
||||
|
@ -1,36 +1,33 @@
|
||||
/*
|
||||
* Copyright (C) 2011 Christopher Eby <kreed@kreed.org>
|
||||
* Copyright (C) 2016 Adrian Ulrich <adrian@blinkenlights.ch>
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
package ch.blinkenlights.android.vanilla;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import ch.blinkenlights.android.medialibrary.MediaLibrary;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
|
||||
/**
|
||||
* Represents a pending query.
|
||||
*/
|
||||
public class QueryTask {
|
||||
public Uri uri;
|
||||
public final String table;
|
||||
public final String[] projection;
|
||||
public final String selection;
|
||||
public final String[] selectionArgs;
|
||||
@ -55,11 +52,10 @@ public class QueryTask {
|
||||
|
||||
/**
|
||||
* Create the tasks. All arguments are passed directly to
|
||||
* ContentResolver.query().
|
||||
* MediaLibrary.runQuery().
|
||||
*/
|
||||
public QueryTask(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
|
||||
{
|
||||
this.uri = uri;
|
||||
public QueryTask(String table, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
|
||||
this.table = table;
|
||||
this.projection = projection;
|
||||
this.selection = selection;
|
||||
this.selectionArgs = selectionArgs;
|
||||
@ -69,10 +65,9 @@ public class QueryTask {
|
||||
/**
|
||||
* Run the query. Should be called on a background thread.
|
||||
*
|
||||
* @param resolver The ContentResolver to query with.
|
||||
* @param context The Context to use
|
||||
*/
|
||||
public Cursor runQuery(ContentResolver resolver)
|
||||
{
|
||||
return MediaUtils.queryResolver(resolver, uri, projection, selection, selectionArgs, sortOrder);
|
||||
public Cursor runQuery(Context context) {
|
||||
return MediaLibrary.queryLibrary(context, table, projection, selection, selectionArgs, sortOrder);
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
/* SD Scanner - A manual implementation of the SD rescan process, compatible
|
||||
* with Android 4.4
|
||||
*
|
||||
* Copyright (C) 2013-2014 Jeremy Erickson
|
||||
/*
|
||||
* Copyright (C) 2016 Xiao Bao Clark
|
||||
* Copyright (C) 2016 Adrian Ulrich
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@ -17,73 +15,22 @@
|
||||
|
||||
package ch.blinkenlights.android.vanilla;
|
||||
|
||||
import ch.blinkenlights.android.medialibrary.MediaLibrary;
|
||||
|
||||
import android.app.Fragment;
|
||||
import android.app.FragmentManager;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.text.method.ScrollingMovementMethod;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.gmail.jerickson314.sdscanner.ScanFragment;
|
||||
import com.gmail.jerickson314.sdscanner.UIStringGenerator;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
|
||||
/**
|
||||
* Fragment version of the MainActivity from the SD Scanner app
|
||||
*/
|
||||
public class SDScannerFragment extends Fragment
|
||||
implements ScanFragment.ScanProgressCallbacks
|
||||
{
|
||||
private static ScanFragment mScanFragment;
|
||||
|
||||
/**
|
||||
* List of common directories with media files
|
||||
*/
|
||||
private File[] mScanTargetStages = {};
|
||||
|
||||
@Override
|
||||
public void updateProgressNum(int progressNum) {
|
||||
ProgressBar progressBar = (ProgressBar)findViewById(R.id.progress_bar);
|
||||
progressBar.setProgress(progressNum);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateProgressText(UIStringGenerator progressText) {
|
||||
TextView progressLabel = (TextView)findViewById(R.id.progress_label);
|
||||
progressLabel.setText(progressText.toString(getActivity()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDebugMessages(UIStringGenerator debugMessages) {
|
||||
TextView debugLabel = (TextView)findViewById(R.id.debug_label);
|
||||
debugLabel.setText(debugMessages.toString(getActivity()));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void updateStartButtonEnabled(boolean startButtonEnabled) {
|
||||
Button startButton = (Button)findViewById(R.id.start_button);
|
||||
startButton.setEnabled(startButtonEnabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updatePath(String path) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void signalFinished() {
|
||||
}
|
||||
private Timer mTimer;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
@ -93,110 +40,53 @@ public class SDScannerFragment extends Fragment
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
// Setup with values from fragment.
|
||||
updateProgressNum(mScanFragment.getProgressNum());
|
||||
updateProgressText(mScanFragment.getProgressText());
|
||||
updateDebugMessages(mScanFragment.getDebugMessages());
|
||||
updateStartButtonEnabled(mScanFragment.getStartButtonEnabled());
|
||||
|
||||
// Make debug output scrollable.
|
||||
TextView debugLabel = (TextView)findViewById(R.id.debug_label);
|
||||
debugLabel.setMovementMethod(new ScrollingMovementMethod());
|
||||
|
||||
view.findViewById(R.id.start_button).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
try {
|
||||
startButtonPressed(v);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
startButtonPressed(v);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Called when the activity is first created. */
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState)
|
||||
{
|
||||
super.onCreate(savedInstanceState);
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
Log.v("VanillaMusic", "onResume! "+mTimer);
|
||||
|
||||
mScanTargetStages = getScanTargets(getActivity().getApplicationContext());
|
||||
|
||||
FragmentManager fm = getFragmentManager();
|
||||
|
||||
if (mScanFragment == null)
|
||||
mScanFragment = (ScanFragment) fm.findFragmentByTag("scan");
|
||||
|
||||
if (mScanFragment == null)
|
||||
mScanFragment = new ScanFragment();
|
||||
|
||||
fm.beginTransaction().add(mScanFragment, "scan").commit();
|
||||
mScanFragment.setScanProgressCallbacks(this);
|
||||
mTimer = new Timer();
|
||||
mTimer.scheduleAtFixedRate((new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
getActivity().runOnUiThread(new Runnable(){
|
||||
public void run() {
|
||||
updateProgress();
|
||||
}
|
||||
});
|
||||
}}), 0, 120);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
mScanFragment.setScanProgressCallbacks(null);
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
Log.v("VanillaMusic", "onPause "+mTimer);
|
||||
if (mTimer != null) {
|
||||
mTimer.cancel();
|
||||
mTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private View findViewById(int viewId) {
|
||||
return getView().findViewById(viewId);
|
||||
private void updateProgress() {
|
||||
View button = getActivity().findViewById(R.id.start_button);
|
||||
TextView progress = (TextView)getActivity().findViewById(R.id.progress_label);
|
||||
String scanText = MediaLibrary.describeScanProgress(getActivity());
|
||||
progress.setText(scanText);
|
||||
button.setEnabled(scanText == null);
|
||||
}
|
||||
|
||||
public void startButtonPressed(View view) throws IOException {
|
||||
mScanFragment.startScan(mScanTargetStages);
|
||||
public void startButtonPressed(View view) {
|
||||
MediaLibrary.scanLibrary(getActivity(), true, false);
|
||||
updateProgress();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a list of directories that can be scanned. Uses
|
||||
* {@link Environment#getExternalStorageDirectory} and {@link Context#getExternalFilesDirs}
|
||||
* (on KITKAT+).
|
||||
*
|
||||
* @param context The context
|
||||
* @return A list of unique directories that can be scanned.
|
||||
*/
|
||||
private File[] getScanTargets(Context context) {
|
||||
ArrayList<File> scanTargets = new ArrayList<>();
|
||||
// Put canonical paths into a HashSet to avoid duplicates
|
||||
HashSet<String> possibleTargets = new HashSet<>();
|
||||
try {
|
||||
possibleTargets.add(Environment.getExternalStorageDirectory().getCanonicalPath());
|
||||
} catch (IOException e) {
|
||||
// Shouldn't happen, but just in case it does add the external storage dir directly
|
||||
scanTargets.add(Environment.getExternalStorageDirectory());
|
||||
}
|
||||
|
||||
File sdcard1 = new File("/storage/sdcard1");
|
||||
if (sdcard1.exists()) {
|
||||
try {
|
||||
possibleTargets.add(sdcard1.getCanonicalPath());
|
||||
} catch (IOException e) {
|
||||
// Shouldn't happen, but just in case it does add the "storage/sdcard1" dir directly
|
||||
scanTargets.add(sdcard1);
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
// Attempt to divine external storage directories from 'getExternalFilesDirs'
|
||||
File[] externalFilesDirs = context.getExternalFilesDirs(null);
|
||||
String packageDirSuffix = "/Android/data/" + context.getPackageName() + "/files";
|
||||
for (int i = 0; i < externalFilesDirs.length; i++) {
|
||||
try {
|
||||
String storageDir = externalFilesDirs[i].getCanonicalPath().replace(packageDirSuffix, "");
|
||||
possibleTargets.add(storageDir);
|
||||
} catch (IOException e) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
for (String possibleTarget : possibleTargets) {
|
||||
File file = new File(possibleTarget);
|
||||
if (file.exists()) {
|
||||
scanTargets.add(file);
|
||||
}
|
||||
}
|
||||
return scanTargets.toArray(new File[scanTargets.size()]);
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,8 @@
|
||||
|
||||
package ch.blinkenlights.android.vanilla;
|
||||
|
||||
import ch.blinkenlights.android.medialibrary.MediaLibrary;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
@ -47,38 +49,37 @@ public class Song implements Comparable<Song> {
|
||||
|
||||
|
||||
public static final String[] EMPTY_PROJECTION = {
|
||||
MediaStore.Audio.Media._ID,
|
||||
MediaLibrary.SongColumns._ID,
|
||||
};
|
||||
|
||||
public static final String[] FILLED_PROJECTION = {
|
||||
MediaStore.Audio.Media._ID,
|
||||
MediaStore.Audio.Media.DATA,
|
||||
MediaStore.Audio.Media.TITLE,
|
||||
MediaStore.Audio.Media.ALBUM,
|
||||
MediaStore.Audio.Media.ARTIST,
|
||||
MediaStore.Audio.Media.ALBUM_ID,
|
||||
MediaStore.Audio.Media.ARTIST_ID,
|
||||
MediaStore.Audio.Media.DURATION,
|
||||
MediaStore.Audio.Media.TRACK,
|
||||
MediaLibrary.SongColumns._ID,
|
||||
MediaLibrary.SongColumns.PATH,
|
||||
MediaLibrary.SongColumns.TITLE,
|
||||
MediaLibrary.AlbumColumns.ALBUM,
|
||||
MediaLibrary.ContributorColumns.ARTIST,
|
||||
MediaLibrary.SongColumns.ALBUM_ID,
|
||||
MediaLibrary.ContributorColumns.ARTIST_ID,
|
||||
MediaLibrary.SongColumns.DURATION,
|
||||
MediaLibrary.SongColumns.SONG_NUMBER,
|
||||
};
|
||||
|
||||
public static final String[] EMPTY_PLAYLIST_PROJECTION = {
|
||||
MediaStore.Audio.Playlists.Members.AUDIO_ID,
|
||||
MediaLibrary.PlaylistSongColumns.SONG_ID,
|
||||
};
|
||||
|
||||
public static final String[] FILLED_PLAYLIST_PROJECTION = {
|
||||
MediaStore.Audio.Playlists.Members.AUDIO_ID,
|
||||
MediaStore.Audio.Playlists.Members.DATA,
|
||||
MediaStore.Audio.Playlists.Members.TITLE,
|
||||
MediaStore.Audio.Playlists.Members.ALBUM,
|
||||
MediaStore.Audio.Playlists.Members.ARTIST,
|
||||
MediaStore.Audio.Playlists.Members.ALBUM_ID,
|
||||
MediaStore.Audio.Playlists.Members.ARTIST_ID,
|
||||
MediaStore.Audio.Playlists.Members.DURATION,
|
||||
MediaStore.Audio.Playlists.Members.TRACK,
|
||||
MediaLibrary.PlaylistSongColumns.SONG_ID,
|
||||
MediaLibrary.SongColumns.PATH,
|
||||
MediaLibrary.SongColumns.TITLE,
|
||||
MediaLibrary.AlbumColumns.ALBUM,
|
||||
MediaLibrary.ContributorColumns.ARTIST,
|
||||
MediaLibrary.SongColumns.ALBUM_ID,
|
||||
MediaLibrary.ContributorColumns.ARTIST_ID,
|
||||
MediaLibrary.SongColumns.DURATION,
|
||||
MediaLibrary.SongColumns.SONG_NUMBER,
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* The cache instance.
|
||||
*/
|
||||
|
@ -23,7 +23,8 @@
|
||||
|
||||
package ch.blinkenlights.android.vanilla;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import ch.blinkenlights.android.medialibrary.MediaLibrary;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
@ -326,7 +327,7 @@ public final class SongTimeline {
|
||||
|
||||
// Fill the selection with the ids of all the saved songs
|
||||
// and initialize the timeline with unpopulated songs.
|
||||
StringBuilder selection = new StringBuilder("_ID IN (");
|
||||
StringBuilder selection = new StringBuilder(MediaLibrary.SongColumns._ID+" IN (");
|
||||
for (int i = 0; i != n; ++i) {
|
||||
long id = in.readLong();
|
||||
if (id == -1)
|
||||
@ -346,10 +347,8 @@ public final class SongTimeline {
|
||||
// return its results in.
|
||||
Collections.sort(songs, new IdComparator());
|
||||
|
||||
ContentResolver resolver = mContext.getContentResolver();
|
||||
Uri media = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
||||
|
||||
Cursor cursor = MediaUtils.queryResolver(resolver, media, Song.FILLED_PROJECTION, selection.toString(), null, "_id");
|
||||
QueryTask query = new QueryTask(MediaLibrary.VIEW_SONGS_ALBUMS_ARTISTS, Song.FILLED_PROJECTION, selection.toString(), null, MediaLibrary.SongColumns._ID);
|
||||
Cursor cursor = query.runQuery(mContext);
|
||||
if (cursor != null) {
|
||||
if (cursor.getCount() != 0) {
|
||||
cursor.moveToNext();
|
||||
@ -576,7 +575,7 @@ public final class SongTimeline {
|
||||
return null;
|
||||
} else if (pos == size) {
|
||||
if (mFinishAction == FINISH_RANDOM) {
|
||||
song = MediaUtils.getRandomSong(mContext.getContentResolver());
|
||||
song = MediaUtils.getRandomSong(mContext);
|
||||
if (song == null)
|
||||
return null;
|
||||
timeline.add(song);
|
||||
@ -700,7 +699,7 @@ public final class SongTimeline {
|
||||
*/
|
||||
public int addSongs(Context context, QueryTask query)
|
||||
{
|
||||
Cursor cursor = query.runQuery(context.getContentResolver());
|
||||
Cursor cursor = query.runQuery(context);
|
||||
if (cursor == null) {
|
||||
return 0;
|
||||
}
|
||||
|
@ -1,555 +0,0 @@
|
||||
/* SD Scanner - A manual implementation of the SD rescan process, compatible
|
||||
* with Android 4.4.
|
||||
*
|
||||
* This file contains the fragment that actually performs all scan activity
|
||||
* and retains state across configuration changes.
|
||||
*
|
||||
* Copyright (C) 2013-2014 Jeremy Erickson
|
||||
*
|
||||
* 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 2 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.
|
||||
*/
|
||||
|
||||
package com.gmail.jerickson314.sdscanner;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Fragment;
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.Cursor;
|
||||
import android.database.DatabaseUtils;
|
||||
import android.media.MediaScannerConnection;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.Handler;
|
||||
import android.os.SystemClock;
|
||||
import android.provider.MediaStore;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import ch.blinkenlights.android.vanilla.R;
|
||||
|
||||
public class ScanFragment extends Fragment {
|
||||
|
||||
private static final String[] MEDIA_PROJECTION =
|
||||
{MediaStore.MediaColumns.DATA,
|
||||
MediaStore.MediaColumns.DATE_MODIFIED,
|
||||
MediaStore.MediaColumns.SIZE};
|
||||
|
||||
private static final String[] STAR = {"*"};
|
||||
|
||||
private static final int DB_RETRIES = 3;
|
||||
|
||||
Context mApplicationContext;
|
||||
|
||||
ArrayList<String> mPathNames;
|
||||
TreeSet<File> mFilesToProcess;
|
||||
int mLastGoodProcessedIndex;
|
||||
|
||||
private Handler mHandler = new Handler();
|
||||
|
||||
int mProgressNum;
|
||||
UIStringGenerator mProgressText =
|
||||
new UIStringGenerator(R.string.progress_unstarted_label);
|
||||
UIStringGenerator mDebugMessages = new UIStringGenerator();
|
||||
boolean mStartButtonEnabled;
|
||||
boolean mHasStarted = false;
|
||||
|
||||
ArrayList<File> mDirectoryScanList;
|
||||
|
||||
/**
|
||||
* Callback interface used by the fragment to update the Activity.
|
||||
*/
|
||||
public static interface ScanProgressCallbacks {
|
||||
void updateProgressNum(int progressNum);
|
||||
void updateProgressText(UIStringGenerator progressText);
|
||||
void updateDebugMessages(UIStringGenerator debugMessages);
|
||||
void updatePath(String path);
|
||||
void updateStartButtonEnabled(boolean startButtonEnabled);
|
||||
void signalFinished();
|
||||
}
|
||||
|
||||
private ScanProgressCallbacks mCallbacks;
|
||||
|
||||
private void updateProgressNum(int progressNum) {
|
||||
mProgressNum = progressNum;
|
||||
if (mCallbacks != null) {
|
||||
mCallbacks.updateProgressNum(mProgressNum);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateProgressText(int resId) {
|
||||
updateProgressText(new UIStringGenerator(resId));
|
||||
}
|
||||
|
||||
private void updateProgressText(int resId, String string) {
|
||||
updateProgressText(new UIStringGenerator(resId, string));
|
||||
}
|
||||
|
||||
private void updateProgressText(UIStringGenerator progressText) {
|
||||
mProgressText = progressText;
|
||||
if (mCallbacks != null) {
|
||||
mCallbacks.updateProgressText(mProgressText);
|
||||
}
|
||||
}
|
||||
|
||||
private void addDebugMessage(int resId, String string) {
|
||||
mDebugMessages.addSubGenerator(resId);
|
||||
mDebugMessages.addSubGenerator(string + "\n");
|
||||
if (mCallbacks != null) {
|
||||
mCallbacks.updateDebugMessages(mDebugMessages);
|
||||
}
|
||||
}
|
||||
|
||||
private void addDebugMessage(String debugMessage) {
|
||||
mDebugMessages.addSubGenerator(debugMessage + "\n");
|
||||
if (mCallbacks != null) {
|
||||
mCallbacks.updateDebugMessages(mDebugMessages);
|
||||
}
|
||||
}
|
||||
|
||||
private void resetDebugMessages() {
|
||||
mDebugMessages = new UIStringGenerator();
|
||||
if (mCallbacks != null) {
|
||||
mCallbacks.updateDebugMessages(mDebugMessages);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateStartButtonEnabled(boolean startButtonEnabled) {
|
||||
mStartButtonEnabled = startButtonEnabled;
|
||||
if (mCallbacks != null) {
|
||||
mCallbacks.updateStartButtonEnabled(mStartButtonEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
private void signalFinished() {
|
||||
if (mCallbacks != null) {
|
||||
mCallbacks.signalFinished();
|
||||
}
|
||||
}
|
||||
|
||||
public int getProgressNum() {
|
||||
return mProgressNum;
|
||||
}
|
||||
|
||||
public UIStringGenerator getProgressText() {
|
||||
return mProgressText;
|
||||
}
|
||||
|
||||
public UIStringGenerator getDebugMessages() {
|
||||
return mDebugMessages;
|
||||
}
|
||||
|
||||
public boolean getStartButtonEnabled() {
|
||||
return mStartButtonEnabled;
|
||||
}
|
||||
|
||||
public boolean getHasStarted() {
|
||||
return mHasStarted;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity) {
|
||||
super.onAttach(activity);
|
||||
if(activity instanceof ScanProgressCallbacks) {
|
||||
mCallbacks = (ScanProgressCallbacks) activity;
|
||||
}
|
||||
mApplicationContext = activity.getApplicationContext();
|
||||
}
|
||||
|
||||
public void setScanProgressCallbacks(ScanProgressCallbacks callbacks) {
|
||||
mCallbacks = callbacks;
|
||||
}
|
||||
|
||||
public ScanFragment() {
|
||||
super();
|
||||
|
||||
// Set correct initial values.
|
||||
mProgressNum = 0;
|
||||
mStartButtonEnabled = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Retain this fragment across configuration changes.
|
||||
setRetainInstance(true);
|
||||
}
|
||||
|
||||
// Purely for debugging and not normally used, so does not translate
|
||||
// strings.
|
||||
public void listPathNamesOnDebug() {
|
||||
StringBuffer listString = new StringBuffer();
|
||||
listString.append("\n\nScanning paths:\n");
|
||||
Iterator<String> iterator = mPathNames.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
listString.append(iterator.next() + "\n");
|
||||
}
|
||||
addDebugMessage(listString.toString());
|
||||
}
|
||||
|
||||
public void advanceScanner() {
|
||||
if (mDirectoryScanList != null && mDirectoryScanList.isEmpty() == false) {
|
||||
File nextDir = mDirectoryScanList.remove(0);
|
||||
startScan(nextDir, false);
|
||||
} else {
|
||||
updateProgressNum(0);
|
||||
updateProgressText(R.string.progress_completed_label);
|
||||
updateStartButtonEnabled(true);
|
||||
signalFinished();
|
||||
}
|
||||
}
|
||||
|
||||
public void startMediaScanner(){
|
||||
//listPathNamesOnDebug();
|
||||
if (mPathNames.size() == 0) {
|
||||
advanceScanner();
|
||||
}
|
||||
else {
|
||||
MediaScannerConnection.scanFile(
|
||||
mApplicationContext,
|
||||
mPathNames.toArray(new String[mPathNames.size()]),
|
||||
null,
|
||||
new MediaScannerConnection.OnScanCompletedListener() {
|
||||
public void onScanCompleted(String path, Uri uri) {
|
||||
mHandler.post(new Updater(path));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void startScan(File[] pathList) {
|
||||
mDirectoryScanList = new ArrayList<File>();
|
||||
for (File f : pathList) {
|
||||
if (f.exists() && f.isDirectory())
|
||||
mDirectoryScanList.add(f);
|
||||
}
|
||||
advanceScanner();
|
||||
}
|
||||
|
||||
public void startScan(File path, boolean restrictDbUpdate) {
|
||||
mHasStarted = true;
|
||||
updateStartButtonEnabled(false);
|
||||
updateProgressText(R.string.progress_filelist_label);
|
||||
mFilesToProcess = new TreeSet<File>();
|
||||
resetDebugMessages();
|
||||
if (path.exists()) {
|
||||
this.new PreprocessTask().execute(new ScanParameters(path, restrictDbUpdate));
|
||||
}
|
||||
else {
|
||||
updateProgressText(R.string.progress_error_bad_path_label);
|
||||
updateStartButtonEnabled(true);
|
||||
signalFinished();
|
||||
}
|
||||
}
|
||||
|
||||
static class ProgressUpdate {
|
||||
public enum Type {
|
||||
DATABASE, STATE, DEBUG
|
||||
}
|
||||
|
||||
Type mType;
|
||||
|
||||
public Type getType() {
|
||||
return mType;
|
||||
}
|
||||
|
||||
int mResId;
|
||||
|
||||
public int getResId() {
|
||||
return mResId;
|
||||
}
|
||||
|
||||
String mString;
|
||||
|
||||
public String getString() {
|
||||
return mString;
|
||||
}
|
||||
|
||||
int mProgress;
|
||||
|
||||
public int getProgress() {
|
||||
return mProgress;
|
||||
}
|
||||
|
||||
public ProgressUpdate(Type type, int resId, String string,
|
||||
int progress) {
|
||||
mType = type;
|
||||
mResId = resId;
|
||||
mString = string;
|
||||
mProgress = progress;
|
||||
}
|
||||
}
|
||||
|
||||
static ProgressUpdate debugUpdate(int resId, String string) {
|
||||
return new ProgressUpdate(ProgressUpdate.Type.DEBUG, resId, string, 0);
|
||||
}
|
||||
|
||||
static ProgressUpdate debugUpdate(int resId) {
|
||||
return debugUpdate(resId, "");
|
||||
}
|
||||
|
||||
static ProgressUpdate databaseUpdate(String file, int progress) {
|
||||
return new ProgressUpdate(ProgressUpdate.Type.DATABASE, 0, file,
|
||||
progress);
|
||||
}
|
||||
|
||||
static ProgressUpdate stateUpdate(int resId) {
|
||||
return new ProgressUpdate(ProgressUpdate.Type.STATE, resId, "", 0);
|
||||
}
|
||||
|
||||
static class ScanParameters {
|
||||
File mPath;
|
||||
boolean mRestrictDbUpdate;
|
||||
|
||||
public ScanParameters(File path, boolean restrictDbUpdate) {
|
||||
mPath = path;
|
||||
mRestrictDbUpdate = restrictDbUpdate;
|
||||
}
|
||||
|
||||
public File getPath() {
|
||||
return mPath;
|
||||
}
|
||||
|
||||
public boolean shouldScan(File file, boolean fromDb)
|
||||
throws IOException {
|
||||
// Empty directory check.
|
||||
if (file.isDirectory()) {
|
||||
File[] files = file.listFiles();
|
||||
if (files == null || files.length == 0) {
|
||||
Log.w("SDScanner", "Scan of empty directory " +
|
||||
file.getCanonicalPath() + " skipped to avoid bug.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!mRestrictDbUpdate && fromDb) {
|
||||
return true;
|
||||
}
|
||||
while (file != null) {
|
||||
if (file.equals(mPath)) {
|
||||
return true;
|
||||
}
|
||||
file = file.getParentFile();
|
||||
}
|
||||
// If we fell through here, got up to root without encountering the
|
||||
// path to scan.
|
||||
if (!fromDb) {
|
||||
Log.w("SDScanner", "File " + file.getCanonicalPath() +
|
||||
" outside of scan directory skipped.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class PreprocessTask extends AsyncTask<ScanParameters, ProgressUpdate, Void> {
|
||||
|
||||
private void recursiveAddFiles(File file, ScanParameters scanParameters)
|
||||
throws IOException {
|
||||
if (!scanParameters.shouldScan(file, false)) {
|
||||
// If we got here, there file was either outside the scan
|
||||
// directory, or was an empty directory.
|
||||
return;
|
||||
}
|
||||
if (!mFilesToProcess.add(file)) {
|
||||
// Avoid infinite recursion caused by symlinks.
|
||||
// If mFilesToProcess already contains this file, add() will
|
||||
// return false.
|
||||
return;
|
||||
}
|
||||
if (file.isDirectory()) {
|
||||
boolean nomedia = new File(file, ".nomedia").exists();
|
||||
// Only recurse downward if not blocked by nomedia.
|
||||
if (!nomedia) {
|
||||
File[] files = file.listFiles();
|
||||
if (files != null) {
|
||||
for (File nextFile : files) {
|
||||
recursiveAddFiles(nextFile.getCanonicalFile(),
|
||||
scanParameters);
|
||||
}
|
||||
}
|
||||
else {
|
||||
publishProgress(debugUpdate(
|
||||
R.string.skipping_folder_label,
|
||||
" " + file.getPath()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void dbOneTry(ScanParameters parameters) {
|
||||
Cursor cursor = mApplicationContext.getContentResolver().query(
|
||||
MediaStore.Files.getContentUri("external"),
|
||||
MEDIA_PROJECTION,
|
||||
//STAR,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
int data_column =
|
||||
cursor.getColumnIndex(MediaStore.MediaColumns.DATA);
|
||||
int modified_column =
|
||||
cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED);
|
||||
int size_column =
|
||||
cursor.getColumnIndex(MediaStore.MediaColumns.SIZE);
|
||||
int totalSize = cursor.getCount();
|
||||
int currentItem = 0;
|
||||
int reportFreq = 0;
|
||||
// Used to calibrate reporting frequency
|
||||
long startTime = SystemClock.currentThreadTimeMillis();
|
||||
while (cursor.moveToNext()) {
|
||||
currentItem++;
|
||||
try {
|
||||
File file = new File(cursor.getString(data_column)).getCanonicalFile();
|
||||
// Ignore non-file backed playlists (size == 0). Fixes playlist removal on scan
|
||||
// for 4.1
|
||||
boolean validSize = cursor.getInt(size_column) > 0;
|
||||
if (validSize && (!file.exists() ||
|
||||
file.lastModified() / 1000L >
|
||||
cursor.getLong(modified_column))
|
||||
&& parameters.shouldScan(file, true)) {
|
||||
// Media scanner handles these cases.
|
||||
// Is a set, so OK if already present.
|
||||
mFilesToProcess.add(file);
|
||||
}
|
||||
else {
|
||||
// Don't want to waste time scanning an up-to-date
|
||||
// file.
|
||||
mFilesToProcess.remove(file);
|
||||
}
|
||||
if (reportFreq == 0) {
|
||||
// Calibration phase
|
||||
if (SystemClock.currentThreadTimeMillis() - startTime > 25) {
|
||||
reportFreq = currentItem + 1;
|
||||
}
|
||||
}
|
||||
else if (currentItem % reportFreq == 0) {
|
||||
publishProgress(databaseUpdate(file.getPath(),
|
||||
(100 * currentItem) / totalSize));
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
// Just ignore it for now.
|
||||
}
|
||||
}
|
||||
// Don't need the cursor any more.
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(ScanParameters... parameters) {
|
||||
try {
|
||||
recursiveAddFiles(parameters[0].getPath(), parameters[0]);
|
||||
}
|
||||
catch (IOException Ex) {
|
||||
// Do nothing.
|
||||
}
|
||||
// Parse database
|
||||
publishProgress(stateUpdate(R.string.progress_database_label));
|
||||
boolean dbSuccess = false;
|
||||
int numRetries = 0;
|
||||
while (!dbSuccess && numRetries < DB_RETRIES) {
|
||||
dbSuccess = true;
|
||||
try {
|
||||
dbOneTry(parameters[0]);
|
||||
}
|
||||
catch (Exception Ex) {
|
||||
// For any of these errors, try again.
|
||||
numRetries++;
|
||||
dbSuccess = false;
|
||||
if (numRetries < DB_RETRIES) {
|
||||
publishProgress(stateUpdate(
|
||||
R.string.db_error_retrying));
|
||||
SystemClock.sleep(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (numRetries > 0) {
|
||||
if (dbSuccess) {
|
||||
publishProgress(debugUpdate(R.string.db_error_recovered));
|
||||
}
|
||||
else {
|
||||
publishProgress(debugUpdate(R.string.db_error_failure));
|
||||
}
|
||||
}
|
||||
// Prepare final path list for processing.
|
||||
mPathNames = new ArrayList<String>(mFilesToProcess.size());
|
||||
Iterator<File> iterator = mFilesToProcess.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
mPathNames.add(iterator.next().getPath());
|
||||
}
|
||||
mLastGoodProcessedIndex = -1;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onProgressUpdate(ProgressUpdate... progress) {
|
||||
switch (progress[0].getType()) {
|
||||
case DATABASE:
|
||||
updateProgressText(R.string.database_proc,
|
||||
" " + progress[0].getString());
|
||||
updateProgressNum(progress[0].getProgress());
|
||||
break;
|
||||
case STATE:
|
||||
updateProgressText(progress[0].getResId());
|
||||
updateProgressNum(0);
|
||||
break;
|
||||
case DEBUG:
|
||||
addDebugMessage(progress[0].getResId(), progress[0].getString());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void result) {
|
||||
startMediaScanner();
|
||||
}
|
||||
}
|
||||
|
||||
class Updater implements Runnable {
|
||||
String mPathScanned;
|
||||
|
||||
public Updater(String path) {
|
||||
mPathScanned = path;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
if (mLastGoodProcessedIndex + 1 < mPathNames.size() &&
|
||||
mPathNames.get(mLastGoodProcessedIndex
|
||||
+ 1).equals(mPathScanned)) {
|
||||
mLastGoodProcessedIndex++;
|
||||
}
|
||||
else {
|
||||
int newIndex = mPathNames.indexOf(mPathScanned);
|
||||
if (newIndex > -1) {
|
||||
mLastGoodProcessedIndex = newIndex;
|
||||
}
|
||||
}
|
||||
int progress = (100 * (mLastGoodProcessedIndex + 1))
|
||||
/ mPathNames.size();
|
||||
if (progress == 100) {
|
||||
advanceScanner();
|
||||
}
|
||||
else {
|
||||
updateProgressNum(progress);
|
||||
updateProgressText(R.string.final_proc, " " + mPathScanned);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
/* SD Scanner - A manual implementation of the SD rescan process, compatible
|
||||
* with Android 4.4
|
||||
*
|
||||
* Copyright (C) 2013-2014 Jeremy Erickson
|
||||
*
|
||||
* 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 2 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.
|
||||
*/
|
||||
|
||||
package com.gmail.jerickson314.sdscanner;
|
||||
|
||||
import android.app.Activity;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
|
||||
public class UIStringGenerator {
|
||||
private static interface SubGenerator {
|
||||
public String toString(Activity activity);
|
||||
}
|
||||
|
||||
private static class ResourceSubGenerator implements SubGenerator {
|
||||
int mResId;
|
||||
|
||||
public ResourceSubGenerator(int resId) {
|
||||
mResId = resId;
|
||||
}
|
||||
|
||||
public String toString(Activity activity) {
|
||||
return activity.getString(mResId);
|
||||
}
|
||||
}
|
||||
|
||||
private static class StringSubGenerator implements SubGenerator {
|
||||
String mString;
|
||||
|
||||
public StringSubGenerator(String string) {
|
||||
mString = string;
|
||||
}
|
||||
|
||||
public String toString(Activity activity) {
|
||||
return mString;
|
||||
}
|
||||
}
|
||||
|
||||
ArrayList<SubGenerator> mSubGenerators = new ArrayList<SubGenerator>();
|
||||
|
||||
public void addSubGenerator(int resId) {
|
||||
mSubGenerators.add(new ResourceSubGenerator(resId));
|
||||
}
|
||||
|
||||
public void addSubGenerator(String string) {
|
||||
mSubGenerators.add(new StringSubGenerator(string));
|
||||
}
|
||||
|
||||
public UIStringGenerator(int resId) {
|
||||
addSubGenerator(resId);
|
||||
}
|
||||
|
||||
public UIStringGenerator(int resId, String string) {
|
||||
addSubGenerator(resId);
|
||||
addSubGenerator(string);
|
||||
}
|
||||
|
||||
public UIStringGenerator(String string) {
|
||||
addSubGenerator(string);
|
||||
}
|
||||
|
||||
public UIStringGenerator() {
|
||||
// Doing nothing results in empty string.
|
||||
}
|
||||
|
||||
public String toString(Activity activity) {
|
||||
StringBuilder toReturn = new StringBuilder();
|
||||
Iterator<SubGenerator> iterator = mSubGenerators.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
toReturn.append(iterator.next().toString(activity));
|
||||
}
|
||||
return toReturn.toString();
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user