package org.openintents.filemanager.lists;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.test.espresso.IdlingResource;
import android.support.v4.app.ListFragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ViewFlipper;
import org.openintents.filemanager.FileHolderListAdapter;
import org.openintents.filemanager.FileManagerApplication;
import org.openintents.filemanager.R;
import org.openintents.filemanager.compatibility.ActionbarRefreshHelper;
import org.openintents.filemanager.files.DirectoryContents;
import org.openintents.filemanager.files.DirectoryScanner;
import org.openintents.filemanager.files.FileHolder;
import org.openintents.filemanager.util.CopyHelper;
import org.openintents.filemanager.util.MimeTypes;
import org.openintents.intents.FileManagerIntents;
import java.io.File;
import java.util.ArrayList;
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.support.v4.content.ContextCompat.checkSelfPermission;
/**
* A {@link ListFragment} that displays the contents of a directory.
* <p>
* Clicks do nothing.
* </p>
* <p>
* Refreshes OnSharedPreferenceChange
* </p>
*
* @author George Venios
*/
public abstract class FileListFragment extends ListFragment {
private static final String INSTANCE_STATE_PATH = "path";
private static final String INSTANCE_STATE_FILES = "files";
private static final int REQUEST_CODE_STORAGE_PERMISSION = 1;
File mPreviousDirectory = null;
// Not an anonymous inner class because of:
// http://stackoverflow.com/questions/2542938/sharedpreferences-onsharedpreferencechangelistener-not-being-called-consistently
private OnSharedPreferenceChangeListener preferenceListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(
SharedPreferences sharedPreferences, String key) {
// We only care for list-altering preferences. This could be
// dangerous though,
// as later contributors might not see this, and have their settings
// not work in realtime.
// Therefore this is commented out, since it's not likely the
// refresh is THAT heavy.
// *****************
// if (PreferenceActivity.PREFS_DISPLAYHIDDENFILES.equals(key)
// || PreferenceActivity.PREFS_SORTBY.equals(key)
// || PreferenceActivity.PREFS_ASCENDING.equals(key))
// Prevent NullPointerException caused from this getting called
// after we have finish()ed the activity.
if (getActivity() != null)
refresh();
}
};
protected FileHolderListAdapter mAdapter;
protected DirectoryScanner mScanner;
protected ArrayList<FileHolder> mFiles = new ArrayList<>();
private String mPath;
private String mFilename;
private ViewFlipper mFlipper;
private File mCurrentDirectory;
private View mClipboardInfo;
private TextView mClipboardContent;
private TextView mClipboardAction;
private IdlingResource.ResourceCallback resourceCallback;
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(INSTANCE_STATE_PATH, mPath);
outState.putParcelableArrayList(INSTANCE_STATE_FILES, mFiles);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.filelist, null);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
// Set auto refresh on preference change.
PreferenceManager.getDefaultSharedPreferences(getActivity())
.registerOnSharedPreferenceChangeListener(preferenceListener);
// Set list properties
getListView().setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) {
startUpdatingFileIcons();
} else
stopUpdatingFileIcons();
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
}
});
getListView().requestFocus();
getListView().requestFocusFromTouch();
// Init flipper
mFlipper = (ViewFlipper) view.findViewById(R.id.flipper);
mClipboardInfo = view.findViewById(R.id.clipboard_info);
mClipboardContent = (TextView) view.findViewById(R.id.clipboard_content);
mClipboardAction = (TextView) view.findViewById(R.id.clipboard_action);
mClipboardAction.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
((FileManagerApplication) getActivity().getApplication()).getCopyHelper().clear();
updateClipboardInfo();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
ActionbarRefreshHelper.activity_invalidateOptionsMenu(getActivity());
}
});
// Get arguments
if (savedInstanceState == null) {
mPath = getArguments().getString(FileManagerIntents.EXTRA_DIR_PATH);
mFilename = getArguments().getString(
FileManagerIntents.EXTRA_FILENAME);
} else {
mPath = savedInstanceState.getString(INSTANCE_STATE_PATH);
mFiles = savedInstanceState
.getParcelableArrayList(INSTANCE_STATE_FILES);
}
pathCheckAndFix();
renewScanner();
mAdapter = new FileHolderListAdapter(mFiles, getActivity());
setListAdapter(mAdapter);
if (hasPermissions()) {
mScanner.start();
} else {
requestPermissions();
}
}
private void startUpdatingFileIcons() {
mAdapter.startProcessingThumbnailLoaderQueue();
}
private void stopUpdatingFileIcons() {
mAdapter.stopProcessingThumbnailLoaderQueue();
}
public boolean isLoading() {
return mFlipper.getDisplayedChild() == 0;
}
@Override
public void onDestroy() {
mScanner.cancel();
super.onDestroy();
}
/**
* Reloads {@link #mPath}'s contents.
*/
public void refresh() {
if (hasPermissions()) {
// Cancel and GC previous scanner so that it doesn't load on top of the
// new list.
// Race condition seen if a long list is requested, and a short list is
// requested before the long one loads.
mScanner.cancel();
mScanner = null;
// Indicate loading and start scanning.
setLoading(true);
renewScanner().start();
} else {
requestPermissions();
}
}
private boolean hasPermissions() {
return checkSelfPermission(getActivity(), WRITE_EXTERNAL_STORAGE) == PERMISSION_GRANTED;
}
private void requestPermissions() {
setLoading(true);
requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, REQUEST_CODE_STORAGE_PERMISSION);
}
/**
* Switch to permission request mode.
*/
private void showPermissionDenied() {
setLoading(false);
Toast.makeText(getActivity(), R.string.details_permissions, Toast.LENGTH_SHORT).show();
}
/**
* Make the UI indicate loading.
*/
private void setLoading(boolean show) {
mFlipper.setDisplayedChild(show ? 0 : 1);
onLoadingChanged(show);
}
public void setResourceCallback(IdlingResource.ResourceCallback resourceCallback) {
this.resourceCallback = resourceCallback;
}
protected void selectInList(File selectFile) {
String filename = selectFile.getName();
int count = mAdapter.getCount();
for (int i = 0; i < count; i++) {
FileHolder it = (FileHolder) mAdapter.getItem(i);
if (it.getName().equals(filename)) {
getListView().setSelection(i);
break;
}
}
}
/**
* Recreates the {@link #mScanner} using the previously set arguments and
* {@link #mPath}.
*
* @return {@link #mScanner} for convenience.
*/
protected DirectoryScanner renewScanner() {
String filetypeFilter = getArguments().getString(
FileManagerIntents.EXTRA_FILTER_FILETYPE);
String mimetypeFilter = getArguments().getString(
FileManagerIntents.EXTRA_FILTER_MIMETYPE);
boolean writeableOnly = getArguments().getBoolean(
FileManagerIntents.EXTRA_WRITEABLE_ONLY);
boolean directoriesOnly = getArguments().getBoolean(
FileManagerIntents.EXTRA_DIRECTORIES_ONLY);
mScanner = new DirectoryScanner(new File(mPath), getActivity(),
new FileListMessageHandler(),
MimeTypes.getInstance(),
filetypeFilter == null ? "" : filetypeFilter,
mimetypeFilter == null ? "" : mimetypeFilter, writeableOnly,
directoriesOnly);
return mScanner;
}
private class FileListMessageHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case DirectoryScanner.MESSAGE_SHOW_DIRECTORY_CONTENTS:
if (getActivity() == null) {
return;
}
DirectoryContents c = (DirectoryContents) msg.obj;
mFiles.clear();
mFiles.addAll(c.listSdCard);
mFiles.addAll(c.listDir);
mFiles.addAll(c.listFile);
mAdapter.notifyDataSetChanged();
if (mPreviousDirectory != null) {
selectInList(mPreviousDirectory);
} else {
// Reset list position.
if (!mFiles.isEmpty())
getListView().setSelection(0);
}
setLoading(false);
updateClipboardInfo();
if (resourceCallback != null) {
resourceCallback.onTransitionToIdle();
}
break;
case DirectoryScanner.MESSAGE_SET_PROGRESS:
// ignore
break;
}
}
}
public void updateClipboardInfo() {
CopyHelper copyHelper = ((FileManagerApplication) getActivity().getApplication()).getCopyHelper();
if (copyHelper.canPaste()) {
mClipboardInfo.setVisibility(View.VISIBLE);
int count = copyHelper.getItemsCount();
if (CopyHelper.Operation.COPY.equals(copyHelper.getOperationType())) {
mClipboardContent.setText(getResources().getQuantityString(R.plurals.clipboard_info_items_to_copy, count, count));
mClipboardAction.setText(getString(R.string.clipboard_dismiss));
} else if (CopyHelper.Operation.CUT.equals(copyHelper.getOperationType())) {
mClipboardContent.setText(getResources().getQuantityString(R.plurals.clipboard_info_items_to_move, count, count));
mClipboardAction.setText(getString(R.string.clipboard_undo));
}
} else {
mClipboardInfo.setVisibility(View.GONE);
}
}
/**
* Used to inform subclasses about loading state changing. Can be used to
* make the ui indicate the loading state of the fragment.
*
* @param loading If the list started or stopped loading.
*/
protected void onLoadingChanged(boolean loading) {
}
/**
* @return The currently displayed directory's absolute path.
*/
public final String getPath() {
return mPath;
}
/**
* This will be ignored if path doesn't pass check as valid.
*
* @param dir The path to set.
*/
public final void setPath(File dir) {
if (dir.exists() && dir.isDirectory()) {
mPreviousDirectory = mCurrentDirectory;
mCurrentDirectory = dir;
mPath = dir.getAbsolutePath();
}
}
private void pathCheckAndFix() {
File file = new File(mPath);
// Sanity check that the path (coming from extras_dir_path) is indeed a
// directory
if (!file.isDirectory() && file.getParentFile() != null) {
// remember the filename for picking.
mFilename = file.getName();
mPath = file.getParentFile().getAbsolutePath();
}
}
public String getFilename() {
return mFilename;
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
switch (requestCode) {
case REQUEST_CODE_STORAGE_PERMISSION:
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0 && grantResults[0] == PERMISSION_GRANTED) {
refresh();
} else {
showPermissionDenied();
}
break;
}
}
}