Can now mark multiple files

Also generalized the dialog
This commit is contained in:
Jonas Kalderstam 2014-04-01 00:20:07 +02:00
parent cab9cd63a3
commit 423716aae4
10 changed files with 399 additions and 151 deletions

View File

@ -18,17 +18,20 @@
package com.nononsenseapps.filepicker;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.FragmentManager;
import android.content.ClipData;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.Window;
import android.view.WindowManager;
import java.util.ArrayList;
import java.util.List;
/**
@ -48,16 +51,26 @@ import java.util.List;
*
* The result of the user's action is returned in onActivityResult intent, access it using getUri.
* In case of multiple choices, these can be accessed with getClipData containing Uri objects.
* If running earlier than JellyBean you can access them with
* getStringArrayListExtra(EXTRA_PATHS)
*
* @param <T>
*/
public abstract class AbstractFilePickerActivity<T> extends Activity implements
AbstractFilePickerFragment.OnFilePickedListener {
public static final String EXTRA_START_PATH = "start_path";
public static final String EXTRA_ONLY_DIRS = "only_dirs";
public static final String EXTRA_START_PATH = "nononsense.intent" +
".extrastart_path";
public static final String EXTRA_ONLY_DIRS = "nononsense.intent.only_dirs";
// For compatibility
public static final String EXTRA_ALLOW_MULTIPLE = "android.intent.extra" +
".ALLOW_MULTIPLE";
public static final String EXTRA_PATHS = "nononsense.intent.paths";
private static final String TAG = "filepicker_fragment";
private String startPath = null;
protected boolean onlyDirs = false;
protected boolean allowMultiple = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -68,14 +81,11 @@ public abstract class AbstractFilePickerActivity<T> extends Activity implements
setContentView(R.layout.activity_filepicker);
String startPath = null;
boolean onlyDirs = false;
boolean allowMultiple = false;
Intent intent = getIntent();
if (intent != null) {
startPath = intent.getStringExtra(EXTRA_START_PATH);
onlyDirs = intent.getBooleanExtra(EXTRA_ONLY_DIRS, onlyDirs);
allowMultiple = intent.getBooleanExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple);
allowMultiple = intent.getBooleanExtra(EXTRA_ALLOW_MULTIPLE, allowMultiple);
}
FragmentManager fm = getFragmentManager();
@ -137,15 +147,30 @@ public abstract class AbstractFilePickerActivity<T> extends Activity implements
finish();
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
@Override
public void onFilesPicked(final List<Uri> files) {
Intent i = new Intent();
i.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
ClipData clip = new ClipData("Paths", new String[] {}, null);
for (Uri file: files) {
clip.addItem(new ClipData.Item(file));
i.putExtra(EXTRA_ALLOW_MULTIPLE, true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
ClipData clip = null;
for (Uri file : files) {
if (clip == null) {
clip = new ClipData("Paths", new String[]{}, new ClipData.Item(file));
} else {
clip.addItem(new ClipData.Item(file));
}
}
i.setClipData(clip);
} else {
ArrayList<String> paths = new ArrayList<String>();
for (Uri file : files) {
paths.add(file.toString());
}
i.putStringArrayListExtra(EXTRA_PATHS, paths);
}
i.setClipData(clip);
setResult(Activity.RESULT_OK, i);
finish();
}

View File

@ -26,11 +26,14 @@ import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.CheckedTextView;
import android.widget.ListView;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
/**
@ -43,14 +46,13 @@ import java.util.List;
public abstract class AbstractFilePickerFragment<T> extends
ListFragment implements
LoaderManager.LoaderCallbacks<List<T>>,
NewFolderFragment.OnNewFolderListener {
NewItemFragment.OnNewFolderListener, AdapterView.OnItemLongClickListener {
public static final String KEY_START_PATH = "KEY_START_PATH";
public static final String KEY_ONLY_DIRS = "KEY_ONLY_DIRS";
public static final String KEY_ALLOW_MULTIPLE = "KEY_ALLOW_MULTIPLE";
private static final String KEY_CURRENT_PATH = "KEY_START_PATH";
protected T currentPath = null;
protected List<T> currentPaths = null;
protected boolean onlyDirs = false;
protected boolean allowMultiple = false;
protected Comparator<T> comparator = null;
@ -58,11 +60,16 @@ public abstract class AbstractFilePickerFragment<T> extends
private BindableArrayAdapter<T> adapter;
private TextView currentDirView;
private View okButton;
protected final DefaultHashMap<Integer, Boolean> checkedItems;
/**
* Mandatory empty constructor for the fragment manager to instantiate the
* fragment (e.g. upon screen orientation changes).
*/
public AbstractFilePickerFragment() {
checkedItems = new DefaultHashMap<Integer, Boolean>(false);
}
/**
@ -100,12 +107,11 @@ public abstract class AbstractFilePickerFragment<T> extends
(KEY_START_PATH)) {
currentPath = getPath(getArguments().getString(KEY_START_PATH));
}
} else {
currentPath = getRoot();
}
if (currentPaths == null) {
currentPaths = new ArrayList<T>();
// If still null
if (currentPath == null) {
currentPath = getRoot();
}
}
@ -127,6 +133,17 @@ public abstract class AbstractFilePickerFragment<T> extends
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_filepicker, null);
ListView lv = (ListView) view.findViewById(android.R.id.list);
if (allowMultiple) {
lv.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
} else if (onlyDirs) {
lv.setChoiceMode(ListView.CHOICE_MODE_NONE);
} else {
lv.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
}
lv.setOnItemLongClickListener(this);
view.findViewById(R.id.button_cancel).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
@ -136,17 +153,19 @@ public abstract class AbstractFilePickerFragment<T> extends
}
});
okButton = view.findViewById(R.id.button_ok);
if (allowMultiple) {
view.findViewById(R.id.button_ok).setOnClickListener(new View.OnClickListener() {
okButton.setOnClickListener
(new View.OnClickListener() {
@Override
public void onClick(final View v) {
if (listener != null) {
listener.onFilesPicked(toUri(currentPaths));
listener.onFilesPicked(toUri(getCheckedItems()));
}
}
});
} else {
view.findViewById(R.id.button_ok).setOnClickListener(new View.OnClickListener() {
okButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
if (listener != null) {
@ -172,7 +191,7 @@ public abstract class AbstractFilePickerFragment<T> extends
AbstractFilePickerFragment.this);
}
});
currentDirView = (TextView) view.findViewById(R.id.current_dir);
// Restore state
if (currentPath != null) {
@ -184,7 +203,7 @@ public abstract class AbstractFilePickerFragment<T> extends
private List<Uri> toUri(List<T> files) {
ArrayList<Uri> uris = new ArrayList<Uri>();
for (T file: files) {
for (T file : files) {
uris.add(toUri(file));
}
return uris;
@ -225,21 +244,70 @@ public abstract class AbstractFilePickerFragment<T> extends
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
super.onListItemClick(l, v, position, id);
currentPath = (T) getListAdapter().getItem(position);
if (isDir(currentPath)) {
refresh();
return;
}
else if (l.getChoiceMode() != ListView.CHOICE_MODE_NONE) {
toggleItemCheck((CheckedTextView) v.findViewById(android.R.id.text1),
position);
}
}
if (allowMultiple) {
// TODO mark multiple
T data = (T) getListAdapter().getItem(position);
if (!currentPaths.remove(data)) {
currentPaths.add(data);
}
} else {
currentPath = (T) getListAdapter().getItem(position);
if (isDir(currentPath)) {
refresh();
} else {
// TODO mark file
private void toggleItemCheck(final CheckedTextView view,
final int position) {
final ListView lv = getListView();
if (lv.getChoiceMode() == ListView.CHOICE_MODE_NONE) {
return;
}
final boolean oldVal = checkedItems.get(position);
if (lv.getChoiceMode() == ListView.CHOICE_MODE_SINGLE) {
checkedItems.clear();
}
checkedItems.put(position, !oldVal);
// Redraw the items
lv.invalidateViews();
}
/**
*
* @return the selected files. Can be empty.
*/
protected List<T> getCheckedItems() {
final BindableArrayAdapter<T> adapter = (BindableArrayAdapter<T>) getListAdapter();
final ArrayList<T> files = new ArrayList<T>();
for (int pos: checkedItems.keySet()) {
if (checkedItems.get(pos)) {
files.add(adapter.getItem(pos));
}
}
return files;
}
/**
* Callback method to be invoked when an item in this view has been
* clicked and held.
* <p/>
* Implementers can call getItemAtPosition(position) if they need to access
* the data associated with the selected item.
*
* @param parent The AbsListView where the click happened
* @param view The view within the AbsListView that was clicked
* @param position The position of the view in the list
* @param id The row id of the item that was clicked
* @return true if the callback consumed the long click, false otherwise
*/
@Override
public boolean onItemLongClick(final AdapterView<?> parent, final View view, final int position, final long id) {
if (getListView().getChoiceMode() == ListView.CHOICE_MODE_NONE) {
return false;
}
toggleItemCheck((CheckedTextView) view.findViewById(android.R.id.text1),
position);
return true;
}
/**
@ -258,14 +326,12 @@ public abstract class AbstractFilePickerFragment<T> extends
protected abstract T getPath(final String path);
/**
*
* @param path
* @return the full path to the file
*/
protected abstract String getFullPath(final T path);
/**
*
* @param path
* @return the name of this file/folder
*/
@ -278,28 +344,56 @@ public abstract class AbstractFilePickerFragment<T> extends
/**
* Convert the path to a URI for the return intent
*
* @param path
* @return
*/
protected abstract Uri toUri(final T path);
/**
* @return a comparator that can sort the items alphabetically
*/
protected abstract Comparator<T> getComparator();
/**
*
* @return a ViewBinder to handle list items, or null.
*/
protected BindableArrayAdapter.ViewBinder<T> getViewBinder() {
return new BindableArrayAdapter.ViewBinder<T>() {
@Override
public void setViewValue(final View view, final T data) {
TextView textView = (TextView) view.findViewById(android.R.id.text1);
textView.setText(getName(data));
class ViewHolder {
protected View icon;
protected TextView text;
protected CheckedTextView checkbox;
}
view.findViewById(R.id.item_icon).setVisibility(isDir(data) ?
return new BindableArrayAdapter.ViewBinder<T>() {
boolean shouldCheck = getListView().getChoiceMode() != ListView
.CHOICE_MODE_NONE;
@Override
public void setViewValue(final View view,
final int position, final T data) {
if (view.getTag() == null) {
ViewHolder viewHolder = new ViewHolder();
viewHolder.icon = view.findViewById(R.id.item_icon);
viewHolder.text = (TextView) view.findViewById(android.R
.id.text1);
if (shouldCheck) {
viewHolder.checkbox = (CheckedTextView) view.findViewById
(android.R.id.text1);
}
view.setTag(viewHolder);
}
((ViewHolder) view.getTag()).text.setText(getName(data));
((ViewHolder) view.getTag()).icon.setVisibility(isDir(data) ?
View.VISIBLE : View.GONE);
if (((ViewHolder) view.getTag()).checkbox != null) {
((ViewHolder) view.getTag()).checkbox.setChecked
(checkedItems.get(position));
}
}
};
}
@ -366,7 +460,9 @@ public abstract class AbstractFilePickerFragment<T> extends
final List<T> data) {
if (adapter == null) {
adapter = new BindableArrayAdapter<T>(getActivity(),
R.layout.filepicker_listitem_dir);
getListView().getChoiceMode() == ListView.CHOICE_MODE_NONE ?
R.layout.filepicker_listitem_dir :
R.layout.filepicker_listitem_checkable);
adapter.setViewBinder(getViewBinder());
} else {
adapter.clear();
@ -374,6 +470,7 @@ public abstract class AbstractFilePickerFragment<T> extends
if (comparator == null) {
comparator = getComparator();
}
checkedItems.clear();
adapter.addAll(data);
adapter.sort(comparator);
setListAdapter(adapter);
@ -406,8 +503,20 @@ public abstract class AbstractFilePickerFragment<T> extends
*/
public interface OnFilePickedListener {
public void onFilePicked(Uri file);
public void onFilesPicked(List<Uri> files);
public void onCancelled();
}
public class DefaultHashMap<K,V> extends HashMap<K,V> {
protected final V defaultValue;
public DefaultHashMap(final V defaultValue) {
this.defaultValue = defaultValue;
}
@Override
public V get(Object k) {
return containsKey(k) ? super.get(k) : defaultValue;
}
}
}

View File

@ -147,12 +147,13 @@ public class BindableArrayAdapter<T> extends ArrayAdapter<T> {
view = convertView;
}
viewBinder.setViewValue(view, getItem(position));
viewBinder.setViewValue(view, position, getItem(position));
return view;
}
}
public interface ViewBinder<T> {
public void setViewValue(final View view, final T data);
public void setViewValue(final View view, final int position, final T
data);
}
}

