/* Copyright (C) 2012 Haowen Ning 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 2 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, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ package org.liberty.android.fantastischmemo.ui; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.SharedPreferences; import android.os.Bundle; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; 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.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import com.google.common.base.Strings; import org.greenrobot.eventbus.Subscribe; import org.liberty.android.fantastischmemo.R; import org.liberty.android.fantastischmemo.common.AMEnv; import org.liberty.android.fantastischmemo.common.AMPrefKeys; import org.liberty.android.fantastischmemo.common.BaseDialogFragment; import org.liberty.android.fantastischmemo.utils.AMFileUtil; import org.liberty.android.fantastischmemo.utils.RecentListUtil; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import javax.inject.Inject; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.functions.Consumer; public class FileBrowserFragment extends BaseDialogFragment { public final static String EXTRA_DEFAULT_ROOT = "default_root"; public final static String EXTRA_FILE_EXTENSIONS = "file_extension"; public final static String EXTRA_DISMISS_ON_SELECT = "dismiss_on_select"; public final static String EXTRA_SHOW_CREATE_DB_BUTTON = "show_create_db_button"; private enum DISPLAYMODE{ABSOLUTE, RELATIVE;} private final DISPLAYMODE displayMode = DISPLAYMODE.RELATIVE; private ArrayList<String> directoryEntries = new ArrayList<String>(); private File currentDirectory = new File("/"); private String defaultRoot; private String[] fileExtensions; private Activity mActivity; private RecyclerView filesListRecyclerView; private FileBrowserAdapter fileListAdapter; private TextView filesListEmptyView; private TextView titleTextView; private boolean dismissOnSelect = false; private boolean showCreateDbButton = false; /* Used when the file is clicked. */ private OnFileClickListener onFileClickListener; private CompositeDisposable disposables; private final static String TAG = FileBrowserFragment.class.getSimpleName(); private final static String UP_ONE_LEVEL_DIR = ".."; private final static String CURRENT_DIR = "."; private SharedPreferences settings; private SharedPreferences.Editor editor; public void setOnFileClickListener(OnFileClickListener listener) { this.onFileClickListener = listener; } @Inject AMFileUtil amFileUtil; @Inject RecentListUtil recentListUtil; public FileBrowserFragment() { } @Override public void onAttach(Context activity) { super.onAttach(activity); mActivity = (Activity) activity; settings = PreferenceManager.getDefaultSharedPreferences(mActivity); editor = settings.edit(); } @Override public void onCreate(Bundle bundle) { super.onCreate(bundle); fragmentComponents().inject(this); disposables = new CompositeDisposable(); Bundle args = this.getArguments(); if(args != null) { defaultRoot = args.getString(EXTRA_DEFAULT_ROOT); String ext = args.getString(EXTRA_FILE_EXTENSIONS); // Default do not dismiss the dialog dismissOnSelect = args.getBoolean(EXTRA_DISMISS_ON_SELECT, false); showCreateDbButton = args.getBoolean(EXTRA_SHOW_CREATE_DB_BUTTON, false); if (ext != null) { fileExtensions = ext.split(","); } else { fileExtensions = new String[]{".db"}; } } else { fileExtensions = new String[]{".db"}; defaultRoot = null; } if(defaultRoot == null){ defaultRoot = settings.getString(AMPrefKeys.SAVED_FILEBROWSER_PATH_KEY, null); // Make sure the path exists. if (!Strings.isNullOrEmpty(defaultRoot) && !new File(defaultRoot).exists()) { defaultRoot = null; } } if (Strings.isNullOrEmpty(defaultRoot)) { File sdPath = new File(AMEnv.DEFAULT_ROOT_PATH); sdPath.mkdir(); currentDirectory = sdPath; } else { currentDirectory = new File(defaultRoot + "/"); } // Should use this to enable menu setHasOptionsMenu(true); } @Override public void onDestroy() { super.onDestroy(); disposables.dispose(); } @Override public void onStart() { super.onStart(); appComponents().eventBus().register(this); } @Override public void onStop() { super.onStop(); appComponents().eventBus().unregister(this); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.file_browser, container, false); filesListRecyclerView = (RecyclerView)v.findViewById(R.id.file_list); filesListRecyclerView.setLayoutManager(new LinearLayoutManager(filesListRecyclerView.getContext())); titleTextView = (TextView) v.findViewById(R.id.file_path_title); filesListEmptyView = (TextView) v.findViewById(R.id.empty_text_view); filesListEmptyView.setText(R.string.directory_empty_text); fileListAdapter = new FileBrowserAdapter(this); filesListRecyclerView.setAdapter(fileListAdapter); return v; } @Override public void onResume() { super.onResume(); browseTo(currentDirectory); } @Subscribe public void onRefreshFileListEvent(RefreshFileListEvent event) { if (event.folderToRefresh != null) { browseTo(event.folderToRefresh); } else if (currentDirectory != null){ browseTo(currentDirectory); } } private void browseTo(final File aDirectory){ if(aDirectory.isDirectory()){ File[] listedFiles = aDirectory.listFiles(); if (listedFiles != null) { titleTextView.setText(aDirectory.getPath()); this.currentDirectory = aDirectory; fill(listedFiles); } else { Toast.makeText(filesListRecyclerView.getContext(), R.string.change_directory_permission_denied_message, Toast.LENGTH_SHORT).show(); } } } private void fill(File[] files){ this.directoryEntries.clear(); if(this.currentDirectory.getParent() != null){ this.directoryEntries.add(UP_ONE_LEVEL_DIR); } switch(this.displayMode){ case ABSOLUTE: for(File file : files){ this.directoryEntries.add(file.getPath()); } break; case RELATIVE: int currentPathStringLength = this.currentDirectory.getAbsolutePath().length(); for(File file: files){ if(file.isDirectory()){ this.directoryEntries.add(file.getAbsolutePath().substring(currentPathStringLength) + "/"); } else{ for(String fileExtension : fileExtensions){ if(file.getName().toLowerCase().endsWith(fileExtension.toLowerCase())){ this.directoryEntries.add(file.getAbsolutePath().substring(currentPathStringLength)); } } } } } if (files.length > 0) { this.filesListEmptyView.setVisibility(View.INVISIBLE); } else { this.filesListEmptyView.setVisibility(View.VISIBLE); } this.fileListAdapter.setItems(directoryEntries); } private void upOneLevel(){ if(this.currentDirectory.getParent() != null){ this.browseTo(this.currentDirectory.getParentFile()); } } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){ super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.file_browser_menu, menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.file_browser_createdb:{ disposables.add(activityComponents().databaseOperationDialogUtil().showCreateDbDialog(currentDirectory.getAbsolutePath()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer<File>() { @Override public void accept(File file) throws Exception { browseTo(file.getParentFile()); } })); return true; } case R.id.file_browser_createdirectory:{ disposables.add(activityComponents().databaseOperationDialogUtil().showCreateFolderDialog(currentDirectory) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer<File>() { @Override public void accept(File file) throws Exception { browseTo(file); } })); return true; } } return false; } private void openFile(final String selectedFileString) { if(selectedFileString.equals(CURRENT_DIR)){ this.browseTo(this.currentDirectory); } else if(selectedFileString.equals(UP_ONE_LEVEL_DIR)) { this.upOneLevel(); } else { File clickedFile = null; switch(this.displayMode){ case RELATIVE: clickedFile = new File(this.currentDirectory.getAbsolutePath() + selectedFileString); break; case ABSOLUTE: clickedFile = new File(selectedFileString); break; } if(clickedFile != null) { try{ if(clickedFile.isDirectory()){ this.browseTo(clickedFile); } else if(clickedFile.isFile()){ /* Save the current path */ editor.putString(AMPrefKeys.SAVED_FILEBROWSER_PATH_KEY, clickedFile.getParent()); editor.commit(); if (onFileClickListener != null) { onFileClickListener.onClick(clickedFile); } else { // Use event bus if the onFileClickListener is not set appComponents().eventBus().post(new FileClickEvent(clickedFile)); } // dismiss on demand if (dismissOnSelect) { dismiss(); } } } catch(Exception e){ new AlertDialog.Builder(mActivity).setMessage(e.toString()).show(); Log.e(TAG, "Error handling click", e); browseTo(new File("/")); } } } } private static class FileBrowserAdapter extends RecyclerView.Adapter<FileBrowserAdapter.ViewHolder> { private final FileBrowserFragment fragment; private final List<String> items = new ArrayList<String>(); public FileBrowserAdapter(FileBrowserFragment fragment) { this.fragment = fragment; } @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { LayoutInflater li = (LayoutInflater)fragment.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); View v = li.inflate(R.layout.filebrowser_item, parent, false); return new ViewHolder(v); } @Override public void onBindViewHolder(ViewHolder holder, final int position) { final String selectedFileName = items.get(position); if (Strings.isNullOrEmpty(selectedFileName)) { return; } String displayFileName = selectedFileName; if(selectedFileName.endsWith("/")){ holder.setImage(R.drawable.dir); displayFileName = displayFileName.substring(0, selectedFileName.length() - 1); } else if(selectedFileName.equals("..")){ holder.setImage(R.drawable.back); } else if(selectedFileName.endsWith(".png") || selectedFileName.endsWith(".jpg") || selectedFileName.endsWith(".tif") || selectedFileName.endsWith(".bmp")){ holder.setImage(R.drawable.picture); } else if(selectedFileName.endsWith(".ogg") || selectedFileName.endsWith(".mp3") || selectedFileName.endsWith(".wav") || selectedFileName.endsWith(".amr")){ holder.setImage(R.drawable.audio); } else if(selectedFileName.endsWith(".txt") || selectedFileName.endsWith(".csv") || selectedFileName.endsWith(".xml")){ holder.setImage(R.drawable.text); } else if(selectedFileName.endsWith(".zip")){ holder.setImage(R.drawable.zip); } else{ holder.setImage(R.drawable.database); } if (displayFileName.charAt(0) == '/'){ displayFileName = displayFileName.substring(1, displayFileName.length()); } holder.setText(displayFileName); // Set click listeners holder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { fragment.openFile(selectedFileName); } }); holder.itemView.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { if(selectedFileName.equals(CURRENT_DIR)){ /* Do nothing */ } else if(selectedFileName.equals(UP_ONE_LEVEL_DIR)){ /* Do nothing */ } else { final File clickedFile; switch(fragment.displayMode){ case RELATIVE: clickedFile = new File(fragment.currentDirectory.getAbsolutePath() + selectedFileName); break; case ABSOLUTE: clickedFile = new File(selectedFileName); break; default: clickedFile = new File(fragment.currentDirectory.getAbsolutePath() + selectedFileName); } if (clickedFile != null){ if(clickedFile.isDirectory()) { fragment.browseTo(clickedFile); } else if (clickedFile.isFile()){ new AlertDialog.Builder(fragment.getContext()) .setTitle(fragment.getContext().getString(R.string.fb_edit_dialog_title)) .setItems(R.array.fb_dialog_list, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { if (which == 0) { fragment.disposables.add(fragment.activityComponents().databaseOperationDialogUtil().showDeleteDbDialog(clickedFile) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer<File>() { @Override public void accept(File file) throws Exception { fragment.browseTo(file.getParentFile()); } })); } else if (which == 1) { fragment.disposables.add(fragment.activityComponents().databaseOperationDialogUtil().showCloneDbDialog(clickedFile) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer<File>() { @Override public void accept(File file) throws Exception { fragment.browseTo(file.getParentFile()); } })); } else if (which == 2) { fragment.disposables.add(fragment.activityComponents().databaseOperationDialogUtil().showRenameDbDialog(clickedFile) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer<File>() { @Override public void accept(File file) throws Exception { fragment.browseTo(file.getParentFile()); } })); } } }) .create() .show(); } } } return true; } }); } @Override public int getItemCount() { return items.size(); } public void setItems(List<String> items) { this.items.clear(); this.items.addAll(items); Collections.sort(this.items, new Comparator<String>() { @Override public int compare(String s1, String s2){ if (s1.equals("..")) { return -1; } else if (s2.equals("..")){ return 1; } else if (s1.endsWith("/") && !s2.endsWith("/")){ return -1; } else if (s2.endsWith("/") && !s1.endsWith("/")){ return 1; } else { return (s1.toLowerCase()).compareTo(s2.toLowerCase()); } } }); this.notifyDataSetChanged(); } public static class ViewHolder extends RecyclerView.ViewHolder { private final TextView textView; private final ImageView imageView; public ViewHolder(View view) { super(view); textView = (TextView)view.findViewById(R.id.file_name); imageView = (ImageView)view.findViewById(R.id.file_icon); } public void setText(String text) { textView.setText(text); } public void setImage(int imageResource) { imageView.setImageResource(imageResource); } } } public interface OnFileClickListener { void onClick(File file); } public static class FileClickEvent { public final File clickedFile; public FileClickEvent(@NonNull File clickedFile) { this.clickedFile = clickedFile; } } public static class RefreshFileListEvent { @Nullable public final File folderToRefresh; public RefreshFileListEvent(@Nullable File folderToRefresh) { if (folderToRefresh != null && folderToRefresh.isDirectory()) { this.folderToRefresh = folderToRefresh; } else { this.folderToRefresh = null; } } } }