Refactor DirectoryFragment.
Consolidate data logic in a DirectoryFragment.Model class, instead of
having it spread out between the DirectoryFragment and the DocumentsAdapter.
BUG=23518578
Change-Id: I7c2b6c7aa388f5872ac18c320b86445f3c354792
diff --git a/src/com/android/documentsui/DirectoryFragment.java b/src/com/android/documentsui/DirectoryFragment.java
index 240cdda..080ba6a 100644
--- a/src/com/android/documentsui/DirectoryFragment.java
+++ b/src/com/android/documentsui/DirectoryFragment.java
@@ -61,6 +61,7 @@
import android.os.SystemProperties;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
+import android.support.annotation.Nullable;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
@@ -96,7 +97,6 @@
import com.android.documentsui.model.DocumentStack;
import com.android.documentsui.model.RootInfo;
import com.android.internal.util.Preconditions;
-
import com.google.common.collect.Lists;
import java.util.ArrayList;
@@ -129,6 +129,8 @@
private static final String EXTRA_QUERY = "query";
private static final String EXTRA_IGNORE_STATE = "ignoreState";
+ private final Model mModel = new Model();
+
private final Handler mHandler = new Handler(Looper.getMainLooper());
private View mEmptyView;
@@ -147,7 +149,6 @@
private LoaderCallbacks<DirectoryResult> mCallbacks;
private FragmentTuner mFragmentTuner;
private DocumentClipper mClipper;
- private MultiSelectManager mSelectionManager;
// These are lazily initialized.
private LinearLayoutManager mListLayout;
private GridLayoutManager mGridLayout;
@@ -261,7 +262,7 @@
}
// Clear any outstanding selection
- mSelectionManager.clearSelection();
+ mModel.clearSelection();
}
@Override
@@ -290,14 +291,17 @@
}
};
- mSelectionManager = new MultiSelectManager(
+ // TODO: instead of inserting the view into the constructor, extract listener-creation code
+ // and set the listener on the view after the fact. Then the view doesn't need to be passed
+ // into the selection manager which is passed into the model.
+ MultiSelectManager selMgr= new MultiSelectManager(
mRecView,
listener,
state.allowMultiple
? MultiSelectManager.MODE_MULTIPLE
: MultiSelectManager.MODE_SINGLE);
-
- mSelectionManager.addCallback(new SelectionModeListener());
+ selMgr.addCallback(new SelectionModeListener());
+ mModel.setSelectionManager(selMgr);
mType = getArguments().getInt(EXTRA_TYPE);
mStateKey = buildStateKey(root, doc);
@@ -367,7 +371,9 @@
if (!isAdded()) return;
- mAdapter.replaceResult(result);
+ // TODO: make the adapter listen to the model
+ mModel.update(result);
+ mAdapter.update();
// Push latest state up to UI
// TODO: if mode change was racing with us, don't overwrite it
@@ -380,7 +386,7 @@
updateDisplayState();
// When launched into empty recents, show drawer
- if (mType == TYPE_RECENT_OPEN && mAdapter.isEmpty() && !state.stackTouched &&
+ if (mType == TYPE_RECENT_OPEN && mModel.isEmpty() && !state.stackTouched &&
context instanceof DocumentsActivity) {
((DocumentsActivity) context).setRootsDrawerOpen(true);
}
@@ -398,7 +404,9 @@
@Override
public void onLoaderReset(Loader<DirectoryResult> loader) {
- mAdapter.replaceResult(null);
+ // TODO: make the adapter listen to the model.
+ mModel.update(null);
+ mAdapter.update();
}
};
@@ -433,7 +441,7 @@
}
private boolean onSingleTapUp(MotionEvent e) {
- if (Events.isTouchEvent(e) && mSelectionManager.getSelection().isEmpty()) {
+ if (Events.isTouchEvent(e) && mModel.getSelection().isEmpty()) {
int position = getEventAdapterPosition(e);
if (position != RecyclerView.NO_POSITION) {
return handleViewItem(position);
@@ -454,14 +462,14 @@
}
private boolean handleViewItem(int position) {
- final Cursor cursor = mAdapter.getItem(position);
+ final Cursor cursor = mModel.getItem(position);
checkNotNull(cursor, "Cursor cannot be null.");
final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
if (isDocumentEnabled(docMimeType, docFlags)) {
final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
- ((BaseActivity) getActivity()).onDocumentPicked(doc, mAdapter);
- mSelectionManager.clearSelection();
+ ((BaseActivity) getActivity()).onDocumentPicked(doc, mModel);
+ mModel.clearSelection();
return true;
}
return false;
@@ -598,7 +606,7 @@
public boolean onBeforeItemStateChange(int position, boolean selected) {
// Directories and footer items cannot be checked
if (selected) {
- final Cursor cursor = mAdapter.getItem(position);
+ final Cursor cursor = mModel.getItem(position);
checkNotNull(cursor, "Cursor cannot be null.");
final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
@@ -610,7 +618,7 @@
@Override
public void onItemStateChanged(int position, boolean selected) {
- final Cursor cursor = mAdapter.getItem(position);
+ final Cursor cursor = mModel.getItem(position);
checkNotNull(cursor, "Cursor cannot be null.");
final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
@@ -621,7 +629,7 @@
@Override
public void onSelectionChanged() {
- mSelectionManager.getSelection(mSelected);
+ mModel.getSelection(mSelected);
if (mSelected.size() > 0) {
if (DEBUG) Log.d(TAG, "Maybe starting action mode.");
if (mActionMode == null) {
@@ -651,7 +659,7 @@
if (DEBUG) Log.d(TAG, "Handling action mode destroyed.");
mActionMode = null;
// clear selection
- mSelectionManager.clearSelection();
+ mModel.clearSelection();
mSelected.clear();
mNoDeleteCount = 0;
}
@@ -659,8 +667,8 @@
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
mode.getMenuInflater().inflate(R.menu.mode_directory, menu);
- mode.setTitle(TextUtils.formatSelectedCount(mSelectionManager.getSelection().size()));
- return mSelectionManager.getSelection().size() > 0;
+ mode.setTitle(TextUtils.formatSelectedCount(mModel.getSelection().size()));
+ return mModel.getSelection().size() > 0;
}
@Override
@@ -679,8 +687,7 @@
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
- Selection selection = new Selection();
- mSelectionManager.getSelection(selection);
+ Selection selection = mModel.getSelection(new Selection());
final int id = item.getItemId();
if (id == R.id.menu_open) {
@@ -948,50 +955,31 @@
}
}
- private final class DocumentsAdapter extends RecyclerView.Adapter<DocumentHolder>
- implements DocumentContext {
+ private final class DocumentsAdapter extends RecyclerView.Adapter<DocumentHolder> {
private final Context mContext;
private final LayoutInflater mInflater;
// TODO: Bring back support for footers.
private final List<Footer> mFooters = new ArrayList<>();
- private Cursor mCursor;
- private int mCursorCount;
-
public DocumentsAdapter(Context context) {
mContext = context;
mInflater = LayoutInflater.from(context);
}
- public void replaceResult(DirectoryResult result) {
- if (DEBUG) Log.i(TAG, "Updating adapter with new result set.");
- mCursor = result != null ? result.cursor : null;
- mCursorCount = mCursor != null ? mCursor.getCount() : 0;
-
+ public void update() {
mFooters.clear();
-
- final Bundle extras = mCursor != null ? mCursor.getExtras() : null;
- if (extras != null) {
- final String info = extras.getString(DocumentsContract.EXTRA_INFO);
- if (info != null) {
- mFooters.add(new MessageFooter(2, R.drawable.ic_dialog_info, info));
- }
- final String error = extras.getString(DocumentsContract.EXTRA_ERROR);
- if (error != null) {
- mFooters.add(new MessageFooter(3, R.drawable.ic_dialog_alert, error));
- }
- if (extras.getBoolean(DocumentsContract.EXTRA_LOADING, false)) {
- mFooters.add(new LoadingFooter());
- }
+ if (mModel.info != null) {
+ mFooters.add(new MessageFooter(2, R.drawable.ic_dialog_info, mModel.info));
+ }
+ if (mModel.error != null) {
+ mFooters.add(new MessageFooter(3, R.drawable.ic_dialog_alert, mModel.error));
+ }
+ if (mModel.isLoading()) {
+ mFooters.add(new LoadingFooter());
}
- if (result != null && result.exception != null) {
- mFooters.add(new MessageFooter(
- 3, R.drawable.ic_dialog_alert, getString(R.string.query_error)));
- }
-
- if (isEmpty()) {
+ if (mModel.isEmpty()) {
mEmptyView.setVisibility(View.VISIBLE);
} else {
mEmptyView.setVisibility(View.GONE);
@@ -1025,7 +1013,7 @@
final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache(
context, mThumbSize);
- final Cursor cursor = getItem(position);
+ final Cursor cursor = mModel.getItem(position);
checkNotNull(cursor, "Cursor cannot be null.");
final String docAuthority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY);
@@ -1041,7 +1029,7 @@
holder.docId = docId;
final View itemView = holder.view;
- itemView.setActivated(mSelectionManager.getSelection().contains(position));
+ itemView.setActivated(mModel.isSelected(position));
final View line1 = itemView.findViewById(R.id.line1);
final View line2 = itemView.findViewById(R.id.line2);
@@ -1214,44 +1202,21 @@
}
@Override
- public Cursor getCursor() {
- if (Looper.myLooper() != Looper.getMainLooper()) {
- throw new IllegalStateException("Can't call getCursor from non-main thread.");
- }
- return mCursor;
- }
-
- private Cursor getItem(int position) {
- if (position < mCursorCount) {
- mCursor.moveToPosition(position);
- return mCursor;
- }
-
- Log.w(TAG, "Returning null cursor for position: " + position);
- if (DEBUG) Log.d(TAG, "...Adapter size: " + mCursorCount);
- if (DEBUG) Log.d(TAG, "...Footer size: " + mFooters.size());
- return null;
- }
-
- @Override
public int getItemCount() {
- return mCursorCount;
+ return mModel.getItemCount();
// return mCursorCount + mFooters.size();
}
@Override
public int getItemViewType(int position) {
- if (position < mCursorCount) {
+ final int itemCount = mModel.getItemCount();
+ if (position < itemCount) {
return 0;
} else {
- position -= mCursorCount;
+ position -= itemCount;
return mFooters.get(position).getItemViewType();
}
}
-
- private boolean isEmpty() {
- return getItemCount() > 0;
- }
}
private static String formatTime(Context context, long when) {
@@ -1328,27 +1293,6 @@
return MimePredicate.mimeMatches(state.acceptMimes, docMimeType);
}
- private List<DocumentInfo> getSelectedDocuments() {
- Selection sel = mSelectionManager.getSelection(new Selection());
- return getItemsAsDocuments(sel);
- }
-
- private List<DocumentInfo> getItemsAsDocuments(Selection items) {
- if (items == null || items.size() == 0) {
- return new ArrayList<>(0);
- }
-
- final List<DocumentInfo> docs = new ArrayList<>(items.size());
- final int size = items.size();
- for (int i = 0; i < size; i++) {
- final Cursor cursor = mAdapter.getItem(items.get(i));
- checkNotNull(cursor, "Cursor cannot be null.");
- final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
- docs.add(doc);
- }
- return docs;
- }
-
private void copyFromClipboard() {
new AsyncTask<Void, Void, List<DocumentInfo>>() {
@@ -1426,7 +1370,7 @@
}
void copySelectedToClipboard() {
- Selection sel = mSelectionManager.getSelection(new Selection());
+ Selection sel = mModel.getSelection(new Selection());
copySelectionToClipboard(sel);
}
@@ -1475,7 +1419,7 @@
}
void selectAllFiles() {
- boolean changed = mSelectionManager.setItemsSelected(0, mAdapter.getItemCount(), true);
+ boolean changed = mModel.selectAll();
if (changed) {
updateDisplayState();
}
@@ -1521,7 +1465,7 @@
int dstPosition = mRecView.getChildAdapterPosition(v);
DocumentInfo dstDir = null;
if (dstPosition != android.widget.AdapterView.INVALID_POSITION) {
- Cursor dstCursor = mAdapter.getItem(dstPosition);
+ Cursor dstCursor = mModel.getItem(dstPosition);
checkNotNull(dstCursor, "Cursor cannot be null.");
dstDir = DocumentInfo.fromDirectoryCursor(dstCursor);
// TODO: Do not drop into the directory where the documents came from.
@@ -1556,16 +1500,16 @@
return Collections.EMPTY_LIST;
}
- final List<DocumentInfo> selectedDocs = getSelectedDocuments();
+ final List<DocumentInfo> selectedDocs = mModel.getSelectedDocuments();
if (!selectedDocs.isEmpty()) {
- if (!mSelectionManager.getSelection().contains(position)) {
+ if (!mModel.isSelected(position)) {
// There is a selection that does not include the current item, drag nothing.
return Collections.EMPTY_LIST;
}
return selectedDocs;
}
- final Cursor cursor = mAdapter.getItem(position);
+ final Cursor cursor = mModel.getItem(position);
checkNotNull(cursor, "Cursor cannot be null.");
final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
@@ -1671,12 +1615,14 @@
mShadow.setBounds(0, 0, mShadowDimension, mShadowDimension);
}
+ @Override
public void onProvideShadowMetrics(
Point shadowSize, Point shadowTouchPoint) {
shadowSize.set(mShadowDimension, mShadowDimension);
shadowTouchPoint.set(mShadowDimension / 2, mShadowDimension / 2);
}
+ @Override
public void onDrawShadow(Canvas canvas) {
mShadow.draw(canvas);
}
@@ -1706,7 +1652,7 @@
extends AsyncTask<Selection, Void, List<DocumentInfo>> {
@Override
protected final List<DocumentInfo> doInBackground(Selection... selected) {
- return getItemsAsDocuments(selected[0]);
+ return mModel.getDocuments(selected[0]);
}
@Override
@@ -1777,4 +1723,136 @@
new BandSelectManager(fragment.mRecView, fragment.mSelectionManager);
}
}
+
+ /**
+ * The data model for the current loaded directory.
+ */
+ private final class Model implements DocumentContext {
+ private MultiSelectManager mSelectionManager;
+ private int mCursorCount;
+ private boolean mIsLoading;
+ @Nullable private Cursor mCursor;
+ @Nullable private String info;
+ @Nullable private String error;
+
+ /**
+ * Sets the selection manager used by the model.
+ * TODO: the model should instantiate the selection manager. See onActivityCreated.
+ */
+ void setSelectionManager(MultiSelectManager mgr) {
+ mSelectionManager = mgr;
+ }
+
+ /**
+ * Selects all files in the current directory.
+ * @return true if the selection state changed for any files.
+ */
+ boolean selectAll() {
+ return mSelectionManager.setItemsSelected(0, mCursorCount, true);
+ }
+
+ /**
+ * Clones the current selection into the given Selection object.
+ * @param selection
+ * @return The selection that was passed in, for convenience.
+ */
+ Selection getSelection(Selection selection) {
+ return mSelectionManager.getSelection(selection);
+ }
+
+ /**
+ * @return The current selection (the live instance, not a copy).
+ */
+ Selection getSelection() {
+ return mSelectionManager.getSelection();
+ }
+
+ boolean isSelected(int position) {
+ return mSelectionManager.getSelection().contains(position);
+ }
+
+ void clearSelection() {
+ mSelectionManager.clearSelection();
+ }
+
+ void update(DirectoryResult result) {
+ if (DEBUG) Log.i(TAG, "Updating model with new result set.");
+
+ if (result == null) {
+ mCursor = null;
+ mCursorCount = 0;
+ info = null;
+ error = null;
+ mIsLoading = false;
+ return;
+ }
+
+ if (result.exception != null) {
+ Log.e(TAG, "Error while loading directory contents", result.exception);
+ error = getString(R.string.query_error);
+ return;
+ }
+
+ mCursor = result.cursor;
+ mCursorCount = mCursor.getCount();
+
+ final Bundle extras = mCursor.getExtras();
+ if (extras != null) {
+ info = extras.getString(DocumentsContract.EXTRA_INFO);
+ error = extras.getString(DocumentsContract.EXTRA_ERROR);
+ mIsLoading = extras.getBoolean(DocumentsContract.EXTRA_LOADING, false);
+ }
+ }
+
+ private int getItemCount() {
+ return mCursorCount;
+ }
+
+ private Cursor getItem(int position) {
+ if (position >= mCursorCount) {
+ throw new IndexOutOfBoundsException("Attempt to retrieve " + position + " of " +
+ mCursorCount + " items");
+ }
+
+ mCursor.moveToPosition(position);
+ return mCursor;
+ }
+
+ private boolean isEmpty() {
+ return mCursorCount == 0;
+ }
+
+ private boolean isLoading() {
+ return mIsLoading;
+ }
+
+ private List<DocumentInfo> getSelectedDocuments() {
+ Selection sel = getSelection(new Selection());
+ return getDocuments(sel);
+ }
+
+ private List<DocumentInfo> getDocuments(Selection items) {
+ if (items == null || items.size() == 0) {
+ return new ArrayList<>(0);
+ }
+
+ final List<DocumentInfo> docs = new ArrayList<>(items.size());
+ final int size = items.size();
+ for (int i = 0; i < size; i++) {
+ final Cursor cursor = getItem(items.get(i));
+ checkNotNull(cursor, "Cursor cannot be null.");
+ final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
+ docs.add(doc);
+ }
+ return docs;
+ }
+
+ @Override
+ public Cursor getCursor() {
+ if (Looper.myLooper() != Looper.getMainLooper()) {
+ throw new IllegalStateException("Can't call getCursor from non-main thread.");
+ }
+ return mCursor;
+ }
+ }
}
diff --git a/src/com/android/documentsui/MultiSelectManager.java b/src/com/android/documentsui/MultiSelectManager.java
index f87fe4c..27bd2b4 100644
--- a/src/com/android/documentsui/MultiSelectManager.java
+++ b/src/com/android/documentsui/MultiSelectManager.java
@@ -177,13 +177,13 @@
/**
* Returns a Selection object that provides a live view
- * on the current selection. Callers wishing to get
+ * on the current selection.
*
- * @see #getSelectionSnapshot() on how to get a snapshot
+ * @see #getSelection(Selection) on how to get a snapshot
* of the selection that will not reflect future changes
* to selection.
*
- * @return The current seleciton.
+ * @return The current selection.
*/
public Selection getSelection() {
return mSelection;