/* * Copyright (c) 2015 Jonas Kalderstam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.nononsenseapps.filepicker; import android.app.Activity; import android.net.Uri; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.app.LoaderManager; import android.support.v4.content.Loader; import android.support.v7.app.AppCompatActivity; import android.support.v7.util.SortedList; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.Toolbar; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.CheckBox; import android.widget.TextView; import android.widget.Toast; import java.util.ArrayList; import java.util.HashSet; import java.util.List; /** * A fragment representing a list of Files. *

*

* Activities containing this fragment MUST implement the {@link * OnFilePickedListener} * interface. */ public abstract class AbstractFilePickerFragment extends Fragment implements LoaderManager.LoaderCallbacks>, NewItemFragment.OnNewFolderListener, LogicHandler { // The different preset modes of operation. This impacts the behaviour // and possible actions in the UI. public static final int MODE_FILE = 0; protected int mode = MODE_FILE; public static final int MODE_DIR = 1; public static final int MODE_FILE_AND_DIR = 2; // Where to display on open. public static final String KEY_START_PATH = "KEY_START_PATH"; // See MODE_XXX constants above for possible values public static final String KEY_MODE = "KEY_MODE"; // If it should be possible to create directories. Only valid with MODE_DIR public static final String KEY_ALLOW_DIR_CREATE = "KEY_ALLOW_DIR_CREATE"; // Allow multiple items to be selected. public static final String KEY_ALLOW_MULTIPLE = "KEY_ALLOW_MULTIPLE"; // Used for saving state. protected static final String KEY_CURRENT_PATH = "KEY_CURRENT PATH"; protected final HashSet checkedItems; protected final HashSet checkedVisibleViewHolders; protected T currentPath = null; protected boolean allowCreateDir = false; protected boolean allowMultiple = false; private OnFilePickedListener listener; private FileItemAdapter mAdapter = null; private TextView currentDirView; private Toolbar mToolbar; private RecyclerView mRecyclerView; private LinearLayoutManager mLayoutManager; private SortedList mFiles = null; protected FileItemAdapter getAdapter() { return mAdapter; } protected FileItemAdapter getDummyAdapter() { return new FileItemAdapter<>(this); } /** * Mandatory empty constructor for the fragment manager to instantiate the * fragment (e.g. upon screen orientation changes). */ public AbstractFilePickerFragment() { checkedItems = new HashSet<>(); checkedVisibleViewHolders = new HashSet<>(); } /** * Set before making the fragment visible. * * @param startPath * @param mode * @param allowMultiple * @param allowDirCreate */ public void setArgs(final String startPath, final int mode, final boolean allowMultiple, final boolean allowDirCreate) { Bundle b = new Bundle(); if (startPath != null) { b.putString(KEY_START_PATH, startPath); } b.putBoolean(KEY_ALLOW_DIR_CREATE, allowDirCreate); b.putBoolean(KEY_ALLOW_MULTIPLE, allowMultiple); b.putInt(KEY_MODE, mode); setArguments(b); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_filepicker, container); mToolbar = (Toolbar) view.findViewById(R.id.picker_toolbar); ((AppCompatActivity) getActivity()).setSupportActionBar(mToolbar); mRecyclerView = (RecyclerView) view.findViewById(android.R.id.list); // improve performance if you know that changes in content // do not change the size of the RecyclerView mRecyclerView.setHasFixedSize(true); // I want some dividers //mRecyclerView.addItemDecoration(new DividerColor // (getActivity(), DividerColor.VERTICAL_LIST, 0, 1)); // use a linear layout manager mLayoutManager = new LinearLayoutManager(getActivity()); mRecyclerView.setLayoutManager(mLayoutManager); // Set adapter mAdapter = new FileItemAdapter<>(this); mRecyclerView.setAdapter(mAdapter); view.findViewById(R.id.button_cancel) .setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View v) { if (listener != null) { listener.onCancelled(); } } }); view.findViewById(R.id.button_ok) .setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View v) { if (listener == null) { return; } // Some invalid cases first if ((allowMultiple || mode == MODE_FILE) && checkedItems.isEmpty()) { Toast.makeText(getActivity(), R.string.select_something_first, Toast.LENGTH_SHORT).show(); return; } if (allowMultiple) { listener.onFilesPicked(toUri(checkedItems)); } else if (mode == MODE_FILE) { listener.onFilePicked(toUri(getFirstCheckedItem())); } else if (mode == MODE_DIR) { listener.onFilePicked(toUri(currentPath)); } else { // single FILE OR DIR if (checkedItems.isEmpty()) { listener.onFilePicked(toUri(currentPath)); } else { listener.onFilePicked(toUri(getFirstCheckedItem())); } } } }); currentDirView = (TextView) view.findViewById(R.id.current_dir); // Restore state if (currentPath != null) { currentDirView.setText(getFullPath(currentPath)); } return view; } public T getFirstCheckedItem() { for (T file : checkedItems) { return file; } return null; } protected List toUri(Iterable files) { ArrayList uris = new ArrayList(); for (T file : files) { uris.add(toUri(file)); } return uris; } public boolean isCheckable(final T data) { final boolean checkable; if (isDir(data)) { checkable = ((mode == MODE_DIR && allowMultiple) || (mode == MODE_FILE_AND_DIR && allowMultiple)); } else { // File checkable = (mode != MODE_DIR); } return checkable; } @Override public void onAttach(Activity activity) { super.onAttach(activity); try { listener = (OnFilePickedListener) activity; } catch (ClassCastException e) { throw new ClassCastException(activity.toString() + " must implement OnFilePickedListener"); } } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Retain this fragment across configuration changes. setRetainInstance(true); // Only if we have no state if (currentPath == null) { if (savedInstanceState != null) { mode = savedInstanceState.getInt(KEY_MODE, mode); allowCreateDir = savedInstanceState .getBoolean(KEY_ALLOW_DIR_CREATE, allowCreateDir); allowMultiple = savedInstanceState .getBoolean(KEY_ALLOW_MULTIPLE, allowMultiple); currentPath = getPath(savedInstanceState.getString(KEY_CURRENT_PATH)); } else if (getArguments() != null) { mode = getArguments().getInt(KEY_MODE, mode); allowCreateDir = getArguments() .getBoolean(KEY_ALLOW_DIR_CREATE, allowCreateDir); allowMultiple = getArguments() .getBoolean(KEY_ALLOW_MULTIPLE, allowMultiple); if (getArguments().containsKey(KEY_START_PATH)) { currentPath = getPath(getArguments().getString(KEY_START_PATH)); } } // If still null if (currentPath == null) { currentPath = getRoot(); } } refresh(); setHasOptionsMenu(true); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.picker_actions, menu); MenuItem item = menu.findItem(R.id.action_createdir); item.setVisible(allowCreateDir && (mode == MODE_DIR)); } @Override public boolean onOptionsItemSelected(MenuItem menuItem) { if (R.id.action_createdir == menuItem.getItemId()) { Activity activity = getActivity(); if (activity instanceof AppCompatActivity) { NewFolderFragment.showDialog(((AppCompatActivity) activity).getSupportFragmentManager(), AbstractFilePickerFragment.this); } return true; } else { return false; } } @Override public void onSaveInstanceState(Bundle b) { super.onSaveInstanceState(b); b.putString(KEY_CURRENT_PATH, currentPath.toString()); b.putBoolean(KEY_ALLOW_MULTIPLE, allowMultiple); b.putBoolean(KEY_ALLOW_DIR_CREATE, allowCreateDir); b.putInt(KEY_MODE, mode); } @Override public void onDetach() { super.onDetach(); listener = null; } /** * Refreshes the list. Call this when current path changes. */ protected void refresh() { getLoaderManager() .restartLoader(0, null, AbstractFilePickerFragment.this); } /** * Instantiate and return a new Loader for the given ID. * * @param id The ID whose loader is to be created. * @param args Any arguments supplied by the caller. * @return Return a new Loader instance that is ready to start loading. */ @Override public Loader> onCreateLoader(final int id, final Bundle args) { return getLoader(); } /** * Called when a previously created loader has finished its load. * * @param loader The Loader that has finished. * @param data The data generated by the Loader. */ @Override public void onLoadFinished(final Loader> loader, final SortedList data) { checkedItems.clear(); checkedVisibleViewHolders.clear(); mFiles = data; mAdapter.setList(data); currentDirView.setText(getFullPath(currentPath)); } /** * Called when a previously created loader is being reset, and thus * making its data unavailable. The application should at this point * remove any references it has to the Loader's data. * * @param loader The Loader that is being reset. */ @Override public void onLoaderReset(final Loader> loader) { mAdapter.setList(null); mFiles = null; } /** * This interface must be implemented by activities that contain this * fragment to allow an interaction in this fragment to be communicated * to the activity and potentially other fragments contained in that * activity. *

* See the Android Training lesson Communicating with Other Fragments for more information. */ public interface OnFilePickedListener { public void onFilePicked(Uri file); public void onFilesPicked(List files); public void onCancelled(); } /** * @param position 0 - n, where the header has been subtracted * @param data * @return an integer greater than 0 */ @Override public int getItemViewType(int position, T data) { if (isCheckable(data)) { return LogicHandler.VIEWTYPE_CHECKABLE; } else { return LogicHandler.VIEWTYPE_DIR; } } @Override public void onBindHeaderViewHolder(HeaderViewHolder viewHolder) { viewHolder.text.setText(".."); } /** * @param parent * @param viewType * @return a view holder for a file or directory */ @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View v; switch (viewType) { case LogicHandler.VIEWTYPE_HEADER: v = LayoutInflater.from(getActivity()).inflate(R.layout.filepicker_listitem_dir, parent, false); return new HeaderViewHolder(v); case LogicHandler.VIEWTYPE_CHECKABLE: v = LayoutInflater.from(getActivity()).inflate(R.layout.filepicker_listitem_checkable, parent, false); return new CheckableViewHolder(v); case LogicHandler.VIEWTYPE_DIR: default: v = LayoutInflater.from(getActivity()).inflate(R.layout.filepicker_listitem_dir, parent, false); return new DirViewHolder(v); } } /** * @param vh to bind data from either a file or directory * @param position 0 - n, where the header has been subtracted * @param data */ @Override public void onBindViewHolder(DirViewHolder vh, int position, T data) { vh.file = data; vh.icon.setVisibility(isDir(data) ? View.VISIBLE : View.GONE); vh.text.setText(getName(data)); if (isCheckable(data)) { if (checkedItems.contains(data)) { checkedVisibleViewHolders.add((CheckableViewHolder) vh); ((CheckableViewHolder) vh).checkbox.setChecked(true); } else { checkedVisibleViewHolders.remove(vh); ((CheckableViewHolder) vh).checkbox.setChecked(false); } } } public class HeaderViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { final TextView text; public HeaderViewHolder(View v) { super(v); v.setOnClickListener(this); text = (TextView) v.findViewById(android.R.id.text1); } /** * Called when a view has been clicked. * * @param v The view that was clicked. */ @Override public void onClick(View v) { currentPath = getParent(currentPath); checkedItems.clear(); checkedVisibleViewHolders.clear(); refresh(); } } public class DirViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener { public View icon; public TextView text; public T file; public DirViewHolder(View v) { super(v); v.setOnClickListener(this); v.setOnLongClickListener(this); icon = v.findViewById(R.id.item_icon); text = (TextView) v.findViewById(android.R.id.text1); } /** * Called when a view has been clicked. * * @param v The view that was clicked. */ @Override public void onClick(View v) { if (isDir(file)) { currentPath = file; checkedItems.clear(); checkedVisibleViewHolders.clear(); refresh(); } } /** * Called when a view has been clicked and held. * * @param v The view that was clicked and held. * @return true if the callback consumed the long click, false otherwise. */ @Override public boolean onLongClick(View v) { return false; } } public class CheckableViewHolder extends DirViewHolder { public CheckBox checkbox; public CheckableViewHolder(View v) { super(v); checkbox = (CheckBox) v.findViewById(R.id.checkbox); checkbox.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { onLongClick(v); } }); } /** * Called when a view has been clicked. * * @param v The view that was clicked. */ @Override public void onClick(View v) { if (isDir(file)) { currentPath = file; checkedItems.clear(); checkedVisibleViewHolders.clear(); refresh(); } else { onLongClick(v); } } /** * Called when a view has been clicked and held. * * @param v The view that was clicked and held. * @return true if the callback consumed the long click, false otherwise. */ @Override public boolean onLongClick(View v) { if (checkedItems.contains(file)) { checkbox.setChecked(false); checkedItems.remove(file); checkedVisibleViewHolders.remove(this); } else { if (!allowMultiple) { clearSelections(); } checkbox.setChecked(true); checkedItems.add(file); checkedVisibleViewHolders.add(this); } return true; } } /** * Animate de-selection of visible views and clear * selected set. */ public void clearSelections() { for (CheckableViewHolder vh : checkedVisibleViewHolders) { vh.checkbox.setChecked(false); } checkedVisibleViewHolders.clear(); checkedItems.clear(); } }