diff --git a/library/src/main/java/com/nononsenseapps/filepicker/AbstractFilePickerActivity.java b/library/src/main/java/com/nononsenseapps/filepicker/AbstractFilePickerActivity.java index 7c3ca25..4f42697 100644 --- a/library/src/main/java/com/nononsenseapps/filepicker/AbstractFilePickerActivity.java +++ b/library/src/main/java/com/nononsenseapps/filepicker/AbstractFilePickerActivity.java @@ -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 */ public abstract class AbstractFilePickerActivity 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 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 extends Activity implements finish(); } + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) @Override public void onFilesPicked(final List 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 paths = new ArrayList(); + for (Uri file : files) { + paths.add(file.toString()); + } + i.putStringArrayListExtra(EXTRA_PATHS, paths); } - i.setClipData(clip); + setResult(Activity.RESULT_OK, i); finish(); } diff --git a/library/src/main/java/com/nononsenseapps/filepicker/AbstractFilePickerFragment.java b/library/src/main/java/com/nononsenseapps/filepicker/AbstractFilePickerFragment.java index 034dc7a..4d27bbc 100644 --- a/library/src/main/java/com/nononsenseapps/filepicker/AbstractFilePickerFragment.java +++ b/library/src/main/java/com/nononsenseapps/filepicker/AbstractFilePickerFragment.java @@ -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 extends ListFragment implements LoaderManager.LoaderCallbacks>, - 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 currentPaths = null; protected boolean onlyDirs = false; protected boolean allowMultiple = false; protected Comparator comparator = null; @@ -58,11 +60,16 @@ public abstract class AbstractFilePickerFragment extends private BindableArrayAdapter adapter; private TextView currentDirView; + private View okButton; + + protected final DefaultHashMap checkedItems; + /** * Mandatory empty constructor for the fragment manager to instantiate the * fragment (e.g. upon screen orientation changes). */ public AbstractFilePickerFragment() { + checkedItems = new DefaultHashMap(false); } /** @@ -100,12 +107,11 @@ public abstract class AbstractFilePickerFragment extends (KEY_START_PATH)) { currentPath = getPath(getArguments().getString(KEY_START_PATH)); } - } else { - currentPath = getRoot(); } - if (currentPaths == null) { - currentPaths = new ArrayList(); + // If still null + if (currentPath == null) { + currentPath = getRoot(); } } @@ -127,6 +133,17 @@ public abstract class AbstractFilePickerFragment 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 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 extends AbstractFilePickerFragment.this); } }); - + currentDirView = (TextView) view.findViewById(R.id.current_dir); // Restore state if (currentPath != null) { @@ -184,7 +203,7 @@ public abstract class AbstractFilePickerFragment extends private List toUri(List files) { ArrayList uris = new ArrayList(); - for (T file: files) { + for (T file : files) { uris.add(toUri(file)); } return uris; @@ -225,21 +244,70 @@ public abstract class AbstractFilePickerFragment 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 getCheckedItems() { + final BindableArrayAdapter adapter = (BindableArrayAdapter) getListAdapter(); + final ArrayList files = new ArrayList(); + 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. + *

+ * 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 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 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 getComparator(); /** - * * @return a ViewBinder to handle list items, or null. */ protected BindableArrayAdapter.ViewBinder getViewBinder() { - return new BindableArrayAdapter.ViewBinder() { - @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() { + + 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 extends final List data) { if (adapter == null) { adapter = new BindableArrayAdapter(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 extends if (comparator == null) { comparator = getComparator(); } + checkedItems.clear(); adapter.addAll(data); adapter.sort(comparator); setListAdapter(adapter); @@ -406,8 +503,20 @@ public abstract class AbstractFilePickerFragment extends */ public interface OnFilePickedListener { public void onFilePicked(Uri file); + public void onFilesPicked(List files); public void onCancelled(); } + + public class DefaultHashMap extends HashMap { + 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; + } + } } diff --git a/library/src/main/java/com/nononsenseapps/filepicker/BindableArrayAdapter.java b/library/src/main/java/com/nononsenseapps/filepicker/BindableArrayAdapter.java index d2284ef..7099150 100644 --- a/library/src/main/java/com/nononsenseapps/filepicker/BindableArrayAdapter.java +++ b/library/src/main/java/com/nononsenseapps/filepicker/BindableArrayAdapter.java @@ -147,12 +147,13 @@ public class BindableArrayAdapter extends ArrayAdapter { view = convertView; } - viewBinder.setViewValue(view, getItem(position)); + viewBinder.setViewValue(view, position, getItem(position)); return view; } } public interface ViewBinder { - public void setViewValue(final View view, final T data); + public void setViewValue(final View view, final int position, final T + data); } } diff --git a/library/src/main/java/com/nononsenseapps/filepicker/FilePickerActivity.java b/library/src/main/java/com/nononsenseapps/filepicker/FilePickerActivity.java index d119435..c421d3c 100644 --- a/library/src/main/java/com/nononsenseapps/filepicker/FilePickerActivity.java +++ b/library/src/main/java/com/nononsenseapps/filepicker/FilePickerActivity.java @@ -20,15 +20,34 @@ package com.nononsenseapps.filepicker; import java.io.File; public class FilePickerActivity extends AbstractFilePickerActivity { - @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 getFragment(final String startPath, final boolean onlyDirs, final boolean allowMultiple) { AbstractFilePickerFragment fragment = new FilePickerFragment(); - fragment.setStartPath(startPath); + fragment.setArgs(startPath, onlyDirs, allowMultiple); return fragment; } } diff --git a/library/src/main/java/com/nononsenseapps/filepicker/NewFolderFragment.java b/library/src/main/java/com/nononsenseapps/filepicker/NewFolderFragment.java index 28a57ce..5673aa9 100644 --- a/library/src/main/java/com/nononsenseapps/filepicker/NewFolderFragment.java +++ b/library/src/main/java/com/nononsenseapps/filepicker/NewFolderFragment.java @@ -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; } } diff --git a/library/src/main/java/com/nononsenseapps/filepicker/NewItemFragment.java b/library/src/main/java/com/nononsenseapps/filepicker/NewItemFragment.java new file mode 100644 index 0000000..02afad4 --- /dev/null +++ b/library/src/main/java/com/nononsenseapps/filepicker/NewItemFragment.java @@ -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 . + */ + +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(); +} diff --git a/library/src/main/res/layout/dialog_new_folder.xml b/library/src/main/res/layout/dialog_new_item.xml similarity index 97% rename from library/src/main/res/layout/dialog_new_folder.xml rename to library/src/main/res/layout/dialog_new_item.xml index d7491fd..cc26f4a 100644 --- a/library/src/main/res/layout/dialog_new_folder.xml +++ b/library/src/main/res/layout/dialog_new_item.xml @@ -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" diff --git a/library/src/main/res/layout/filepicker_listitem_checkable.xml b/library/src/main/res/layout/filepicker_listitem_checkable.xml new file mode 100644 index 0000000..eb10d71 --- /dev/null +++ b/library/src/main/res/layout/filepicker_listitem_checkable.xml @@ -0,0 +1,51 @@ + + + + + + + + + + \ No newline at end of file diff --git a/library/src/main/res/layout/filepicker_listitem_dir.xml b/library/src/main/res/layout/filepicker_listitem_dir.xml index 5ea721f..2c8f39a 100644 --- a/library/src/main/res/layout/filepicker_listitem_dir.xml +++ b/library/src/main/res/layout/filepicker_listitem_dir.xml @@ -24,6 +24,7 @@ android:orientation="horizontal" android:background="@drawable/selectable_background_filepickertheme"> + + android:id="@android:id/text1" + android:maxLines="1" + android:ellipsize="end" + android:singleLine="true"/> \ No newline at end of file diff --git a/library/src/main/res/values/strings.xml b/library/src/main/res/values/strings.xml index b6ea539..f749486 100644 --- a/library/src/main/res/values/strings.xml +++ b/library/src/main/res/values/strings.xml @@ -19,16 +19,18 @@ NoNonsense File Picker New folder Failed to create folder - Folder name + Name Go back + Select directory Select directories + - Select file - Select files + name="select_dir_or_file"> + Select directory or file + Select directories or files