diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 6cbd88b1..d1473edd 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -152,6 +152,8 @@ THE SOFTWARE. android:name="TabOrderActivity" /> + diff --git a/orig/file_document.svgz b/orig/file_document.svgz new file mode 100644 index 00000000..c31d7490 Binary files /dev/null and b/orig/file_document.svgz differ diff --git a/orig/file_image.svgz b/orig/file_image.svgz new file mode 100644 index 00000000..0622ea83 Binary files /dev/null and b/orig/file_image.svgz differ diff --git a/orig/file_music.svgz b/orig/file_music.svgz new file mode 100644 index 00000000..1686008d Binary files /dev/null and b/orig/file_music.svgz differ diff --git a/orig/folder.svgz b/orig/folder.svgz index 5622bf26..8a7b8d42 100644 Binary files a/orig/folder.svgz and b/orig/folder.svgz differ diff --git a/res/drawable-hdpi/file_document.png b/res/drawable-hdpi/file_document.png new file mode 100644 index 00000000..a74b647a Binary files /dev/null and b/res/drawable-hdpi/file_document.png differ diff --git a/res/drawable-hdpi/file_image.png b/res/drawable-hdpi/file_image.png new file mode 100644 index 00000000..1f2b0408 Binary files /dev/null and b/res/drawable-hdpi/file_image.png differ diff --git a/res/drawable-hdpi/file_music.png b/res/drawable-hdpi/file_music.png new file mode 100644 index 00000000..47f77bd7 Binary files /dev/null and b/res/drawable-hdpi/file_music.png differ diff --git a/res/drawable-hdpi/folder.png b/res/drawable-hdpi/folder.png index 77d7e869..b2034e80 100644 Binary files a/res/drawable-hdpi/folder.png and b/res/drawable-hdpi/folder.png differ diff --git a/res/drawable-mdpi/file_document.png b/res/drawable-mdpi/file_document.png new file mode 100644 index 00000000..b055c54d Binary files /dev/null and b/res/drawable-mdpi/file_document.png differ diff --git a/res/drawable-mdpi/file_image.png b/res/drawable-mdpi/file_image.png new file mode 100644 index 00000000..a37bb03c Binary files /dev/null and b/res/drawable-mdpi/file_image.png differ diff --git a/res/drawable-mdpi/file_music.png b/res/drawable-mdpi/file_music.png new file mode 100644 index 00000000..72201ddf Binary files /dev/null and b/res/drawable-mdpi/file_music.png differ diff --git a/res/drawable-mdpi/folder.png b/res/drawable-mdpi/folder.png index 137b88cf..629ed014 100644 Binary files a/res/drawable-mdpi/folder.png and b/res/drawable-mdpi/folder.png differ diff --git a/res/drawable-xhdpi/file_document.png b/res/drawable-xhdpi/file_document.png new file mode 100644 index 00000000..37e62102 Binary files /dev/null and b/res/drawable-xhdpi/file_document.png differ diff --git a/res/drawable-xhdpi/file_image.png b/res/drawable-xhdpi/file_image.png new file mode 100644 index 00000000..d479164b Binary files /dev/null and b/res/drawable-xhdpi/file_image.png differ diff --git a/res/drawable-xhdpi/file_music.png b/res/drawable-xhdpi/file_music.png new file mode 100644 index 00000000..4f018f99 Binary files /dev/null and b/res/drawable-xhdpi/file_music.png differ diff --git a/res/drawable-xhdpi/folder.png b/res/drawable-xhdpi/folder.png index 669b0ea5..ac5633cb 100644 Binary files a/res/drawable-xhdpi/folder.png and b/res/drawable-xhdpi/folder.png differ diff --git a/res/drawable-xxhdpi/file_document.png b/res/drawable-xxhdpi/file_document.png new file mode 100644 index 00000000..3174bb61 Binary files /dev/null and b/res/drawable-xxhdpi/file_document.png differ diff --git a/res/drawable-xxhdpi/file_image.png b/res/drawable-xxhdpi/file_image.png new file mode 100644 index 00000000..95d22fa4 Binary files /dev/null and b/res/drawable-xxhdpi/file_image.png differ diff --git a/res/drawable-xxhdpi/file_music.png b/res/drawable-xxhdpi/file_music.png new file mode 100644 index 00000000..7ce874fa Binary files /dev/null and b/res/drawable-xxhdpi/file_music.png differ diff --git a/res/drawable-xxhdpi/folder.png b/res/drawable-xxhdpi/folder.png index 38676410..fb369492 100644 Binary files a/res/drawable-xxhdpi/folder.png and b/res/drawable-xxhdpi/folder.png differ diff --git a/res/layout/filebrowser_content.xml b/res/layout/folderpicker_content.xml similarity index 96% rename from res/layout/filebrowser_content.xml rename to res/layout/folderpicker_content.xml index 970d2dab..7bf3ae09 100644 --- a/res/layout/filebrowser_content.xml +++ b/res/layout/folderpicker_content.xml @@ -44,7 +44,7 @@ THE SOFTWARE. android:layout_height="wrap_content" android:layout_width="0dp" android:layout_weight="1" - android:text="@string/select" /> + android:text="@string/empty" /> @@ -61,7 +61,7 @@ THE SOFTWARE. android:layout_height="0px" android:layout_width="fill_parent" android:layout_weight="1" - android:choiceMode="multipleChoice" + android:choiceMode="none" dslv:drag_enabled="false" /> diff --git a/res/layout/medialibrary_preferences.xml b/res/layout/medialibrary_preferences.xml index f6c47df9..664843f0 100644 --- a/res/layout/medialibrary_preferences.xml +++ b/res/layout/medialibrary_preferences.xml @@ -39,6 +39,25 @@ along with this program. If not, see . android:layout_height="wrap_content" android:text="@string/media_scan_force_bastp" /> + + + + + + Filebrowser home Filebrowser starts at this directory Select + Save Automatic playlist creation @@ -319,6 +320,7 @@ THE SOFTWARE. No receiving apps found for this media type! Media library + Indexed directories Media scanner Scan full filesystem (Slow) Flush media database (Dangerzone!) @@ -334,6 +336,11 @@ THE SOFTWARE. Scanner options changed Changing this option will start a full rescan of your library. Would you like to continue? + + Long press to modify folder options + Include + Exclude + Neutral Plugins diff --git a/src/ch/blinkenlights/android/medialibrary/MediaLibrary.java b/src/ch/blinkenlights/android/medialibrary/MediaLibrary.java index d6c7163c..53fd05be 100644 --- a/src/ch/blinkenlights/android/medialibrary/MediaLibrary.java +++ b/src/ch/blinkenlights/android/medialibrary/MediaLibrary.java @@ -23,8 +23,14 @@ import android.database.Cursor; import android.database.ContentObserver; import android.provider.MediaStore; import android.os.Environment; +import android.util.Log; import java.util.ArrayList; + +import java.io.ObjectOutputStream; +import java.io.ObjectInputStream; +import java.io.Serializable; + import java.io.File; public class MediaLibrary { @@ -37,7 +43,6 @@ public class MediaLibrary { 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 TABLE_PREFERENCES = "preferences"; public static final String VIEW_ARTISTS = "_artists"; public static final String VIEW_ALBUMARTISTS = "_albumartists"; public static final String VIEW_COMPOSERS = "_composers"; @@ -50,17 +55,16 @@ public class MediaLibrary { public static final int ROLE_COMPOSER = 1; public static final int ROLE_ALBUMARTIST = 2; - private static final String PREF_KEY_FORCE_BASTP = "force_bastp"; - private static final String PREF_KEY_GROUP_ALBUMS = "group_albums"; - private static final String PREF_KEY_NATIVE_LIBRARY_COUNT = "native_audio_db_count"; - private static final String PREF_KEY_NATIVE_LAST_MTIME = "native_last_mtime"; + public static final String PREFERENCES_FILE = "_prefs-v1.obj"; /** * Options used by the MediaScanner class */ - public static class Preferences { + public static class Preferences implements Serializable { public boolean forceBastp; public boolean groupAlbumsByFolder; + public ArrayList mediaFolders; + public ArrayList blacklistedFolders; int _nativeLibraryCount; int _nativeLastMtime; } @@ -121,17 +125,63 @@ public class MediaLibrary { public static MediaLibrary.Preferences getPreferences(Context context) { MediaLibrary.Preferences prefs = sPreferences; if (prefs == null) { - MediaLibraryBackend backend = getBackend(context); - prefs = new MediaLibrary.Preferences(); - prefs.forceBastp = backend.getSetPreference(PREF_KEY_FORCE_BASTP, -1) != 0; - prefs.groupAlbumsByFolder = backend.getSetPreference(PREF_KEY_GROUP_ALBUMS, -1) != 0; - prefs._nativeLibraryCount = backend.getSetPreference(PREF_KEY_NATIVE_LIBRARY_COUNT, -1); - prefs._nativeLastMtime = backend.getSetPreference(PREF_KEY_NATIVE_LAST_MTIME, -1); + try (ObjectInputStream ois = new ObjectInputStream(context.openFileInput(PREFERENCES_FILE))) { + prefs = (MediaLibrary.Preferences)ois.readObject(); + } catch (Exception e) { + Log.w("VanillaMusic", "Returning default media-library preferences due to error: "+ e); + } + + if (prefs == null) + prefs = new MediaLibrary.Preferences(); + + if (prefs.mediaFolders == null || prefs.mediaFolders.size() == 0) + prefs.mediaFolders = discoverDefaultMediaPaths(); + + if (prefs.blacklistedFolders == null) // we allow this to be empty, but it must not be null. + prefs.blacklistedFolders = discoverDefaultBlacklistedPaths(); + sPreferences = prefs; // cached for frequent access } return prefs; } + /** + * Returns the guessed media paths for this device + * + * @return array with guessed directories + */ + private static ArrayList discoverDefaultMediaPaths() { + ArrayList defaultPaths = new ArrayList<>(); + + // this should always exist + defaultPaths.add(Environment.getExternalStorageDirectory().getAbsolutePath()); + + // this *may* exist + File sdCard = new File("/storage/sdcard1"); + if (sdCard.isDirectory()) + defaultPaths.add(sdCard.getAbsolutePath()); + return defaultPaths; + } + + /** + * Returns default paths which should be blacklisted + * + * @return array with guessed blacklist + */ + private static ArrayList discoverDefaultBlacklistedPaths() { + final String[] defaultBlacklistPostfix = { "Android/data", "Alarms", "Notifications", "Ringtones" }; + ArrayList defaultPaths = new ArrayList<>(); + + for (String path : discoverDefaultMediaPaths()) { + for (int i = 0; i < defaultBlacklistPostfix.length; i++) { + File guess = new File(path + "/" + defaultBlacklistPostfix[i]); + if (guess.isDirectory()) + defaultPaths.add(guess.getAbsolutePath()); + } + } + return defaultPaths; + } + /** * Updates the scanner preferences * @@ -141,11 +191,14 @@ public class MediaLibrary { */ public static void setPreferences(Context context, MediaLibrary.Preferences prefs) { MediaLibraryBackend backend = getBackend(context); - backend.getSetPreference(PREF_KEY_FORCE_BASTP, prefs.forceBastp ? 1 : 0); - backend.getSetPreference(PREF_KEY_GROUP_ALBUMS, prefs.groupAlbumsByFolder ? 1 : 0); - backend.getSetPreference(PREF_KEY_NATIVE_LIBRARY_COUNT, prefs._nativeLibraryCount); - backend.getSetPreference(PREF_KEY_NATIVE_LAST_MTIME, prefs._nativeLastMtime); - sPreferences = null; + + try (ObjectOutputStream oos = new ObjectOutputStream(context.openFileOutput(PREFERENCES_FILE, 0))) { + oos.writeObject(prefs); + } catch (Exception e) { + Log.w("VanillaMusic", "Failed to store media preferences: " + e); + } + + sPreferences = prefs; } /** @@ -456,25 +509,6 @@ public class MediaLibrary { return (hash < 0 ? hash*-1 : hash); } - /** - * Returns the guessed media paths for this device - * - * @return array with guessed directories - */ - public static File[] discoverMediaPaths() { - ArrayList 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 { /** diff --git a/src/ch/blinkenlights/android/medialibrary/MediaLibraryBackend.java b/src/ch/blinkenlights/android/medialibrary/MediaLibraryBackend.java index a6998d02..ae97a118 100644 --- a/src/ch/blinkenlights/android/medialibrary/MediaLibraryBackend.java +++ b/src/ch/blinkenlights/android/medialibrary/MediaLibraryBackend.java @@ -35,7 +35,7 @@ public class MediaLibraryBackend extends SQLiteOpenHelper { /** * The database version we are using */ - private static final int DATABASE_VERSION = 20170217; + private static final int DATABASE_VERSION = 20170407; /** * on-disk file to store the database */ @@ -104,33 +104,6 @@ public class MediaLibraryBackend extends SQLiteOpenHelper { return mtime; } - /** - * Simple interface to set and get preference values - * - * @param stringKey the key to use - * @param newVal the value to set - * - * Note: The new value will only be set if it is >= 0 - * Lookup failures will return 0 - */ - int getSetPreference(String stringKey, int newVal) { - int oldVal = 0; // this is returned if we found nothing - int key = Math.abs(stringKey.hashCode()); - SQLiteDatabase dbh = getWritableDatabase(); - - Cursor cursor = dbh.query(MediaLibrary.TABLE_PREFERENCES, new String[] { MediaLibrary.PreferenceColumns.VALUE }, MediaLibrary.PreferenceColumns.KEY+"="+key, null, null, null, null, null); - if (cursor.moveToFirst()) { - oldVal = cursor.getInt(0); - } - cursor.close(); - - if (newVal >= 0 && newVal != oldVal) { - dbh.execSQL("INSERT OR REPLACE INTO "+MediaLibrary.TABLE_PREFERENCES+" ("+MediaLibrary.PreferenceColumns.KEY+", "+MediaLibrary.PreferenceColumns.VALUE+") " - +" VALUES("+key+", "+newVal+")"); - } - return oldVal; - } - /** * Wrapper for SQLiteDatabse.delete() function * diff --git a/src/ch/blinkenlights/android/medialibrary/MediaScanner.java b/src/ch/blinkenlights/android/medialibrary/MediaScanner.java index e8cc3781..72ce47bd 100644 --- a/src/ch/blinkenlights/android/medialibrary/MediaScanner.java +++ b/src/ch/blinkenlights/android/medialibrary/MediaScanner.java @@ -108,8 +108,9 @@ public class MediaScanner implements Handler.Callback { * 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); + MediaLibrary.Preferences prefs = MediaLibrary.getPreferences(mContext); + for (String path : prefs.mediaFolders) { + mScanPlan.addNextStep(RPC_READ_DIR, new File(path)); } mScanPlan.addNextStep(RPC_LIBRARY_VRFY, null); mScanPlan.addNextStep(RPC_NATIVE_VRFY, null); @@ -559,7 +560,6 @@ public class MediaScanner implements Handler.Callback { } private static final Pattern sIgnoredFilenames = Pattern.compile("^([^\\.]+|.+\\.(jpe?g|gif|png|bmp|webm|txt|pdf|avi|mp4|mkv|zip|tgz|xml))$", Pattern.CASE_INSENSITIVE); - private static final Pattern sIgnoredDirectories = Pattern.compile("^.+/(Android/data|Alarms|Notifications|Ringtones)/.+$", Pattern.CASE_INSENSITIVE); /** * Returns true if the file should not be scanned * @@ -567,8 +567,30 @@ public class MediaScanner implements Handler.Callback { * @return boolean */ private boolean isBlacklisted(File file) { - boolean blacklisted = sIgnoredFilenames.matcher(file.getName()).matches() || sIgnoredDirectories.matcher(file.getPath()).matches(); - return blacklisted; + if (sIgnoredFilenames.matcher(file.getName()).matches()) + return true; + + int wlPoints = -1; + int blPoints = -1; + + for (String path : MediaLibrary.getPreferences(mContext).mediaFolders) { + if (path.length() > wlPoints && + file.getPath().startsWith(path)) { + wlPoints = path.length(); + } + } + + for (String path : MediaLibrary.getPreferences(mContext).blacklistedFolders) { + if (path.length() > blPoints && + file.getPath().startsWith(path)) { + blPoints = path.length(); + } + } + + // Consider a file to be blacklisted if it is not + // present in any whitelisted dir OR if we found + // a blacklist entry with a longer prefix. + return (wlPoints < 0 || blPoints > wlPoints); } diff --git a/src/ch/blinkenlights/android/medialibrary/MediaSchema.java b/src/ch/blinkenlights/android/medialibrary/MediaSchema.java index 91d6ed8f..934f45bf 100644 --- a/src/ch/blinkenlights/android/medialibrary/MediaSchema.java +++ b/src/ch/blinkenlights/android/medialibrary/MediaSchema.java @@ -116,13 +116,6 @@ public class MediaSchema { + MediaLibrary.PlaylistSongColumns.POSITION +" INTEGER NOT NULL " + ");"; - /** - * SQL schema for our preferences - */ - private static final String DATABASE_CREATE_PREFERENCES = "CREATE TABLE "+ MediaLibrary.TABLE_PREFERENCES + " (" - + MediaLibrary.PreferenceColumns.KEY +" INTEGER PRIMARY KEY, " - + MediaLibrary.PreferenceColumns.VALUE +" INTEGER NOT NULL " - + ");"; /** * Index to select a playlist quickly */ @@ -263,7 +256,6 @@ public class MediaSchema { dbh.execSQL(VIEW_CREATE_ALBUMARTISTS); dbh.execSQL(VIEW_CREATE_COMPOSERS); dbh.execSQL(VIEW_CREATE_PLAYLIST_SONGS); - dbh.execSQL(DATABASE_CREATE_PREFERENCES); } /** @@ -286,11 +278,6 @@ public class MediaSchema { dbh.execSQL("UPDATE songs SET disc_num=1 WHERE disc_num IS null"); } - if (oldVersion < 20170120) { - dbh.execSQL(DATABASE_CREATE_PREFERENCES); - triggerFullMediaScan(dbh); - } - if (oldVersion < 20170211) { // older versions of triggerFullMediaScan did this by mistake dbh.execSQL("UPDATE songs SET mtime=1 WHERE mtime=0"); @@ -302,18 +289,10 @@ public class MediaSchema { dbh.execSQL(VIEW_CREATE_SONGS_ALBUMS_ARTISTS_HUGE); } - } + if (oldVersion >= 20170120 && oldVersion < 20170407) { + dbh.execSQL("DROP TABLE preferences"); + } - /** - * Changes the mtime of all songs and flushes the scanner progress / preferences - * This triggers a full rebuild of the library on startup - * - * @param dbh the writeable dbh to use - */ - private static void triggerFullMediaScan(SQLiteDatabase dbh) { - dbh.execSQL("UPDATE "+MediaLibrary.TABLE_SONGS+" SET "+MediaLibrary.SongColumns.MTIME+"=1"); - // wipes non-bools only - not nice but good enough for now - dbh.execSQL("DELETE FROM "+MediaLibrary.TABLE_PREFERENCES+" WHERE "+MediaLibrary.PreferenceColumns.VALUE+" > 1"); } } diff --git a/src/ch/blinkenlights/android/vanilla/FileSystemAdapter.java b/src/ch/blinkenlights/android/vanilla/FileSystemAdapter.java index 426b14bc..e0ce49ac 100644 --- a/src/ch/blinkenlights/android/vanilla/FileSystemAdapter.java +++ b/src/ch/blinkenlights/android/vanilla/FileSystemAdapter.java @@ -48,6 +48,8 @@ public class FileSystemAdapter { private static final Pattern SPACE_SPLIT = Pattern.compile("\\s+"); private static final Pattern FILE_SEPARATOR = Pattern.compile(File.separator); + private static final Pattern GUESS_MUSIC = Pattern.compile("^([^\\.]+|.+\\.(mp3|ogg|mka|opus|flac|aac|m4a|wav))$", Pattern.CASE_INSENSITIVE); + private static final Pattern GUESS_IMAGE = Pattern.compile("^([^\\.]+|.+\\.(gif|jpe?g|png|bmp|tiff?))$", Pattern.CASE_INSENSITIVE); /** * Sort by filename. @@ -243,19 +245,17 @@ public class FileSystemAdapter holder = new ViewHolder(); row.setTag(holder); - row.getCoverView().setImageDrawable(mFolderIcon); } else { row = (DraggableRow)convertView; holder = (ViewHolder)row.getTag(); } - File file = mFiles[pos]; - boolean isDirectory = file.isDirectory(); holder.id = pos; + final File file = mFiles[pos]; row.getTextView().setText(file.getName()); - row.getCoverView().setVisibility(isDirectory ? View.VISIBLE : View.GONE); - row.showDragger(isDirectory); + row.showDragger(file.isDirectory()); + row.getCoverView().setImageDrawable(getDrawableForFile(file)); return row; } @@ -283,6 +283,25 @@ public class FileSystemAdapter return mLimiter; } + /** + * Returns a drawable for given file. + * This function is rather fast as the file type is guessed + * based on the extension. + * + * @return drawable for the guessed mime type + */ + private Drawable getDrawableForFile(File file) { + int res = R.drawable.file_document; + if (file.isDirectory()) { + res = R.drawable.folder; + } else if (GUESS_MUSIC.matcher(file.getName()).matches()) { + res = R.drawable.file_music; + } else if (GUESS_IMAGE.matcher(file.getName()).matches()) { + res = R.drawable.file_image; + } + return mActivity.getResources().getDrawable(res); + } + /** * Returns the unixpath represented by this limiter * diff --git a/src/ch/blinkenlights/android/vanilla/FilebrowserStartActivity.java b/src/ch/blinkenlights/android/vanilla/FilebrowserStartActivity.java index fa0c3b31..4dba973d 100644 --- a/src/ch/blinkenlights/android/vanilla/FilebrowserStartActivity.java +++ b/src/ch/blinkenlights/android/vanilla/FilebrowserStartActivity.java @@ -22,6 +22,7 @@ import android.os.Bundle; import android.content.SharedPreferences; import java.io.File; +import java.util.ArrayList; public class FilebrowserStartActivity extends FolderPickerActivity { @@ -30,6 +31,8 @@ public class FilebrowserStartActivity extends FolderPickerActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + setTitle(R.string.filebrowser_start); + mPrefEditor = PlaybackService.getSettings(this).edit(); // Make sure that we display the current selection @@ -39,7 +42,7 @@ public class FilebrowserStartActivity extends FolderPickerActivity { @Override - public void onFolderSelected(File directory) { + public void onFolderPicked(File directory, ArrayList a, ArrayList b) { mPrefEditor.putString(PrefKeys.FILESYSTEM_BROWSE_START, directory.getAbsolutePath()); mPrefEditor.commit(); finish(); diff --git a/src/ch/blinkenlights/android/vanilla/FolderPickerActivity.java b/src/ch/blinkenlights/android/vanilla/FolderPickerActivity.java index 7fe470a4..9a65b236 100644 --- a/src/ch/blinkenlights/android/vanilla/FolderPickerActivity.java +++ b/src/ch/blinkenlights/android/vanilla/FolderPickerActivity.java @@ -18,8 +18,11 @@ package ch.blinkenlights.android.vanilla; import java.util.Arrays; +import java.util.ArrayList; import java.io.File; import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; import android.os.Bundle; import android.view.View; import android.view.MenuItem; @@ -33,7 +36,8 @@ import android.widget.Toast; import com.mobeta.android.dslv.DragSortListView; public abstract class FolderPickerActivity extends Activity - implements AdapterView.OnItemClickListener + implements AdapterView.OnItemClickListener, + AdapterView.OnItemLongClickListener { /** @@ -56,14 +60,26 @@ public abstract class FolderPickerActivity extends Activity * The array adapter of our listview */ private FolderPickerAdapter mListAdapter; + /** + * True if folder-tri-state selection mode + * is enabled + */ + private boolean mTritastic; + /** + * List of included dirs in tristate mode + */ + private ArrayList mIncludedDirs; + /** + * List of excluded dirs in tristate mode + */ + private ArrayList mExcludedDirs; @Override public void onCreate(Bundle savedInstanceState) { ThemeHelper.setTheme(this, R.style.BackActionBar); super.onCreate(savedInstanceState); - setTitle(R.string.filebrowser_start); - setContentView(R.layout.filebrowser_content); + setContentView(R.layout.folderpicker_content); mCurrentPath = new File("/"); mListAdapter = new FolderPickerAdapter(this, 0); @@ -76,16 +92,39 @@ public abstract class FolderPickerActivity extends Activity mSaveButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { - onFolderSelected(mCurrentPath); + onFolderPicked(mCurrentPath, mIncludedDirs, mExcludedDirs); }}); + // init defaults + enableTritasticSelect(false, null, null); } /** * Called after a folder was selected * * @param directory the selected directory + * @param included unique list of included directories in tristastic mode + * @param excluded unique list of excluded directories in triatastic mode */ - public abstract void onFolderSelected(File directory); + public abstract void onFolderPicked(File directory, ArrayList included, ArrayList excluded); + + /** + * Enables tritastic selection, that is: user can select each + * directory to be in included, excluded or neutral state. + * + * @param enabled enables or disables this feature + * @param included initial list of included dirs + * @param excluded initial list of excluded dirs + */ + public void enableTritasticSelect(boolean enabled, ArrayList included, ArrayList excluded) { + mTritastic = enabled; + mIncludedDirs = (enabled ? included : null); + mExcludedDirs = (enabled ? excluded : null); + mListView.setOnItemLongClickListener(enabled ? this : null); + mSaveButton.setText(enabled ? R.string.save : R.string.select); + + if (enabled) + Toast.makeText(this, R.string.hint_long_press_to_modify_folder, Toast.LENGTH_SHORT).show(); + } /** * Jumps to given directory @@ -94,7 +133,7 @@ public abstract class FolderPickerActivity extends Activity */ void setCurrentDirectory(File directory) { mCurrentPath = directory; - refreshDirectoryList(); + refreshDirectoryList(true); } /** @@ -104,7 +143,7 @@ public abstract class FolderPickerActivity extends Activity @Override public void onResume() { super.onResume(); - refreshDirectoryList(); + refreshDirectoryList(false); } /** @@ -131,35 +170,83 @@ public abstract class FolderPickerActivity extends Activity */ @Override public void onItemClick(AdapterView parent, View view, int pos, long id) { - String dirent = mListAdapter.getItem(pos); + FolderPickerAdapter.Item item = mListAdapter.getItem(pos); File newPath = null; if(pos == 0) { newPath = mCurrentPath.getParentFile(); } else { - newPath = new File(mCurrentPath, dirent); + newPath = new File(mCurrentPath, item.name); } if (newPath != null) setCurrentDirectory(newPath); } + /** + * Called on long-click on a row + */ + @Override + public boolean onItemLongClick(AdapterView parent, View view, int pos, long id) { + FolderPickerAdapter.Item item = mListAdapter.getItem(pos); + + if (item.file == null) + return false; + + final String path = item.file.getAbsolutePath(); + final CharSequence[] options = new CharSequence[]{ + getResources().getString(R.string.folder_include), + getResources().getString(R.string.folder_exclude), + getResources().getString(R.string.folder_neutral) + }; + + AlertDialog.Builder builder = new AlertDialog.Builder(this) + .setTitle(item.name) + .setItems(options, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + mIncludedDirs.remove(path); + mExcludedDirs.remove(path); + switch (which) { + case 0: + mIncludedDirs.add(path); + break; + case 1: + mExcludedDirs.add(path); + break; + default: + } + refreshDirectoryList(false); + } + }); + builder.create().show(); + return true; + } + /** * display mCurrentPath in the dialog */ - private void refreshDirectoryList() { + private void refreshDirectoryList(boolean scroll) { File path = mCurrentPath; File[]dirs = path.listFiles(); mListAdapter.clear(); - mListAdapter.add("../"); + mListAdapter.add(new FolderPickerAdapter.Item("../", null, 0)); if(dirs != null) { Arrays.sort(dirs); for(File fentry: dirs) { if(fentry.isDirectory()) { - mListAdapter.add(fentry.getName()); + int color = 0; + if (mTritastic) { + if (mIncludedDirs.contains(fentry.getAbsolutePath())) + color = 0xff00c853; + if (mExcludedDirs.contains(fentry.getAbsolutePath())) + color = 0xffd50000; + } + FolderPickerAdapter.Item item = new FolderPickerAdapter.Item(fentry.getName(), fentry, color); + mListAdapter.add(item); } } } @@ -167,7 +254,8 @@ public abstract class FolderPickerActivity extends Activity Toast.makeText(this, "Failed to display " + path.getAbsolutePath(), Toast.LENGTH_SHORT).show(); } mPathDisplay.setText(path.getAbsolutePath()); - mListView.setSelectionFromTop(0, 0); + if (scroll) + mListView.setSelectionFromTop(0, 0); } } diff --git a/src/ch/blinkenlights/android/vanilla/FolderPickerAdapter.java b/src/ch/blinkenlights/android/vanilla/FolderPickerAdapter.java index 9998d2f5..2144d5f8 100644 --- a/src/ch/blinkenlights/android/vanilla/FolderPickerAdapter.java +++ b/src/ch/blinkenlights/android/vanilla/FolderPickerAdapter.java @@ -29,10 +29,23 @@ import android.widget.TextView; import android.widget.ArrayAdapter; import android.graphics.drawable.Drawable; +import java.io.File; + public class FolderPickerAdapter - extends ArrayAdapter + extends ArrayAdapter { - + + public static class Item { + String name; + File file; + int color; + public Item(String name, File file, int color) { + this.name = name; + this.file = file; + this.color = color; + } + } + private final LayoutInflater mInflater; public FolderPickerAdapter(Context context, int resource) { @@ -47,15 +60,15 @@ public class FolderPickerAdapter if (convertView == null) { row = (DraggableRow)mInflater.inflate(R.layout.draggable_row, parent, false); row.setupLayout(DraggableRow.LAYOUT_LISTVIEW); - row.getCoverView().setImageResource(R.drawable.folder); } else { row = (DraggableRow)convertView; } - String label = getItem(pos); - row.getTextView().setText(label); + Item item = (Item)getItem(pos); + row.getTextView().setText(item.name); + row.getCoverView().setColorFilter(item.color); return row; } diff --git a/src/ch/blinkenlights/android/vanilla/MediaFoldersSelectionActivity.java b/src/ch/blinkenlights/android/vanilla/MediaFoldersSelectionActivity.java new file mode 100644 index 00000000..2e49df37 --- /dev/null +++ b/src/ch/blinkenlights/android/vanilla/MediaFoldersSelectionActivity.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2017 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 + * 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 . + */ + +package ch.blinkenlights.android.vanilla; + +import ch.blinkenlights.android.medialibrary.MediaLibrary; + +import android.app.Activity; +import android.os.Bundle; +import android.content.SharedPreferences; + +import java.io.File; +import java.util.ArrayList; + +public class MediaFoldersSelectionActivity extends FolderPickerActivity { + + private SharedPreferences.Editor mPrefEditor; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setTitle(R.string.media_folders_header); + + MediaLibrary.Preferences prefs = MediaLibrary.getPreferences(this); + File startPath = FileUtils.getFilesystemBrowseStart(this); + + // Make sure that we display the current selection + setCurrentDirectory(startPath); + enableTritasticSelect(true, prefs.mediaFolders, prefs.blacklistedFolders); + } + + + @Override + public void onFolderPicked(File directory, ArrayList included, ArrayList excluded) { + MediaLibrary.Preferences prefs = MediaLibrary.getPreferences(this); + prefs.mediaFolders = included; + prefs.blacklistedFolders = excluded; + MediaLibrary.setPreferences(this, prefs); + finish(); + } + +} diff --git a/src/ch/blinkenlights/android/vanilla/PreferencesMediaLibrary.java b/src/ch/blinkenlights/android/vanilla/PreferencesMediaLibrary.java index 0b125c10..a87656fb 100644 --- a/src/ch/blinkenlights/android/vanilla/PreferencesMediaLibrary.java +++ b/src/ch/blinkenlights/android/vanilla/PreferencesMediaLibrary.java @@ -23,6 +23,7 @@ import android.app.AlertDialog; import android.app.Fragment; import android.content.Context; import android.content.DialogInterface; +import android.content.Intent; import android.database.Cursor; import android.os.Bundle; import android.view.LayoutInflater; @@ -49,6 +50,10 @@ public class PreferencesMediaLibrary extends Fragment implements View.OnClickLis * The cancel button */ private View mCancelButton; + /** + * The edit-media-folders button + */ + private View mEditButton; /** * The debug / progress text describing the scan status */ @@ -65,6 +70,10 @@ public class PreferencesMediaLibrary extends Fragment implements View.OnClickLis * The number of hours of music we have */ private TextView mStatsPlaytime; + /** + * A list of scanned media directories + */ + private TextView mMediaDirectories; /** * Checkbox for full scan */ @@ -85,6 +94,10 @@ public class PreferencesMediaLibrary extends Fragment implements View.OnClickLis * Set if we should start a full scan due to option changes */ private boolean mFullScanPending; + /** + * Set if we are in the edit dialog + */ + private boolean mIsEditingDirectories; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -97,10 +110,12 @@ public class PreferencesMediaLibrary extends Fragment implements View.OnClickLis mStartButton = (View)view.findViewById(R.id.start_button); mCancelButton = (View)view.findViewById(R.id.cancel_button); + mEditButton = (View)view.findViewById(R.id.edit_button); mProgressText = (TextView)view.findViewById(R.id.media_stats_progress_text); mProgressBar = (ProgressBar)view.findViewById(R.id.media_stats_progress_bar); mStatsTracks = (TextView)view.findViewById(R.id.media_stats_tracks); mStatsPlaytime = (TextView)view.findViewById(R.id.media_stats_playtime); + mMediaDirectories = (TextView)view.findViewById(R.id.media_directories); mFullScanCheck = (CheckBox)view.findViewById(R.id.media_scan_full); mDropDbCheck = (CheckBox)view.findViewById(R.id.media_scan_drop_db); mGroupAlbumsCheck = (CheckBox)view.findViewById(R.id.media_scan_group_albums); @@ -109,6 +124,7 @@ public class PreferencesMediaLibrary extends Fragment implements View.OnClickLis // Bind onClickListener to some elements mStartButton.setOnClickListener(this); mCancelButton.setOnClickListener(this); + mEditButton.setOnClickListener(this); mGroupAlbumsCheck.setOnClickListener(this); mForceBastpCheck.setOnClickListener(this); } @@ -129,6 +145,9 @@ public class PreferencesMediaLibrary extends Fragment implements View.OnClickLis }); }}), 0, 200); + if (mIsEditingDirectories) + mIsEditingDirectories = false; + updatePreferences(null); } @@ -140,7 +159,7 @@ public class PreferencesMediaLibrary extends Fragment implements View.OnClickLis mTimer = null; } - if (mFullScanPending) { + if (mFullScanPending && !mIsEditingDirectories) { MediaLibrary.startLibraryScan(getActivity(), true, true); mFullScanPending = false; } @@ -155,6 +174,11 @@ public class PreferencesMediaLibrary extends Fragment implements View.OnClickLis case R.id.cancel_button: cancelButtonPressed(view); break; + case R.id.edit_button: + mIsEditingDirectories = true; + mFullScanPending = true; + startActivity(new Intent(getActivity(), MediaFoldersSelectionActivity.class)); + break; case R.id.media_scan_group_albums: case R.id.media_scan_force_bastp: confirmUpdatePreferences((CheckBox)view); @@ -212,6 +236,16 @@ public class PreferencesMediaLibrary extends Fragment implements View.OnClickLis mGroupAlbumsCheck.setChecked(prefs.groupAlbumsByFolder); mForceBastpCheck.setChecked(prefs.forceBastp); + + String txt = ""; + for (String path : prefs.mediaFolders) { + txt += "✔ " + path + "\n"; + } + for (String path : prefs.blacklistedFolders) { + txt += "✘ " + path + "\n"; + } + mMediaDirectories.setText(txt); + } /** @@ -227,6 +261,7 @@ public class PreferencesMediaLibrary extends Fragment implements View.OnClickLis mProgressBar.setProgress(progress.seen); mStartButton.setEnabled(idle); + mEditButton.setEnabled(idle); mDropDbCheck.setEnabled(idle); mFullScanCheck.setEnabled(idle); mForceBastpCheck.setEnabled(idle);