package net.rdrei.android.dirchooser;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.DialogFragment;
import android.content.DialogInterface;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.os.Bundle;
import android.os.Environment;
import android.os.FileObserver;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import com.gu.option.Option;
import com.gu.option.UnitFunction;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Activities that contain this fragment must implement the
* {@link OnFragmentInteractionListener} interface
* to handle interaction events.
* Use the {@link DirectoryChooserFragment#newInstance} factory method to
* create an instance of this fragment.
*/
public class DirectoryChooserFragment extends DialogFragment {
public static final String KEY_CURRENT_DIRECTORY = "CURRENT_DIRECTORY";
private static final String ARG_CONFIG = "CONFIG";
private static final String TAG = DirectoryChooserFragment.class.getSimpleName();
private String mNewDirectoryName;
private String mInitialDirectory;
private Option<OnFragmentInteractionListener> mListener = Option.none();
private Button mBtnConfirm;
private Button mBtnCancel;
private ImageButton mBtnNavUp;
private ImageButton mBtnCreateFolder;
private TextView mTxtvSelectedFolder;
private ListView mListDirectories;
private ArrayAdapter<String> mListDirectoriesAdapter;
private List<String> mFilenames;
/**
* The directory that is currently being shown.
*/
private File mSelectedDir;
private File[] mFilesInDir;
private FileObserver mFileObserver;
private DirectoryChooserConfig mConfig;
public DirectoryChooserFragment() {
// Required empty public constructor
}
/**
* To create the config, make use of the provided
* {@link DirectoryChooserConfig#builder()}.
*
* @return A new instance of DirectoryChooserFragment.
*/
public static DirectoryChooserFragment newInstance(@NonNull final DirectoryChooserConfig config) {
final DirectoryChooserFragment fragment = new DirectoryChooserFragment();
final Bundle args = new Bundle();
args.putParcelable(ARG_CONFIG, config);
fragment.setArguments(args);
return fragment;
}
@Override
public void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState);
if (mSelectedDir != null) {
outState.putString(KEY_CURRENT_DIRECTORY, mSelectedDir.getAbsolutePath());
}
}
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() == null) {
throw new IllegalArgumentException(
"You must create DirectoryChooserFragment via newInstance().");
}
mConfig = getArguments().getParcelable(ARG_CONFIG);
if (mConfig == null) {
throw new NullPointerException("No ARG_CONFIG provided for DirectoryChooserFragment " +
"creation.");
}
mNewDirectoryName = mConfig.newDirectoryName();
mInitialDirectory = mConfig.initialDirectory();
if (savedInstanceState != null) {
mInitialDirectory = savedInstanceState.getString(KEY_CURRENT_DIRECTORY);
}
if (getShowsDialog()) {
setStyle(DialogFragment.STYLE_NO_TITLE, 0);
} else {
setHasOptionsMenu(true);
}
if (!mConfig.allowNewDirectoryNameModification() && TextUtils.isEmpty(mNewDirectoryName)) {
throw new IllegalArgumentException("New directory name must have a strictly positive " +
"length (not zero) when user is not allowed to modify it.");
}
}
@Override
public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
final Bundle savedInstanceState) {
assert getActivity() != null;
final View view = inflater.inflate(R.layout.directory_chooser, container, false);
mBtnConfirm = (Button) view.findViewById(R.id.btnConfirm);
mBtnCancel = (Button) view.findViewById(R.id.btnCancel);
mBtnNavUp = (ImageButton) view.findViewById(R.id.btnNavUp);
mBtnCreateFolder = (ImageButton) view.findViewById(R.id.btnCreateFolder);
mTxtvSelectedFolder = (TextView) view.findViewById(R.id.txtvSelectedFolder);
mListDirectories = (ListView) view.findViewById(R.id.directoryList);
mBtnConfirm.setOnClickListener(new OnClickListener() {
@Override
public void onClick(final View v) {
if (isValidFile(mSelectedDir)) {
returnSelectedFolder();
}
}
});
mBtnCancel.setOnClickListener(new OnClickListener() {
@Override
public void onClick(final View v) {
mListener.foreach(new UnitFunction<OnFragmentInteractionListener>() {
@Override
public void apply(final OnFragmentInteractionListener listener) {
listener.onCancelChooser();
}
});
}
});
mListDirectories.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(final AdapterView<?> parent, final View view,
final int position, final long id) {
debug("Selected index: %d", position);
if (mFilesInDir != null && position >= 0
&& position < mFilesInDir.length) {
changeDirectory(mFilesInDir[position]);
}
}
});
mBtnNavUp.setOnClickListener(new OnClickListener() {
@Override
public void onClick(final View v) {
final File parent;
if (mSelectedDir != null
&& (parent = mSelectedDir.getParentFile()) != null) {
changeDirectory(parent);
}
}
});
mBtnCreateFolder.setOnClickListener(new OnClickListener() {
@Override
public void onClick(final View v) {
openNewFolderDialog();
}
});
if (!getShowsDialog()) {
mBtnCreateFolder.setVisibility(View.GONE);
}
adjustResourceLightness();
mFilenames = new ArrayList<>();
mListDirectoriesAdapter = new ArrayAdapter<>(getActivity(),
android.R.layout.simple_list_item_1, mFilenames);
mListDirectories.setAdapter(mListDirectoriesAdapter);
final File initialDir;
if (!TextUtils.isEmpty(mInitialDirectory) && isValidFile(new File(mInitialDirectory))) {
initialDir = new File(mInitialDirectory);
} else {
initialDir = Environment.getExternalStorageDirectory();
}
changeDirectory(initialDir);
return view;
}
private void adjustResourceLightness() {
// change up button to light version if using dark theme
int color = 0xFFFFFF;
final Resources.Theme theme = getActivity().getTheme();
if (theme != null) {
final TypedArray backgroundAttributes = theme.obtainStyledAttributes(
new int[]{android.R.attr.colorBackground});
if (backgroundAttributes != null) {
color = backgroundAttributes.getColor(0, 0xFFFFFF);
backgroundAttributes.recycle();
}
}
// convert to greyscale and check if < 128
if (color != 0xFFFFFF && 0.21 * Color.red(color) +
0.72 * Color.green(color) +
0.07 * Color.blue(color) < 128) {
mBtnNavUp.setImageResource(R.drawable.navigation_up_light);
mBtnCreateFolder.setImageResource(R.drawable.ic_action_create_light);
}
}
@Override
public void onAttach(final Activity activity) {
super.onAttach(activity);
try {
mListener = Option.some((OnFragmentInteractionListener) activity);
} catch (final ClassCastException ignore) {
}
}
@Override
public void onDetach() {
super.onDetach();
mListener = null;
}
@Override
public void onPause() {
super.onPause();
if (mFileObserver != null) {
mFileObserver.stopWatching();
}
}
@Override
public void onResume() {
super.onResume();
if (mFileObserver != null) {
mFileObserver.startWatching();
}
}
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
inflater.inflate(R.menu.directory_chooser, menu);
final MenuItem menuItem = menu.findItem(R.id.new_folder_item);
if (menuItem == null) {
return;
}
menuItem.setVisible(isValidFile(mSelectedDir) && mNewDirectoryName != null);
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
final int itemId = item.getItemId();
if (itemId == R.id.new_folder_item) {
openNewFolderDialog();
return true;
}
return super.onOptionsItemSelected(item);
}
/**
* Shows a confirmation dialog that asks the user if he wants to create a
* new folder. User can modify provided name, if it was not disallowed.
*/
private void openNewFolderDialog() {
@SuppressLint("InflateParams")
final View dialogView = getActivity().getLayoutInflater().inflate(
R.layout.dialog_new_folder, null);
final TextView msgView = (TextView) dialogView.findViewById(R.id.msgText);
final EditText editText = (EditText) dialogView.findViewById(R.id.editText);
editText.setText(mNewDirectoryName);
msgView.setText(getString(R.string.create_folder_msg, mNewDirectoryName));
final AlertDialog alertDialog = new AlertDialog.Builder(getActivity())
.setTitle(R.string.create_folder_label)
.setView(dialogView)
.setNegativeButton(R.string.cancel_label,
new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int which) {
dialog.dismiss();
}
})
.setPositiveButton(R.string.confirm_label,
new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int which) {
dialog.dismiss();
mNewDirectoryName = editText.getText().toString();
final int msg = createFolder();
Toast.makeText(getActivity(), msg, Toast.LENGTH_SHORT).show();
}
})
.show();
alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(editText.getText().length() != 0);
editText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(final CharSequence charSequence, final int i, final int i2, final int i3) {
}
@Override
public void onTextChanged(final CharSequence charSequence, final int i, final int i2, final int i3) {
final boolean textNotEmpty = charSequence.length() != 0;
alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(textNotEmpty);
msgView.setText(getString(R.string.create_folder_msg, charSequence.toString()));
}
@Override
public void afterTextChanged(final Editable editable) {
}
});
editText.setVisibility(mConfig.allowNewDirectoryNameModification()
? View.VISIBLE : View.GONE);
}
private static void debug(final String message, final Object... args) {
Log.d(TAG, String.format(message, args));
}
/**
* Change the directory that is currently being displayed.
*
* @param dir The file the activity should switch to. This File must be
* non-null and a directory, otherwise the displayed directory
* will not be changed
*/
private void changeDirectory(final File dir) {
if (dir == null) {
debug("Could not change folder: dir was null");
} else if (!dir.isDirectory()) {
debug("Could not change folder: dir is no directory");
} else {
final File[] contents = dir.listFiles();
if (contents != null) {
int numDirectories = 0;
for (final File f : contents) {
if (f.isDirectory()) {
numDirectories++;
}
}
mFilesInDir = new File[numDirectories];
mFilenames.clear();
for (int i = 0, counter = 0; i < numDirectories; counter++) {
if (contents[counter].isDirectory()) {
mFilesInDir[i] = contents[counter];
mFilenames.add(contents[counter].getName());
i++;
}
}
Arrays.sort(mFilesInDir);
Collections.sort(mFilenames);
mSelectedDir = dir;
mTxtvSelectedFolder.setText(dir.getAbsolutePath());
mListDirectoriesAdapter.notifyDataSetChanged();
mFileObserver = createFileObserver(dir.getAbsolutePath());
mFileObserver.startWatching();
debug("Changed directory to %s", dir.getAbsolutePath());
} else {
debug("Could not change folder: contents of dir were null");
}
}
refreshButtonState();
}
/**
* Changes the state of the buttons depending on the currently selected file
* or folder.
*/
private void refreshButtonState() {
final Activity activity = getActivity();
if (activity != null && mSelectedDir != null) {
mBtnConfirm.setEnabled(isValidFile(mSelectedDir));
getActivity().invalidateOptionsMenu();
}
}
/**
* Refresh the contents of the directory that is currently shown.
*/
private void refreshDirectory() {
if (mSelectedDir != null) {
changeDirectory(mSelectedDir);
}
}
/**
* Sets up a FileObserver to watch the current directory.
*/
private FileObserver createFileObserver(final String path) {
return new FileObserver(path, FileObserver.CREATE | FileObserver.DELETE
| FileObserver.MOVED_FROM | FileObserver.MOVED_TO) {
@Override
public void onEvent(final int event, final String path) {
debug("FileObserver received event %d", event);
final Activity activity = getActivity();
if (activity != null) {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
refreshDirectory();
}
});
}
}
};
}
/**
* Returns the selected folder as a result to the activity the fragment's attached to. The
* selected folder can also be null.
*/
private void returnSelectedFolder() {
if (mSelectedDir != null) {
debug("Returning %s as result", mSelectedDir.getAbsolutePath());
mListener.foreach(new UnitFunction<OnFragmentInteractionListener>() {
@Override
public void apply(final OnFragmentInteractionListener f) {
f.onSelectDirectory(mSelectedDir.getAbsolutePath());
}
});
} else {
mListener.foreach(new UnitFunction<OnFragmentInteractionListener>() {
@Override
public void apply(final OnFragmentInteractionListener f) {
f.onCancelChooser();
}
});
}
}
/**
* Creates a new folder in the current directory with the name
* CREATE_DIRECTORY_NAME.
*/
private int createFolder() {
if (mNewDirectoryName != null && mSelectedDir != null
&& mSelectedDir.canWrite()) {
final File newDir = new File(mSelectedDir, mNewDirectoryName);
if (newDir.exists()) {
return R.string.create_folder_error_already_exists;
} else {
final boolean result = newDir.mkdir();
if (result) {
return R.string.create_folder_success;
} else {
return R.string.create_folder_error;
}
}
} else if (mSelectedDir != null && !mSelectedDir.canWrite()) {
return R.string.create_folder_error_no_write_access;
} else {
return R.string.create_folder_error;
}
}
/**
* Returns true if the selected file or directory would be valid selection.
*/
private boolean isValidFile(final File file) {
return (file != null && file.isDirectory() && file.canRead() &&
(mConfig.allowNewDirectoryNameModification() || file.canWrite()));
}
@Nullable
public OnFragmentInteractionListener getDirectoryChooserListener() {
return mListener.get();
}
public void setDirectoryChooserListener(@Nullable final OnFragmentInteractionListener listener) {
mListener = Option.option(listener);
}
/**
* 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.
* <p/>
* See the Android Training lesson <a href=
* "http://developer.android.com/training/basics/fragments/communicating.html"
* >Communicating with Other Fragments</a> for more information.
*/
public interface OnFragmentInteractionListener {
/**
* Triggered when the user successfully selected their destination directory.
*/
void onSelectDirectory(@NonNull String path);
/**
* Advices the activity to remove the current fragment.
*/
void onCancelChooser();
}
}