View File

@ -20,15 +20,34 @@ package com.nononsenseapps.filepicker;
import java.io.File;
public class FilePickerActivity extends AbstractFilePickerActivity<File> {
@Override
protected String getWindowTitle() {
return getResources().getQuantityString(R.plurals.select_dir, 1);
public FilePickerActivity() {
super();
}
@Override
protected AbstractFilePickerFragment getFragment(final String startPath) {
protected String getWindowTitle() {
final int res;
if (onlyDirs) {
res = R.plurals.select_dir;
} else {
res = R.plurals.select_dir_or_file;
}
final int count;
if (allowMultiple) {
count = 99;
} else {
count = 1;
}
return getResources().getQuantityString(res, count);
}
@Override
protected AbstractFilePickerFragment<File> getFragment(final String startPath, final boolean onlyDirs, final boolean allowMultiple) {
AbstractFilePickerFragment fragment = new FilePickerFragment();
fragment.setStartPath(startPath);
fragment.setArgs(startPath, onlyDirs, allowMultiple);
return fragment;
}
}

View File

@ -18,111 +18,25 @@
package com.nononsenseapps.filepicker;
import android.app.Activity;
import android.app.DialogFragment;
import android.app.FragmentManager;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
public class NewFolderFragment extends DialogFragment {
public class NewFolderFragment extends NewItemFragment {
private static final String TAG = "new_folder_fragment";
private String folderName = null;
private View okButton = null;
private OnNewFolderListener listener = null;
public NewFolderFragment() {
}
public static void showDialog(final FragmentManager fm, final OnNewFolderListener listener) {
NewFolderFragment d = new NewFolderFragment();
NewItemFragment d = new NewFolderFragment();
d.setListener(listener);
d.show(fm, TAG);
}
public void setListener(final OnNewFolderListener listener) {
this.listener = listener;
@Override
protected boolean validateName(final String itemName) {
return itemName != null && !itemName.isEmpty()
&& !itemName.contains("/");
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
setRetainInstance(true);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
getDialog().setTitle(R.string.new_folder);
final View view = inflater.inflate(R.layout.dialog_new_folder, null);
okButton = view.findViewById(R.id.button_ok);
okButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
if (listener != null) {
listener.onNewFolder(folderName);
}
dismiss();
}
});
view.findViewById(R.id.button_cancel).setOnClickListener(new View
.OnClickListener() {
@Override
public void onClick(final View v) {
dismiss();
}
});
final EditText editText = (EditText) view.findViewById(R.id.edit_text);
if (folderName == null) {
okButton.setEnabled(false);
} else {
editText.setText(folderName);
validateFolderName();
}
editText.addTextChangedListener
(new TextWatcher() {
@Override
public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) {
}
@Override
public void onTextChanged(final CharSequence s, final int start, final int before, final int count) {
}
@Override
public void afterTextChanged(final Editable s) {
folderName = s.toString();
validateFolderName();
}
});
return view;
}
private void validateFolderName() {
if (okButton != null) {
okButton.setEnabled(folderName != null && !folderName.isEmpty()
&& !folderName.contains("/"));
}
}
public interface OnNewFolderListener {
/**
* Name is validated to be non-null, non-empty and not containing any
* slashes.
*
* @param name The name of the folder the user wishes to create.
*/
public void onNewFolder(final String name);
protected int getDialogTitle() {
return R.string.new_folder;
}
}

View File

@ -0,0 +1,123 @@
/*
* Copyright (c) 2014 Jonas Kalderstam
*
* 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 com.nononsenseapps.filepicker;
import android.app.Activity;
import android.app.DialogFragment;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
public abstract class NewItemFragment extends DialogFragment {
private String itemName = null;
private View okButton = null;
private OnNewFolderListener listener = null;
public NewItemFragment() {
super();
}
public void setListener(final OnNewFolderListener listener) {
this.listener = listener;
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
setRetainInstance(true);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
getDialog().setTitle(getDialogTitle());
final View view = inflater.inflate(R.layout.dialog_new_item, null);
okButton = view.findViewById(R.id.button_ok);
okButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
if (listener != null) {
listener.onNewFolder(itemName);
}
dismiss();
}
});
view.findViewById(R.id.button_cancel).setOnClickListener(new View
.OnClickListener() {
@Override
public void onClick(final View v) {
dismiss();
}
});
final EditText editText = (EditText) view.findViewById(R.id.edit_text);
if (itemName == null) {
okButton.setEnabled(false);
} else {
editText.setText(itemName);
validateItemName();
}
editText.addTextChangedListener
(new TextWatcher() {
@Override
public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) {
}
@Override
public void onTextChanged(final CharSequence s, final int start, final int before, final int count) {
}
@Override
public void afterTextChanged(final Editable s) {
itemName = s.toString();
validateItemName();
}
});
return view;
}
private void validateItemName() {
if (okButton != null) {
okButton.setEnabled(validateName(itemName));
}
}
public interface OnNewFolderListener {
/**
* Name is validated to be non-null, non-empty and not containing any
* slashes.
*
* @param name The name of the folder the user wishes to create.
*/
public void onNewFolder(final String name);
}
protected abstract boolean validateName(final String itemName);
protected abstract int getDialogTitle();
}

