/* * Copyright 2016 Gleb Godonoga. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.andrada.sitracker.ui.fragment; import android.app.Activity; import android.app.DialogFragment; import android.content.Context; import android.graphics.Color; import android.os.Bundle; import android.os.Environment; import android.os.FileObserver; import android.support.annotation.NonNull; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.ListView; import com.afollestad.materialdialogs.DialogAction; import com.afollestad.materialdialogs.MaterialDialog; import com.andrada.sitracker.R; import com.andrada.sitracker.ui.components.FileFolderView; import com.andrada.sitracker.ui.components.FileFolderView_; import com.andrada.sitracker.util.LogUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.FileFilter; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import static com.andrada.sitracker.util.LogUtils.LOGD; /** * Activities that contain this fragment must implement the * {@link DirectoryChooserFragment.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_IS_DIRECTORY_CHOOSER = "DIRECTORY_CHOOSER_SETTING"; private static final String ARG_INITIAL_DIRECTORY = "INITIAL_DIRECTORY"; private static final String TAG = LogUtils.makeLogTag(DirectoryChooserFragment.class); private String mInitialDirectory; private Boolean mIsDirectoryChooser = true; private WeakReference<OnFragmentInteractionListener> mListener; private FolderArrayAdapter mListDirectoriesAdapter; private ArrayList<FileDescriptor> mFilenames; /** * The directory that is currently being shown. */ @Nullable private File mSelectedDir; @Nullable private File mSelectedFile; private ArrayList<File> mFilesInDir; @Nullable private FileObserver mFileObserver; MaterialDialog mCurrentDialog; public DirectoryChooserFragment() { // Required empty public constructor } /** * Use this factory method to create a new instance of * this fragment using the provided parameters. * * @param initialDirectory Optional argument to define the path of the directory * that will be shown first. * If it is not sent or if path denotes a non readable/writable * directory * or it is not a directory, it defaults to * {@link android.os.Environment#getExternalStorageDirectory()} * @return A new instance of fragment DirectoryChooserFragment. */ @NotNull public static DirectoryChooserFragment newInstance( final String initialDirectory, final Boolean isDirectoryChooser, final OnFragmentInteractionListener listener) { DirectoryChooserFragment fragment = new DirectoryChooserFragment(); Bundle args = new Bundle(); args.putString(ARG_INITIAL_DIRECTORY, initialDirectory); args.putBoolean(ARG_IS_DIRECTORY_CHOOSER, isDirectoryChooser); fragment.setArguments(args); fragment.mListener = new WeakReference<>(listener); return fragment; } @Override public void onSaveInstanceState(@NotNull Bundle outState) { super.onSaveInstanceState(outState); if (mSelectedDir != null) { outState.putString(KEY_CURRENT_DIRECTORY, mSelectedDir.getAbsolutePath()); } } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() == null) { throw new IllegalArgumentException( "You must create DirectoryChooserFragment via newInstance()."); } else { mInitialDirectory = getArguments().getString(ARG_INITIAL_DIRECTORY); mIsDirectoryChooser = getArguments().getBoolean(ARG_IS_DIRECTORY_CHOOSER, true); } if (savedInstanceState != null) { mInitialDirectory = savedInstanceState.getString(KEY_CURRENT_DIRECTORY); } if (this.getShowsDialog()) { setStyle(DialogFragment.STYLE_NO_TITLE, 0); } } @Override public View onCreateView(@NotNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { assert getActivity() != null; MaterialDialog.Builder dialogBuilder = new MaterialDialog.Builder(getActivity()) .title("") .negativeText(R.string.fp_cancel_label) .negativeColor(Color.GRAY) .onNegative(new MaterialDialog.SingleButtonCallback() { @Override public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { if (mListener.get() != null) mListener.get().onCancelChooser(); } }); if (mIsDirectoryChooser) { dialogBuilder .positiveText(R.string.fp_confirm_label) .onPositive(new MaterialDialog.SingleButtonCallback() { @Override public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { if (isValidFile(mSelectedDir) && mIsDirectoryChooser) { returnSelectedFolder(); } } }); } if (getShowsDialog()) { dialogBuilder .neutralText(R.string.fp_new_folder) .neutralColor(Color.GRAY) .onNeutral(new MaterialDialog.SingleButtonCallback() { @Override public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { openNewFolderDialog(); } }); } mFilenames = new ArrayList<>(); mListDirectoriesAdapter = new FolderArrayAdapter(getActivity(), android.R.layout.simple_list_item_1, mFilenames); dialogBuilder.customView(R.layout.directory_chooser, false); mCurrentDialog = dialogBuilder.build(); ListView lv = (ListView) mCurrentDialog.findViewById(R.id.directoryList); lv.setAdapter(mListDirectoriesAdapter); lv.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int which, long id) { if (mFilesInDir != null && which >= 0 && which < mFilesInDir.size()) { changeDirectory(mFilesInDir.get(which)); } } }); final File initialDir; if (mInitialDirectory != null && isValidFile(new File(mInitialDirectory))) { initialDir = new File(mInitialDirectory); } else { initialDir = Environment.getExternalStorageDirectory(); } changeDirectory(initialDir); ViewGroup parent = ((ViewGroup) mCurrentDialog.getView().getParent()); if (parent != null) { parent.removeView(mCurrentDialog.getView()); } return mCurrentDialog.getView(); } public void setListener(@NotNull OnFragmentInteractionListener mListener) { this.mListener = new WeakReference<>(mListener); } @Override public void onPause() { super.onPause(); if (mFileObserver != null) { mFileObserver.stopWatching(); } } @Override public void onResume() { super.onResume(); if (mFileObserver != null) { mFileObserver.startWatching(); } } /** * Shows a confirmation dialog that asks the user if he wants to create a * new folder. */ private void openNewFolderDialog() { new MaterialDialog.Builder(getActivity()) .title(R.string.fp_create_folder_label) .inputRangeRes(2, 20, R.color.md_edittext_error) .input(R.string.fp_create_folder_msg, R.string.fp_default_folder_name, new MaterialDialog.InputCallback() { @Override public void onInput(@NonNull MaterialDialog dialog, CharSequence input) { int msg = createFolder(input); if (msg != R.string.fp_create_folder_success) { new MaterialDialog.Builder(getActivity()).content(msg) .positiveText(android.R.string.ok) .show(); } } }) .positiveText(R.string.fp_confirm_label) .negativeText(R.string.fp_cancel_label) .onNegative(new MaterialDialog.SingleButtonCallback() { @Override public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { dialog.dismiss(); } }).show(); } /** * 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(@Nullable File dir) { if (dir == null) { debug("Could not change folder: dir was null"); } else if (!dir.isDirectory() && !mIsDirectoryChooser) { debug("Could not change folder: dir is no directory, selecting file"); //Selecting file mSelectedFile = dir; mCurrentDialog.setTitle(dir.getAbsolutePath()); if (isValidFile(mSelectedFile)) { returnSelectedFile(); } } else { File[] contents = dir.listFiles(new FileFilter() { @Override public boolean accept(@NotNull File file) { return !file.isHidden(); } }); if (contents != null) { int numDirectories = 0; if (mIsDirectoryChooser) { for (File f : contents) { if (f.isDirectory()) { numDirectories++; } } } else { numDirectories = contents.length; } mFilesInDir = new ArrayList<>(); mFilenames.clear(); for (int i = 0, counter = 0; i < numDirectories; counter++) { if ((mIsDirectoryChooser && contents[counter].isDirectory()) || !mIsDirectoryChooser) { mFilesInDir.add(contents[counter]); mFilenames.add(new FileDescriptor(contents[counter].getName(), contents[counter].isDirectory())); i++; } } Collections.sort(mFilesInDir, new Comparator<File>() { @Override public int compare(@NotNull File aThis, @Nullable File aThat) { final int BEFORE = -1; final int EQUAL = 0; final int AFTER = 1; if (aThis == aThat) { return EQUAL; } if (aThat == null) { return BEFORE; } //Compare by type first if (aThis.isDirectory() && !aThat.isDirectory()) { return BEFORE; } if (!aThis.isDirectory() && aThat.isDirectory()) { return AFTER; } //Compare by filename int comparison = aThis.getName().compareTo(aThat.getName()); if (comparison != EQUAL) { return comparison; } return EQUAL; } }); Collections.sort(mFilenames); mSelectedDir = dir; mSelectedFile = null; if (mSelectedDir.getParentFile() != null) { //Insert back navigation FileDescriptor bDescriptor = new FileDescriptor("..", true); mFilenames.add(0, bDescriptor); mFilesInDir.add(0, dir.getParentFile()); } mCurrentDialog.setTitle(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"); } } } private void debug(@NotNull String message, Object... args) { LOGD(TAG, String.format(message, args)); } /** * 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. */ @NotNull private FileObserver createFileObserver(String path) { return new FileObserver(path, FileObserver.CREATE | FileObserver.DELETE | FileObserver.MOVED_FROM | FileObserver.MOVED_TO) { @Override public void onEvent(int event, 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 (mListener.get() == null) return; if (mSelectedDir != null && mIsDirectoryChooser) { debug("Returning %s as result", mSelectedDir.getAbsolutePath()); mListener.get().onSelectDirectory(mSelectedDir.getAbsolutePath()); } else { mListener.get().onCancelChooser(); } } private void returnSelectedFile() { if (mListener.get() == null) return; if (mSelectedFile != null && !mIsDirectoryChooser) { mListener.get().onSelectDirectory(mSelectedFile.getAbsolutePath()); } else { mListener.get().onCancelChooser(); } } /** * Creates a new folder in the current directory with the name * CREATE_DIRECTORY_NAME. * * @param input */ private int createFolder(CharSequence input) { if (input != null && mSelectedDir != null && mSelectedDir.canWrite()) { File newDir = new File(mSelectedDir, input.toString()); if (!newDir.exists()) { boolean result = newDir.mkdir(); if (result) { return R.string.fp_create_folder_success; } else { return R.string.fp_create_folder_error; } } else { return R.string.fp_create_folder_error_already_exists; } } else if (mSelectedDir != null && !mSelectedDir.canWrite()) { return R.string.fp_create_folder_error_no_write_access; } else { return R.string.fp_create_folder_error; } } /** * Returns true if the selected file or directory would be valid selection. */ private boolean isValidFile(@Nullable File file) { if (mIsDirectoryChooser) { return (file != null && file.isDirectory() && file.canRead() && file .canWrite()); } else { return (file != null && !file.isDirectory() && file.canRead()); } } /** * 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(String path); /** * Advices the activity to remove the current fragment. */ void onCancelChooser(); } private class FileDescriptor implements Comparable<FileDescriptor> { private String fileName; private Boolean isDirectory; public FileDescriptor(String filename, Boolean isDirectory) { fileName = filename; this.isDirectory = isDirectory; } public String getFileName() { return fileName; } public Boolean getIsDirectory() { return isDirectory; } @Override public int compareTo(@Nullable FileDescriptor that) { final int BEFORE = -1; final int EQUAL = 0; final int AFTER = 1; if (this == that) { return EQUAL; } if (that == null) { return BEFORE; } //Compare by type first if (this.isDirectory && !that.isDirectory) { return BEFORE; } if (!this.isDirectory && that.isDirectory) { return AFTER; } //Compare by filename int comparison = this.fileName.compareTo(that.fileName); if (comparison != EQUAL) { return comparison; } return EQUAL; } } private class FolderArrayAdapter extends ArrayAdapter<FileDescriptor> { public FolderArrayAdapter(@NotNull Context context, int resource, List<FileDescriptor> objects) { super(context, resource, objects); } @Nullable @Override public View getView(int position, @Nullable View convertView, ViewGroup parent) { FileFolderView folderItemView; if (convertView == null) { folderItemView = FileFolderView_.build(getContext()); } else { folderItemView = (FileFolderView) convertView; } if (position < getCount()) { FileDescriptor descriptor = getItem(position); folderItemView.bind(descriptor.getFileName(), descriptor.getIsDirectory()); } return folderItemView; } } }