Merge indexer-select branch.
This adds the ability to include and exclude media folders.
@ -152,6 +152,8 @@ THE SOFTWARE.
|
||||
android:name="TabOrderActivity" />
|
||||
<activity
|
||||
android:name="FilebrowserStartActivity" />
|
||||
<activity
|
||||
android:name="MediaFoldersSelectionActivity" />
|
||||
<activity
|
||||
android:name="PermissionRequestActivity"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||
|
BIN
orig/file_document.svgz
Normal file
BIN
orig/file_image.svgz
Normal file
BIN
orig/file_music.svgz
Normal file
BIN
orig/folder.svgz
BIN
res/drawable-hdpi/file_document.png
Normal file
After Width: | Height: | Size: 985 B |
BIN
res/drawable-hdpi/file_image.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
res/drawable-hdpi/file_music.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 757 B After Width: | Height: | Size: 882 B |
BIN
res/drawable-mdpi/file_document.png
Normal file
After Width: | Height: | Size: 723 B |
BIN
res/drawable-mdpi/file_image.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
res/drawable-mdpi/file_music.png
Normal file
After Width: | Height: | Size: 934 B |
Before Width: | Height: | Size: 632 B After Width: | Height: | Size: 645 B |
BIN
res/drawable-xhdpi/file_document.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
res/drawable-xhdpi/file_image.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
res/drawable-xhdpi/file_music.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 875 B After Width: | Height: | Size: 1.1 KiB |
BIN
res/drawable-xxhdpi/file_document.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
res/drawable-xxhdpi/file_image.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
res/drawable-xxhdpi/file_music.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.6 KiB |
@ -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" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@ -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" />
|
||||
|
||||
</LinearLayout>
|
@ -39,6 +39,25 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/media_scan_force_bastp" />
|
||||
|
||||
<TextView
|
||||
style="?android:attr/listSeparatorTextViewStyle"
|
||||
android:textColor="?overlay_foreground_color"
|
||||
android:text="@string/media_folders_header" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/media_directories"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="-" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/edit_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:text="@string/edit">
|
||||
</Button>
|
||||
|
||||
<TextView
|
||||
style="?android:attr/listSeparatorTextViewStyle"
|
||||
android:textColor="?overlay_foreground_color"
|
||||
|
@ -303,6 +303,7 @@ THE SOFTWARE.
|
||||
<string name="filebrowser_start">Filebrowser home</string>
|
||||
<string name="customize_filebrowser_start">Filebrowser starts at this directory</string>
|
||||
<string name="select">Select</string>
|
||||
<string name="save">Save</string>
|
||||
|
||||
|
||||
<string name="autoplaylist_playcounts_title">Automatic playlist creation</string>
|
||||
@ -319,6 +320,7 @@ THE SOFTWARE.
|
||||
<string name="no_receiving_apps">No receiving apps found for this media type!</string>
|
||||
|
||||
<string name="media_library">Media library</string>
|
||||
<string name="media_folders_header">Indexed directories</string>
|
||||
<string name="media_scan_header">Media scanner</string>
|
||||
<string name="media_scan_full">Scan full filesystem (Slow)</string>
|
||||
<string name="media_scan_drop_db">Flush media database (Dangerzone!)</string>
|
||||
@ -334,6 +336,11 @@ THE SOFTWARE.
|
||||
<string name="media_scan_preferences_change_title">Scanner options changed</string>
|
||||
<string name="media_scan_preferences_change_message">Changing this option will start a full rescan of your library. Would you like to continue?</string>
|
||||
|
||||
<!-- Folder selector -->
|
||||
<string name="hint_long_press_to_modify_folder">Long press to modify folder options</string>
|
||||
<string name="folder_include">Include</string>
|
||||
<string name="folder_exclude">Exclude</string>
|
||||
<string name="folder_neutral">Neutral</string>
|
||||
<!-- Plugin system -->
|
||||
<string name="plugins">Plugins</string>
|
||||
</resources>
|
||||
|
@ -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<String> mediaFolders;
|
||||
public ArrayList<String> 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<String> discoverDefaultMediaPaths() {
|
||||
ArrayList<String> 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<String> discoverDefaultBlacklistedPaths() {
|
||||
final String[] defaultBlacklistPostfix = { "Android/data", "Alarms", "Notifications", "Ringtones" };
|
||||
ArrayList<String> 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<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 {
|
||||
/**
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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<String> a, ArrayList<String> b) {
|
||||
mPrefEditor.putString(PrefKeys.FILESYSTEM_BROWSE_START, directory.getAbsolutePath());
|
||||
mPrefEditor.commit();
|
||||
finish();
|
||||
|
@ -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<String> mIncludedDirs;
|
||||
/**
|
||||
* List of excluded dirs in tristate mode
|
||||
*/
|
||||
private ArrayList<String> 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<String> included, ArrayList<String> 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<String> included, ArrayList<String> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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<String>
|
||||
extends ArrayAdapter<FolderPickerAdapter.Item>
|
||||
{
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.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<String> included, ArrayList<String> excluded) {
|
||||
MediaLibrary.Preferences prefs = MediaLibrary.getPreferences(this);
|
||||
prefs.mediaFolders = included;
|
||||
prefs.blacklistedFolders = excluded;
|
||||
MediaLibrary.setPreferences(this, prefs);
|
||||
finish();
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
|