package com.obsez.android.lib.filechooser; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import static com.obsez.android.lib.filechooser.internals.FileUtil.NewFolderFilter; import android.Manifest; import android.app.Activity; import android.app.AlertDialog; import android.app.Fragment; import android.content.Context; import android.content.DialogInterface; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.os.Build; import android.support.annotation.DrawableRes; import android.support.annotation.LayoutRes; import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; import android.support.annotation.StringRes; import android.support.annotation.StyleRes; import android.support.v4.content.ContextCompat; import android.support.v4.view.ViewCompat; import android.support.v7.view.ContextThemeWrapper; import android.util.TypedValue; import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.Window; import android.widget.AdapterView; import android.widget.Button; import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import com.obsez.android.lib.filechooser.internals.ExtFileFilter; import com.obsez.android.lib.filechooser.internals.FileUtil; import com.obsez.android.lib.filechooser.internals.RegexFileFilter; import com.obsez.android.lib.filechooser.permissions.PermissionsUtil; import com.obsez.android.lib.filechooser.tool.DirAdapter; import com.obsez.android.lib.filechooser.tool.RootFile; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.regex.Pattern; /** * Created by coco on 6/7/15. */ public class ChooserDialog implements AdapterView.OnItemClickListener, DialogInterface.OnClickListener, AdapterView.OnItemLongClickListener, AdapterView.OnItemSelectedListener { @FunctionalInterface public interface Result { void onChoosePath(String dir, File dirFile); } /** * @deprecated will be removed at v1.2 */ public ChooserDialog() { } public ChooserDialog(Context cxt, @StyleRes int fileChooserTheme) { this._context = cxt; init(fileChooserTheme); } public ChooserDialog(Activity activity, @StyleRes int fileChooserTheme) { this._context = activity; init(fileChooserTheme); } public ChooserDialog(Fragment fragment, @StyleRes int fileChooserTheme) { this._context = fragment.getActivity(); init(fileChooserTheme); } public ChooserDialog(Context cxt) { this._context = cxt; init(); } public ChooserDialog(Activity activity) { this._context = activity; init(); } public ChooserDialog(Fragment fragment) { this._context = fragment.getActivity(); init(); } /** * @param cxt android context * @return `this` reference * @deprecated will be removed at v1.2 */ public ChooserDialog with(Context cxt) { this._context = cxt; init(); return this; } private void init() { init(null); } private void init(@Nullable @StyleRes Integer fileChooserTheme) { _onBackPressed = new defBackPressed(this); if (fileChooserTheme == null) { TypedValue typedValue = new TypedValue(); if (!this._context.getTheme().resolveAttribute( R.attr.fileChooserStyle, typedValue, true)) { this._context = new ContextThemeWrapper(this._context, R.style.FileChooserStyle); } else { this._context = new ContextThemeWrapper(this._context, typedValue.resourceId); } } else { //noinspection UnnecessaryUnboxing this._context = new ContextThemeWrapper(this._context, fileChooserTheme.intValue()); } } public ChooserDialog withFilter(FileFilter ff) { withFilter(false, false, (String[]) null); this._fileFilter = ff; return this; } public ChooserDialog withFilter(boolean dirOnly, boolean allowHidden, FileFilter ff) { withFilter(dirOnly, allowHidden, (String[]) null); this._fileFilter = ff; return this; } public ChooserDialog withFilter(boolean allowHidden, String... suffixes) { return withFilter(false, allowHidden, suffixes); } public ChooserDialog withFilter(boolean dirOnly, final boolean allowHidden, String... suffixes) { this._dirOnly = dirOnly; if (suffixes == null || suffixes.length == 0) { this._fileFilter = dirOnly ? file -> file.isDirectory() && (!file.isHidden() || allowHidden) : file -> !file.isHidden() || allowHidden; } else { this._fileFilter = new ExtFileFilter(_dirOnly, allowHidden, suffixes); } return this; } public ChooserDialog withFilterRegex(boolean dirOnly, boolean allowHidden, String pattern, int flags) { this._dirOnly = dirOnly; this._fileFilter = new RegexFileFilter(_dirOnly, allowHidden, pattern, flags); return this; } public ChooserDialog withFilterRegex(boolean dirOnly, boolean allowHidden, String pattern) { this._dirOnly = dirOnly; this._fileFilter = new RegexFileFilter(_dirOnly, allowHidden, pattern, Pattern.CASE_INSENSITIVE); return this; } public ChooserDialog withStartFile(String startFile) { if (startFile != null) { _currentDir = new File(startFile); } else { _currentDir = new File(FileUtil.getStoragePath(_context, false)); } if (!_currentDir.isDirectory()) { _currentDir = _currentDir.getParentFile(); } if (_currentDir == null) { _currentDir = new File(FileUtil.getStoragePath(_context, false)); } return this; } @Deprecated public ChooserDialog dismissOnButtonClick(final boolean dismissOnButtonClick) { return this; } public ChooserDialog withChosenListener(Result r) { this._result = r; return this; } /** * called every time {@link KeyEvent#KEYCODE_BACK} is caught, * and current directory is not the root of Primary/SdCard storage. */ public ChooserDialog withOnBackPressedListener(OnBackPressedListener listener) { if (this._onBackPressed instanceof defBackPressed) { ((defBackPressed) this._onBackPressed)._onBackPressed = listener; } return this; } /** * called if {@link KeyEvent#KEYCODE_BACK} is caught, * and current directory is the root of Primary/SdCard storage. */ public ChooserDialog withOnLastBackPressedListener(OnBackPressedListener listener) { if (this._onBackPressed instanceof defBackPressed) { ((defBackPressed) this._onBackPressed)._onLastBackPressed = listener; } return this; } public ChooserDialog withResources(@StringRes int titleRes, @StringRes int okRes, @StringRes int cancelRes) { this._titleRes = titleRes; this._okRes = okRes; this._negativeRes = cancelRes; return this; } public ChooserDialog withStringResources(@Nullable String titleRes, @Nullable String okRes, @Nullable String cancelRes) { this._title = titleRes; this._ok = okRes; this._negative = cancelRes; return this; } /** * To enable the option pane with create/delete folder on the fly. * When u set it true, you may need WRITE_EXTERNAL_STORAGE declaration too. * * @param enableOptions true/false * @return this */ public ChooserDialog enableOptions(boolean enableOptions) { this._enableOptions = enableOptions; return this; } public ChooserDialog withOptionResources(@StringRes int createDirRes, @StringRes int deleteRes, @StringRes int newFolderCancelRes, @StringRes int newFolderOkRes) { this._createDirRes = createDirRes; this._deleteRes = deleteRes; this._newFolderCancelRes = newFolderCancelRes; this._newFolderOkRes = newFolderOkRes; return this; } public ChooserDialog withOptionStringResources(@Nullable String createDir, @Nullable String delete, @Nullable String newFolderCancel, @Nullable String newFolderOk) { this._createDir = createDir; this._delete = delete; this._newFolderCancel = newFolderCancel; this._newFolderOk = newFolderOk; return this; } public ChooserDialog withOptionIcons(@DrawableRes int optionsIconRes, @DrawableRes int createDirIconRes, @DrawableRes int deleteRes) { this._optionsIconRes = optionsIconRes; this._createDirIconRes = createDirIconRes; this._deleteIconRes = deleteRes; return this; } public ChooserDialog withOptionIcons(@Nullable Drawable optionsIcon, @Nullable Drawable createDirIcon, @Nullable Drawable deleteIcon) { this._optionsIcon = optionsIcon; this._createDirIcon = createDirIcon; this._deleteIcon = deleteIcon; return this; } public ChooserDialog withNewFolderFilter(NewFolderFilter filter) { this._newFolderFilter = filter; return this; } public ChooserDialog withIcon(@DrawableRes int iconId) { this._iconRes = iconId; return this; } public ChooserDialog withIcon(@Nullable Drawable icon) { this._icon = icon; return this; } @RequiresApi(Build.VERSION_CODES.LOLLIPOP) public ChooserDialog withLayoutView(@LayoutRes int layoutResId) { this._layoutRes = layoutResId; return this; } /** * @deprecated use {@link AdapterSetter#apply(DirAdapter)} * and then {@link DirAdapter.GetView#getView(File, boolean, boolean, View, ViewGroup, LayoutInflater)} * instead */ public ChooserDialog withRowLayoutView(@LayoutRes int layoutResId) { this._rowLayoutRes = layoutResId; return this; } public ChooserDialog withDateFormat() { return this.withDateFormat("yyyy/MM/dd HH:mm:ss"); } public ChooserDialog withDateFormat(String format) { this._dateFormat = format; return this; } public ChooserDialog withNegativeButton(@StringRes int cancelTitle, final DialogInterface.OnClickListener listener) { this._negativeRes = cancelTitle; this._negativeListener = listener; return this; } public ChooserDialog withNegativeButton(@Nullable String cancelTitle, final DialogInterface.OnClickListener listener) { this._negative = cancelTitle; if (cancelTitle != null) this._negativeRes = -1; this._negativeListener = listener; return this; } public ChooserDialog withNegativeButtonListener(final DialogInterface.OnClickListener listener) { this._negativeListener = listener; return this; } /** * onCancelListener will be triggered on back pressed or clicked outside of dialog */ public ChooserDialog withOnCancelListener(final DialogInterface.OnCancelListener listener) { this._cancelListener = listener; return this; } @RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR1) public ChooserDialog withOnDismissListener(final DialogInterface.OnDismissListener listener) { _onDismissListener = listener; return this; } public ChooserDialog withFileIcons(final boolean tryResolveFileTypeAndIcon, final Drawable fileIcon, final Drawable folderIcon) { _adapterSetter = adapter -> { if (fileIcon != null) adapter.setDefaultFileIcon(fileIcon); if (folderIcon != null) adapter.setDefaultFolderIcon(folderIcon); adapter.setResolveFileType(tryResolveFileTypeAndIcon); }; return this; } public ChooserDialog withFileIconsRes(final boolean tryResolveFileTypeAndIcon, final int fileIcon, final int folderIcon) { _adapterSetter = adapter -> { if (fileIcon != -1) { adapter.setDefaultFileIcon(ContextCompat.getDrawable(_context, fileIcon)); } if (folderIcon != -1) { adapter.setDefaultFolderIcon( ContextCompat.getDrawable(_context, folderIcon)); } adapter.setResolveFileType(tryResolveFileTypeAndIcon); }; return this; } /** * @param setter you can override {@link DirAdapter#getView(int, View, ViewGroup)} * see {@link AdapterSetter} for more information * @return this */ public ChooserDialog withAdapterSetter(AdapterSetter setter) { _adapterSetter = setter; return this; } /** * @param cb give a hook at navigating up to a directory * @return this */ public ChooserDialog withNavigateUpTo(CanNavigateUp cb) { _folderNavUpCB = cb; return this; } /** * @param cb give a hook at navigating to a child directory * @return this */ public ChooserDialog withNavigateTo(CanNavigateTo cb) { _folderNavToCB = cb; return this; } public ChooserDialog disableTitle(boolean disableTitle) { _disableTitle = disableTitle; return this; } /** * allows dialog title follows the current folder name * * @param followDir dialog title will follow the changing of directory * @return this */ public ChooserDialog titleFollowsDir(boolean followDir) { _followDir = followDir; return this; } /** * @param followDir deprecated * @return this * @deprecated at 1.1.17 or 1.2.0 */ public ChooserDialog followDir(boolean followDir) { _followDir = followDir; return this; } public ChooserDialog displayPath(boolean displayPath) { _displayPath = displayPath; return this; } public ChooserDialog customizePathView(CustomizePathView callback) { _customizePathView = callback; return this; } public ChooserDialog enableMultiple(boolean enableMultiple) { this._enableMultiple = enableMultiple; return this; } public ChooserDialog cancelOnTouchOutside(boolean cancelOnTouchOutside) { this._cancelOnTouchOutside = cancelOnTouchOutside; return this; } public ChooserDialog enableDpad(boolean enableDpad) { this._enableDpad = enableDpad; return this; } public ChooserDialog build() { TypedArray ta = _context.obtainStyledAttributes(R.styleable.FileChooser); final AlertDialog.Builder builder = new AlertDialog.Builder(_context, ta.getResourceId(R.styleable.FileChooser_fileChooserDialogStyle, R.style.FileChooserDialogStyle)); final int style = ta.getResourceId(R.styleable.FileChooser_fileChooserListItemStyle, R.style.FileChooserListItemStyle); ta.recycle(); final Context context = new ContextThemeWrapper(_context, style); ta = context.obtainStyledAttributes(R.styleable.FileChooser); final int listview_item_selector = ta.getResourceId(R.styleable.FileChooser_fileListItemFocusedDrawable, R.drawable.listview_item_selector); ta.recycle(); if (_rowLayoutRes != -1) { _adapter = new DirAdapter(context, new ArrayList<>(), _rowLayoutRes, this._dateFormat); } else { _adapter = new DirAdapter(context, this._dateFormat); } if (_adapterSetter != null) _adapterSetter.apply(_adapter); refreshDirs(); builder.setAdapter(_adapter, this); if (!_disableTitle) { if (_titleRes != -1) { builder.setTitle(_titleRes); } else if (_title != null) { builder.setTitle(_title); } else { builder.setTitle(R.string.choose_file); } } if (_iconRes != -1) { builder.setIcon(_iconRes); } else if (_icon != null) { builder.setIcon(_icon); } if (_layoutRes != -1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { builder.setView(_layoutRes); } } if (_dirOnly || _enableMultiple) { // choosing folder, or multiple files picker DialogInterface.OnClickListener listener = (dialog, which) -> { if (_result != null) { _result.onChoosePath(_currentDir.getAbsolutePath(), _currentDir); } }; if (_okRes != -1) { builder.setPositiveButton(_okRes, listener); } else if (_ok != null) { builder.setPositiveButton(_ok, listener); } else { builder.setPositiveButton(R.string.title_choose, listener); } } if (_negativeRes != -1) { builder.setNegativeButton(_negativeRes, _negativeListener); } else if (_negative != null) { builder.setNegativeButton(_negative, _negativeListener); } else { builder.setNegativeButton(R.string.dialog_cancel, _negativeListener); } if (_cancelListener != null) { builder.setOnCancelListener(_cancelListener); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && _onDismissListener != null) { builder.setOnDismissListener(_onDismissListener); } builder.setOnItemSelectedListener(this) .setOnKeyListener(new keyListener(this)); _alertDialog = builder.create(); _alertDialog.setCanceledOnTouchOutside(this._cancelOnTouchOutside); _alertDialog.setOnShowListener(new onShowListener(this, listview_item_selector)); _list = _alertDialog.getListView(); _list.setOnItemClickListener(this); if (_enableMultiple) { _list.setOnItemLongClickListener(this); } if (_enableDpad) { this._list.setSelector(listview_item_selector); this._list.setDrawSelectorOnTop(true); this._list.setItemsCanFocus(true); this._list.setChoiceMode(ListView.CHOICE_MODE_SINGLE); } _list.requestFocus(); return this; } private void showDialog() { Window window = _alertDialog.getWindow(); if (window != null) { TypedArray ta = _context.obtainStyledAttributes(R.styleable.FileChooser); window.setGravity(ta.getInt(R.styleable.FileChooser_fileChooserDialogGravity, Gravity.CENTER)); ta.recycle(); } _alertDialog.show(); } public ChooserDialog show() { if (_alertDialog == null || _list == null) { build(); } if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { showDialog(); return this; } if (_permissionListener == null) { _permissionListener = new PermissionsUtil.OnPermissionListener() { @Override public void onPermissionGranted(String[] permissions) { boolean show = false; for (String permission : permissions) { if (permission.equals(Manifest.permission.READ_EXTERNAL_STORAGE)) { show = true; break; } } if (!show) return; if (_enableOptions) { show = false; for (String permission : permissions) { if (permission.equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { show = true; break; } } } if (!show) return; if (_adapter.isEmpty()) refreshDirs(); showDialog(); } @Override public void onPermissionDenied(String[] permissions) { // } @Override public void onShouldShowRequestPermissionRationale(final String[] permissions) { Toast.makeText(_context, "You denied the Read/Write permissions on SDCard.", Toast.LENGTH_LONG).show(); } }; } final String[] permissions = _enableOptions ? new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE} : new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}; PermissionsUtil.checkPermissions(_context, _permissionListener, permissions); return this; } private boolean displayRoot; private void displayPath(String path) { if (_pathView == null) { final int rootId = _context.getResources().getIdentifier("contentPanel", "id", "android"); final ViewGroup root = ((AlertDialog) _alertDialog).findViewById(rootId); // In case the id was changed or not found. if (root == null) return; ViewGroup.MarginLayoutParams params; if (root instanceof LinearLayout) { params = new LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT); } else { params = new FrameLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT, Gravity.TOP); } TypedArray ta = _context.obtainStyledAttributes(R.styleable.FileChooser); int style = ta.getResourceId(R.styleable.FileChooser_fileChooserPathViewStyle, R.style.FileChooserPathViewStyle); final Context context = new ContextThemeWrapper(_context, style); ta.recycle(); ta = context.obtainStyledAttributes(R.styleable.FileChooser); displayRoot = ta.getBoolean(R.styleable.FileChooser_fileChooserPathViewDisplayRoot, true); _pathView = new TextView(context); root.addView(_pathView, 0, params); int elevation = ta.getInt(R.styleable.FileChooser_fileChooserPathViewElevation, 2); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { _pathView.setElevation(elevation); } else { ViewCompat.setElevation(_pathView, elevation); } ta.recycle(); if (_customizePathView != null) { _customizePathView.customize(_pathView); } } if (path == null) { _pathView.setVisibility(View.GONE); ViewGroup.MarginLayoutParams param = ((ViewGroup.MarginLayoutParams) _list.getLayoutParams()); if (_pathView.getParent() instanceof FrameLayout) { param.topMargin = _pathView.getHeight(); } _list.setLayoutParams(param); } else { if (removableRoot == null || primaryRoot == null) { removableRoot = FileUtil.getStoragePath(_context, true); primaryRoot = FileUtil.getStoragePath(_context, false); } if (path.contains(removableRoot)) { path = path.substring(displayRoot ? removableRoot.lastIndexOf('/') + 1 : removableRoot.length()); } if (path.contains(primaryRoot)) { path = path.substring(displayRoot ? primaryRoot.lastIndexOf('/') + 1 : primaryRoot.length()); } _pathView.setText(path); while (_pathView.getLineCount() > 1) { int i = path.indexOf("/"); i = path.indexOf("/", i + 1); if (i == -1) break; path = "..." + path.substring(i); _pathView.setText(path); } _pathView.setVisibility(View.VISIBLE); ViewGroup.MarginLayoutParams param = ((ViewGroup.MarginLayoutParams) _list.getLayoutParams()); if (_pathView.getHeight() == 0) { ViewTreeObserver viewTreeObserver = _pathView.getViewTreeObserver(); viewTreeObserver.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { if (_pathView.getHeight() <= 0) { return false; } viewTreeObserver.removeOnPreDrawListener(this); if (_pathView.getParent() instanceof FrameLayout) { param.topMargin = _pathView.getHeight(); } _list.setLayoutParams(param); return true; } }); } else { if (_pathView.getParent() instanceof FrameLayout) { param.topMargin = _pathView.getHeight(); } _list.setLayoutParams(param); } } } private String removableRoot = null; private String primaryRoot = null; private void listDirs() { _entries.clear(); if (_currentDir == null) { _currentDir = new File(FileUtil.getStoragePath(_context, false)); } // Get files File[] files = _currentDir.listFiles(_fileFilter); // Add the ".." entry if (removableRoot == null || primaryRoot == null) { removableRoot = FileUtil.getStoragePath(_context, true); primaryRoot = FileUtil.getStoragePath(_context, false); } if (!removableRoot.equals(primaryRoot)) { if (_currentDir.getAbsolutePath().equals(primaryRoot)) { _entries.add(new RootFile(removableRoot, sSdcardStorage)); //⇠ } else if (_currentDir.getAbsolutePath().equals(removableRoot)) { _entries.add(new RootFile(primaryRoot, sPrimaryStorage)); //⇽ } } boolean displayPath = false; if (_entries.isEmpty() && _currentDir.getParentFile() != null && _currentDir.getParentFile().canRead()) { _entries.add(new RootFile(_currentDir.getParentFile().getAbsolutePath(), "..")); displayPath = true; } if (files == null) return; List dirList = new LinkedList<>(); List fileList = new LinkedList<>(); for (File f : files) { if (f.isDirectory()) { dirList.add(f); } else { fileList.add(f); } } sortByName(dirList); sortByName(fileList); _entries.addAll(dirList); _entries.addAll(fileList); // #45: setup dialog title too if (_alertDialog != null && !_disableTitle) { if (_followDir) { if (displayPath) { _alertDialog.setTitle(_currentDir.getName()); } else { if (_titleRes != -1) { _alertDialog.setTitle(_titleRes); } else if (_title != null) { _alertDialog.setTitle(_title); } else { _alertDialog.setTitle(R.string.choose_file); } } } } // don't display path before alert dialog is shown // to avoid the exception under android M: // Caused by android.util.AndroidRuntimeException: requestFeature() must be called before adding // content // issue #60 if (_alertDialog != null && _alertDialog.isShowing() && _displayPath) { if (displayPath) { displayPath(_currentDir.getPath()); } else { displayPath(null); } } } private void sortByName(List list) { Collections.sort(list, (f1, f2) -> f1.getName().toLowerCase().compareTo(f2.getName().toLowerCase())); } void createNewDirectory(String name) { if (FileUtil.createNewDirectory(name, _currentDir)) { refreshDirs(); return; } final File newDir = new File(_currentDir, name); Toast.makeText(_context, "Couldn't create folder " + newDir.getName() + " at " + newDir.getAbsolutePath(), Toast.LENGTH_LONG).show(); } Runnable _deleteModeIndicator; private int scrollTo; @Override public void onItemClick(AdapterView parent_, View list_, int position, long id_) { if (position < 0 || position >= _entries.size()) return; scrollTo = 0; File file = _entries.get(position); if (file instanceof RootFile) { if (_folderNavUpCB == null) _folderNavUpCB = _defaultNavUpCB; if (_folderNavUpCB.canUpTo(file)) { _currentDir = file; _chooseMode = _chooseMode == CHOOSE_MODE_DELETE ? CHOOSE_MODE_NORMAL : _chooseMode; if (_deleteModeIndicator != null) _deleteModeIndicator.run(); lastSelected = false; if (!_adapter.getIndexStack().empty()) { scrollTo = _adapter.getIndexStack().pop(); } } } else { switch (_chooseMode) { case CHOOSE_MODE_NORMAL: if (file.isDirectory()) { if (_folderNavToCB == null) _folderNavToCB = _defaultNavToCB; if (_folderNavToCB.canNavigate(file)) { _currentDir = file; scrollTo = 0; _adapter.getIndexStack().push(position); } } else if ((!_dirOnly) && _result != null) { _alertDialog.dismiss(); _result.onChoosePath(file.getAbsolutePath(), file); if (_enableMultiple) { _result.onChoosePath(_currentDir.getAbsolutePath(), _currentDir); } return; } lastSelected = false; break; case CHOOSE_MODE_SELECT_MULTIPLE: if (file.isDirectory()) { if (_folderNavToCB == null) _folderNavToCB = _defaultNavToCB; if (_folderNavToCB.canNavigate(file)) { _currentDir = file; scrollTo = 0; _adapter.getIndexStack().push(position); } } else { _adapter.selectItem(position); if (!_adapter.isAnySelected()) { _chooseMode = CHOOSE_MODE_NORMAL; _positiveBtn.setVisibility(View.INVISIBLE); } _result.onChoosePath(file.getAbsolutePath(), file); return; } break; case CHOOSE_MODE_DELETE: try { FileUtil.deleteFileRecursively(file); } catch (IOException e) { e.printStackTrace(); Toast.makeText(_context, e.getMessage(), Toast.LENGTH_LONG).show(); } _chooseMode = CHOOSE_MODE_NORMAL; if (_deleteModeIndicator != null) _deleteModeIndicator.run(); break; default: // ERROR! It shouldn't get here... return; } } refreshDirs(); _list.setSelection(scrollTo); _list.post(() -> _list.setSelection(scrollTo)); } @Override public boolean onItemLongClick(AdapterView parent, View list, int position, long id) { File file = _entries.get(position); if (file instanceof RootFile || file.isDirectory()) { return true; } if (_adapter.isSelected(position)) return true; _result.onChoosePath(file.getAbsolutePath(), file); _adapter.selectItem(position); _chooseMode = CHOOSE_MODE_SELECT_MULTIPLE; _positiveBtn.setVisibility(View.VISIBLE); if (_deleteModeIndicator != null) _deleteModeIndicator.run(); return true; } @Override public void onClick(DialogInterface dialog, int which) { // } void refreshDirs() { listDirs(); _adapter.setEntries(_entries); } public void dismiss() { _alertDialog.dismiss(); } boolean lastSelected = false; @Override public void onItemSelected(final AdapterView parent, final View view, final int position, final long id) { lastSelected = position == _entries.size() - 1; } @Override public void onNothingSelected(final AdapterView parent) { lastSelected = false; } List _entries = new ArrayList<>(); DirAdapter _adapter; File _currentDir; Context _context; AlertDialog _alertDialog; ListView _list; Result _result = null; private boolean _dirOnly; private FileFilter _fileFilter; private @StringRes int _titleRes = -1, _okRes = -1, _negativeRes = -1; private @Nullable String _title, _ok, _negative; private @DrawableRes int _iconRes = -1; private @Nullable Drawable _icon; private @LayoutRes int _layoutRes = -1; private @LayoutRes @Deprecated int _rowLayoutRes = -1; private String _dateFormat; private DialogInterface.OnClickListener _negativeListener; private DialogInterface.OnCancelListener _cancelListener; private DialogInterface.OnDismissListener _onDismissListener; private boolean _disableTitle; boolean _enableOptions; private boolean _followDir; private boolean _displayPath = true; TextView _pathView; private CustomizePathView _customizePathView; View _options; @StringRes int _createDirRes = -1, _deleteRes = -1, _newFolderCancelRes = -1, _newFolderOkRes = -1; @Nullable String _createDir, _delete, _newFolderCancel, _newFolderOk; @DrawableRes int _optionsIconRes = -1, _createDirIconRes = -1, _deleteIconRes = -1; @Nullable Drawable _optionsIcon, _createDirIcon, _deleteIcon; @Nullable View _newFolderView; boolean _enableMultiple; private PermissionsUtil.OnPermissionListener _permissionListener; private boolean _cancelOnTouchOutside; boolean _enableDpad = true; Button _neutralBtn; Button _negativeBtn; Button _positiveBtn; @FunctionalInterface public interface AdapterSetter { void apply(DirAdapter adapter); } private AdapterSetter _adapterSetter = null; @FunctionalInterface public interface CanNavigateUp { boolean canUpTo(File dir); } @FunctionalInterface public interface CanNavigateTo { boolean canNavigate(File dir); } private CanNavigateUp _folderNavUpCB; private CanNavigateTo _folderNavToCB; private final static CanNavigateUp _defaultNavUpCB = dir -> dir != null && dir.canRead(); private final static CanNavigateTo _defaultNavToCB = dir -> true; /** * attempts to move to the parent directory * * @return true if successful. false otherwise */ public boolean goBack() { if (_entries.size() > 0 && (_entries.get(0).getName().equals(".."))) { _list.performItemClick(_list, 0, 0); return true; } return false; } @FunctionalInterface public interface OnBackPressedListener { void onBackPressed(AlertDialog dialog); } OnBackPressedListener _onBackPressed; private final static String sSdcardStorage = ".. SDCard Storage"; private final static String sPrimaryStorage = ".. Primary Storage"; static final int CHOOSE_MODE_NORMAL = 0; static final int CHOOSE_MODE_DELETE = 1; static final int CHOOSE_MODE_SELECT_MULTIPLE = 2; int _chooseMode = CHOOSE_MODE_NORMAL; NewFolderFilter _newFolderFilter; @FunctionalInterface public interface CustomizePathView { void customize(TextView pathView); } }