View File

@ -29,7 +29,7 @@
android:layout_height="48dp"
android:fontFamily="light"
android:padding="4dp"
android:hint="@string/folder_name"
android:hint="@string/name"
android:singleLine="true"
android:maxLines="1"
android:gravity="center_vertical"

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (c) 2014 Jonas Kalderstam
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/>.
-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="?android:listPreferredItemHeight"
xmlns:android="http://schemas.android.com/apk/res/android"
android:minHeight="?android:listPreferredItemHeight"
android:orientation="horizontal"
android:background="@drawable/selectable_background_filepickertheme">
<ImageView
android:id="@+id/item_icon"
android:src="@drawable/ic_collections_collection_light"
android:visibility="visible"
android:layout_width="wrap_content"
android:layout_height="match_parent"
/>
<CheckedTextView
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
style="?android:textAppearanceLarge"
android:checked="false"
android:checkMark="?android:attr/listChoiceIndicatorMultiple"
android:fontFamily="light"
android:padding="8dp"
android:gravity="center_vertical"
android:textColor="?android:attr/textColorPrimary"
android:id="@android:id/text1"
android:maxLines="1"
android:ellipsize="end"
android:singleLine="true"/>
</LinearLayout>

View File

@ -24,6 +24,7 @@
android:orientation="horizontal"
android:background="@drawable/selectable_background_filepickertheme">
<ImageView
android:id="@+id/item_icon"
android:src="@drawable/ic_collections_collection_light"
@ -41,5 +42,8 @@
android:padding="8dp"
android:gravity="center_vertical"
android:textColor="?android:attr/textColorPrimary"
android:id="@android:id/text1"/>
android:id="@android:id/text1"
android:maxLines="1"
android:ellipsize="end"
android:singleLine="true"/>
</LinearLayout>

View File

@ -19,16 +19,18 @@
<string name="app_name">NoNonsense File Picker</string>
<string name="new_folder">New folder</string>
<string name="create_folder_error">Failed to create folder</string>
<string name="folder_name">Folder name</string>
<string name="name">Name</string>
<string name="go_back">Go back</string>
<plurals
name="select_dir">
<item quantity="one">Select directory</item>
<item quantity="other">Select directories</item>
</plurals>
<plurals
name="select_file">
<item quantity="one">Select file</item>
<item quantity="other">Select files</item>
name="select_dir_or_file">
<item quantity="one">Select directory or file</item>
<item quantity="other">Select directories or files</item>
</plurals>
</resources>