diff --git a/core/java/android/util/SparseBooleanArray.java b/core/java/android/util/SparseBooleanArray.java
index e293b1f652cd1..8fd8226da6ac3 100644
--- a/core/java/android/util/SparseBooleanArray.java
+++ b/core/java/android/util/SparseBooleanArray.java
@@ -185,6 +185,11 @@ public class SparseBooleanArray implements Cloneable {
mValues[index] = value;
}
+ /** @hide */
+ public void setKeyAt(int index, int key) {
+ mKeys[index] = key;
+ }
+
/**
* Returns the index for which {@link #keyAt} would return the
* specified key, or a negative number if the specified
diff --git a/packages/DocumentsUI/Android.mk b/packages/DocumentsUI/Android.mk
index 67d8ab6d8ce25..3430bb4797172 100644
--- a/packages/DocumentsUI/Android.mk
+++ b/packages/DocumentsUI/Android.mk
@@ -5,7 +5,9 @@ LOCAL_MODULE_TAGS := optional
LOCAL_SRC_FILES := $(call all-java-files-under, src)
-LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4 guava
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4 \
+ android-support-v7-recyclerview \
+ guava
LOCAL_PACKAGE_NAME := DocumentsUI
LOCAL_CERTIFICATE := platform
diff --git a/packages/DocumentsUI/res/layout/fragment_directory.xml b/packages/DocumentsUI/res/layout/fragment_directory.xml
index 471783928f9a6..7ae7eeacf764b 100644
--- a/packages/DocumentsUI/res/layout/fragment_directory.xml
+++ b/packages/DocumentsUI/res/layout/fragment_directory.xml
@@ -28,24 +28,23 @@
android:visibility="gone"
style="@android:style/TextAppearance.Material.Subhead" />
+
-
+ android:drawSelectorOnTop="true" />
diff --git a/packages/DocumentsUI/res/layout/item_doc_grid.xml b/packages/DocumentsUI/res/layout/item_doc_grid.xml
index d62d050b91457..5d8a834ad310f 100644
--- a/packages/DocumentsUI/res/layout/item_doc_grid.xml
+++ b/packages/DocumentsUI/res/layout/item_doc_grid.xml
@@ -17,7 +17,8 @@
+ android:background="@color/item_doc_grid_background"
+ android:padding="@dimen/grid_item_padding">
mCallbacks;
-
+ private static final int LOADER_ID = 42;
+ private static final boolean DEBUG = false;
private static final boolean DEBUG_ENABLE_DND = false;
private static final String EXTRA_TYPE = "type";
@@ -149,11 +130,28 @@ public class DirectoryFragment extends Fragment {
private static final String EXTRA_QUERY = "query";
private static final String EXTRA_IGNORE_STATE = "ignoreState";
- private final int mLoaderId = 42;
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
+ private View mEmptyView;
+ private RecyclerView mRecView;
+
+ private int mType = TYPE_NORMAL;
+ private String mStateKey;
+
+ private int mLastMode = MODE_UNKNOWN;
+ private int mLastSortOrder = SORT_ORDER_UNKNOWN;
+ private boolean mLastShowSize;
+ private boolean mHideGridTitles;
+ private boolean mSvelteRecents;
+ private Point mThumbSize;
+ private DocumentsAdapter mAdapter;
+ private LoaderCallbacks mCallbacks;
private FragmentTuner mFragmentTuner;
private DocumentClipper mClipper;
- private final Handler mHandler = new Handler(Looper.getMainLooper());
+ private MultiSelectManager mSelectionManager;
+ // These are lazily initialized.
+ private LayoutManager mListLayout;
+ private LayoutManager mGridLayout;
public static void showNormal(FragmentManager fm, RootInfo root, DocumentInfo doc, int anim) {
show(fm, TYPE_NORMAL, root, doc, null, anim);
@@ -218,29 +216,18 @@ public class DirectoryFragment extends Fragment {
mEmptyView = view.findViewById(android.R.id.empty);
- mListView = (ListView) view.findViewById(R.id.list);
- mListView.setOnItemClickListener(mItemListener);
- mListView.setMultiChoiceModeListener(mMultiListener);
- mListView.setRecyclerListener(mRecycleListener);
-
- // Indent our list divider to align with text
- final Drawable divider = mListView.getDivider();
- final boolean insetLeft = res.getBoolean(R.bool.list_divider_inset_left);
- final int insetSize = res.getDimensionPixelSize(R.dimen.list_divider_inset);
- if (insetLeft) {
- mListView.setDivider(new InsetDrawable(divider, insetSize, 0, 0, 0));
- } else {
- mListView.setDivider(new InsetDrawable(divider, 0, 0, insetSize, 0));
- }
-
- mGridView = (GridView) view.findViewById(R.id.grid);
- mGridView.setOnItemClickListener(mItemListener);
- mGridView.setMultiChoiceModeListener(mMultiListener);
- mGridView.setRecyclerListener(mRecycleListener);
+ mRecView = (RecyclerView) view.findViewById(R.id.recyclerView);
+ mRecView.setRecyclerListener(
+ new RecyclerListener() {
+ @Override
+ public void onViewRecycled(ViewHolder holder) {
+ cancelThumbnailTask(holder.itemView);
+ }
+ });
+ // TODO: Add a divider between views (which might use RecyclerView.ItemDecoration).
if (DEBUG_ENABLE_DND) {
- setupDragAndDropOnDirectoryView(mListView);
- setupDragAndDropOnDirectoryView(mGridView);
+ setupDragAndDropOnDirectoryView(mRecView);
}
return view;
@@ -251,16 +238,14 @@ public class DirectoryFragment extends Fragment {
super.onDestroyView();
// Cancel any outstanding thumbnail requests
- final ViewGroup target = (mListView.getAdapter() != null) ? mListView : mGridView;
- final int count = target.getChildCount();
+ final int count = mRecView.getChildCount();
for (int i = 0; i < count; i++) {
- final View view = target.getChildAt(i);
- mRecycleListener.onMovedToScrapHeap(view);
+ final View view = mRecView.getChildAt(i);
+ cancelThumbnailTask(view);
}
- // Tear down any selection in progress
- mListView.setChoiceMode(AbsListView.CHOICE_MODE_NONE);
- mGridView.setChoiceMode(AbsListView.CHOICE_MODE_NONE);
+ // Clear any outstanding selection
+ mSelectionManager.clearSelection();
}
@Override
@@ -273,7 +258,20 @@ public class DirectoryFragment extends Fragment {
final RootInfo root = getArguments().getParcelable(EXTRA_ROOT);
final DocumentInfo doc = getArguments().getParcelable(EXTRA_DOC);
- mAdapter = new DocumentsAdapter();
+ mAdapter = new DocumentsAdapter(context);
+ mRecView.setAdapter(mAdapter);
+
+ GestureDetector.SimpleOnGestureListener listener =
+ new GestureDetector.SimpleOnGestureListener() {
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ return DirectoryFragment.this.onSingleTapUp(e);
+ }
+ };
+
+ mSelectionManager = new MultiSelectManager(mRecView, listener);
+ mSelectionManager.addCallback(new SelectionModeListener());
+
mType = getArguments().getInt(EXTRA_TYPE);
mStateKey = buildStateKey(root, doc);
@@ -342,7 +340,7 @@ public class DirectoryFragment extends Fragment {
if (!isAdded()) return;
- mAdapter.swapResult(result);
+ mAdapter.replaceResult(result);
// Push latest state up to UI
// TODO: if mode change was racing with us, don't overwrite it
@@ -365,8 +363,7 @@ public class DirectoryFragment extends Fragment {
if (container != null && !getArguments().getBoolean(EXTRA_IGNORE_STATE, false)) {
getView().restoreHierarchyState(container);
} else if (mLastSortOrder != state.derivedSortOrder) {
- mListView.smoothScrollToPosition(0);
- mGridView.smoothScrollToPosition(0);
+ mRecView.smoothScrollToPosition(0);
}
mLastSortOrder = state.derivedSortOrder;
@@ -374,12 +371,12 @@ public class DirectoryFragment extends Fragment {
@Override
public void onLoaderReset(Loader loader) {
- mAdapter.swapResult(null);
+ mAdapter.replaceResult(null);
}
};
// Kick off loader at least once
- getLoaderManager().restartLoader(mLoaderId, null, mCallbacks);
+ getLoaderManager().restartLoader(LOADER_ID, null, mCallbacks);
updateDisplayState();
}
@@ -402,6 +399,29 @@ public class DirectoryFragment extends Fragment {
data.getIntExtra(CopyService.EXTRA_TRANSFER_MODE, CopyService.TRANSFER_MODE_NONE));
}
+ private int getEventAdapterPosition(MotionEvent e) {
+ View view = mRecView.findChildViewUnder(e.getX(), e.getY());
+ return view != null ? mRecView.getChildAdapterPosition(view) : RecyclerView.NO_POSITION;
+ }
+
+ private boolean onSingleTapUp(MotionEvent e) {
+ int position = getEventAdapterPosition(e);
+
+ if (position != RecyclerView.NO_POSITION) {
+ final Cursor cursor = mAdapter.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);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
@Override
public void onStop() {
super.onStop();
@@ -426,7 +446,7 @@ public class DirectoryFragment extends Fragment {
public void onUserSortOrderChanged() {
// Sort order change always triggers reload; we'll trigger state change
// on the flip side.
- getLoaderManager().restartLoader(mLoaderId, null, mCallbacks);
+ getLoaderManager().restartLoader(LOADER_ID, null, mCallbacks);
}
public void onUserModeChanged() {
@@ -466,8 +486,7 @@ public class DirectoryFragment extends Fragment {
mLastMode = state.derivedMode;
mLastShowSize = state.showSize;
- mListView.setVisibility(state.derivedMode == MODE_LIST ? View.VISIBLE : View.GONE);
- mGridView.setVisibility(state.derivedMode == MODE_GRID ? View.VISIBLE : View.GONE);
+ updateLayout(state.derivedMode);
final int choiceMode;
if (state.allowMultiple) {
@@ -476,51 +495,104 @@ public class DirectoryFragment extends Fragment {
choiceMode = ListView.CHOICE_MODE_NONE;
}
+ final int thumbSize = getResources().getDimensionPixelSize(R.dimen.icon_size);
+ mThumbSize = new Point(thumbSize, thumbSize);
+ mRecView.setAdapter(mAdapter);
+ }
+
+ /**
+ * Returns a {@code LayoutManager} for {@code mode}, lazily initializing
+ * classes as needed.
+ */
+ private void updateLayout(int mode) {
final int thumbSize;
- if (state.derivedMode == MODE_GRID) {
- thumbSize = getResources().getDimensionPixelSize(R.dimen.grid_width);
- mListView.setAdapter(null);
- mListView.setChoiceMode(ListView.CHOICE_MODE_NONE);
- mGridView.setAdapter(mAdapter);
- mGridView.setColumnWidth(getResources().getDimensionPixelSize(R.dimen.grid_width));
- mGridView.setNumColumns(GridView.AUTO_FIT);
- mGridView.setChoiceMode(choiceMode);
- mCurrentView = mGridView;
- } else if (state.derivedMode == MODE_LIST) {
- thumbSize = getResources().getDimensionPixelSize(R.dimen.icon_size);
- mGridView.setAdapter(null);
- mGridView.setChoiceMode(ListView.CHOICE_MODE_NONE);
- mListView.setAdapter(mAdapter);
- mListView.setChoiceMode(choiceMode);
- mCurrentView = mListView;
- } else {
- throw new IllegalStateException("Unknown state " + state.derivedMode);
+
+ final LayoutManager layout;
+ switch (mode) {
+ case MODE_GRID:
+ if (mGridLayout == null) {
+ // TODO: Determine appropriate column count.
+ mGridLayout = new GridLayoutManager(getContext(), 4);
+ }
+ thumbSize = getResources().getDimensionPixelSize(R.dimen.grid_width);
+ layout = mGridLayout;
+ break;
+ case MODE_LIST:
+ if (mListLayout == null) {
+ mListLayout = new LinearLayoutManager(getContext());
+ }
+ thumbSize = getResources().getDimensionPixelSize(R.dimen.icon_size);
+ layout = mListLayout;
+ break;
+ case MODE_UNKNOWN:
+ default:
+ throw new IllegalArgumentException("Unsupported layout mode: " + mode);
}
+ mRecView.setLayoutManager(layout);
+ // setting layout manager automatically invalidates existing ViewHolders.
mThumbSize = new Point(thumbSize, thumbSize);
}
- private OnItemClickListener mItemListener = new OnItemClickListener() {
+ /**
+ * Manages the integration between our ActionMode and MultiSelectManager, initiating
+ * ActionMode when there is a selection, canceling it when there is no selection,
+ * and clearing selection when action mode is explicitly exited by the user.
+ */
+ private final class SelectionModeListener
+ implements MultiSelectManager.Callback, ActionMode.Callback {
+
+ private Selection mSelected = new Selection();
+ private ActionMode mActionMode;
+
@Override
- public void onItemClick(AdapterView> parent, View view, int position, long id) {
- final Cursor cursor = mAdapter.getItem(position);
- if (cursor != null) {
+ public boolean onBeforeItemStateChange(int position, boolean selected) {
+ // Directories and footer items cannot be checked
+ if (selected) {
+ final Cursor cursor = mAdapter.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);
+ return isDocumentEnabled(docMimeType, docFlags);
+ }
+ return true;
+ }
+
+ @Override
+ public void onItemStateChanged(int position, boolean selected) {
+ mSelectionManager.getSelection(mSelected);
+ if (mSelected.size() == 0) {
+ if (DEBUG) Log.d(TAG, "Finishing action mode.");
+ if (mActionMode != null) {
+ mActionMode.finish();
+ }
+ } else {
+ if (DEBUG) Log.d(TAG, "Maybe starting action mode.");
+ if (mActionMode == null) {
+ if (DEBUG) Log.d(TAG, "Yeah. Starting action mode.");
+ mActionMode = getActivity().startActionMode(this);
}
}
- }
- };
- private MultiChoiceModeListener mMultiListener = new MultiChoiceModeListener() {
+ if (mActionMode != null) {
+ mActionMode.setTitle(TextUtils.formatSelectedCount(mSelected.size()));
+ }
+ }
+
+ // Called when the user exits the action mode
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ if (DEBUG) Log.d(TAG, "Handling action mode destroyed.");
+ mActionMode = null;
+ // clear selection
+ mSelectionManager.clearSelection();
+ }
+
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
mode.getMenuInflater().inflate(R.menu.mode_directory, menu);
- mode.setTitle(TextUtils.formatSelectedCount(mCurrentView.getCheckedItemCount()));
- return true;
+ mode.setTitle(TextUtils.formatSelectedCount(mSelectionManager.getSelection().size()));
+ return mSelectionManager.getSelection().size() > 0;
}
@Override
@@ -532,41 +604,39 @@ public class DirectoryFragment extends Fragment {
}
@Override
- public boolean onActionItemClicked(final ActionMode mode, MenuItem item) {
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
- // ListView returns a reference to its internal selection container,
- // which will get cleared when we cancel action mode. So we
- // make a defensive clone here.
- final SparseBooleanArray selected = mCurrentView.getCheckedItemPositions().clone();
+ Selection selection = new Selection();
+ mSelectionManager.getSelection(selection);
final int id = item.getItemId();
if (id == R.id.menu_open) {
- openDocuments(selected);
+ openDocuments(selection);
mode.finish();
return true;
} else if (id == R.id.menu_share) {
- shareDocuments(selected);
+ shareDocuments(selection);
mode.finish();
return true;
} else if (id == R.id.menu_delete) {
- deleteDocuments(selected);
+ deleteDocuments(selection);
mode.finish();
return true;
} else if (id == R.id.menu_copy_to) {
- transferDocuments(selected, CopyService.TRANSFER_MODE_COPY);
+ transferDocuments(selection, CopyService.TRANSFER_MODE_COPY);
mode.finish();
return true;
} else if (id == R.id.menu_move_to) {
- transferDocuments(selected, CopyService.TRANSFER_MODE_MOVE);
+ transferDocuments(selection, CopyService.TRANSFER_MODE_MOVE);
mode.finish();
return true;
} else if (id == R.id.menu_copy_to_clipboard) {
- copySelectionToClipboard(selected);
+ copySelectionToClipboard(selection);
mode.finish();
return true;
@@ -578,50 +648,20 @@ public class DirectoryFragment extends Fragment {
return false;
}
}
+ }
- @Override
- public void onDestroyActionMode(ActionMode mode) {
- // ignored
- }
-
- @Override
- public void onItemCheckedStateChanged(
- ActionMode mode, int position, long id, boolean checked) {
- if (checked) {
- // Directories and footer items cannot be checked
- boolean valid = false;
-
- final Cursor cursor = mAdapter.getItem(position);
- if (cursor != null) {
- final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
- final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
- valid = isDocumentEnabled(docMimeType, docFlags);
- }
-
- if (!valid) {
- mCurrentView.setItemChecked(position, false);
- }
- }
-
- mode.setTitle(TextUtils.formatSelectedCount(mCurrentView.getCheckedItemCount()));
- }
- };
-
- private RecyclerListener mRecycleListener = new RecyclerListener() {
- @Override
- public void onMovedToScrapHeap(View view) {
- final ImageView iconThumb = (ImageView) view.findViewById(R.id.icon_thumb);
- if (iconThumb != null) {
- final ThumbnailAsyncTask oldTask = (ThumbnailAsyncTask) iconThumb.getTag();
- if (oldTask != null) {
- oldTask.preempt();
- iconThumb.setTag(null);
- }
+ private static void cancelThumbnailTask(View view) {
+ final ImageView iconThumb = (ImageView) view.findViewById(R.id.icon_thumb);
+ if (iconThumb != null) {
+ final ThumbnailAsyncTask oldTask = (ThumbnailAsyncTask) iconThumb.getTag();
+ if (oldTask != null) {
+ oldTask.preempt();
+ iconThumb.setTag(null);
}
}
- };
+ }
- private void openDocuments(final SparseBooleanArray selected) {
+ private void openDocuments(final Selection selected) {
new GetDocumentsTask() {
@Override
void onDocumentsReady(List docs) {
@@ -631,7 +671,7 @@ public class DirectoryFragment extends Fragment {
}.execute(selected);
}
- private void shareDocuments(final SparseBooleanArray selected) {
+ private void shareDocuments(final Selection selected) {
new GetDocumentsTask() {
@Override
void onDocumentsReady(List docs) {
@@ -679,7 +719,7 @@ public class DirectoryFragment extends Fragment {
}.execute(selected);
}
- private void deleteDocuments(final SparseBooleanArray selected) {
+ private void deleteDocuments(final Selection selected) {
final Context context = getActivity();
final ContentResolver resolver = context.getContentResolver();
@@ -717,7 +757,7 @@ public class DirectoryFragment extends Fragment {
}.execute(selected);
}
- private void transferDocuments(final SparseBooleanArray selected, final int mode) {
+ private void transferDocuments(final Selection selected, final int mode) {
// Pop up a dialog to pick a destination. This is inadequate but works for now.
// TODO: Implement a picker that is to spec.
final Intent intent = new Intent(
@@ -822,13 +862,36 @@ public class DirectoryFragment extends Fragment {
}
}
- private class DocumentsAdapter extends BaseAdapter {
+ // Provide a reference to the views for each data item
+ // Complex data items may need more than one view per item, and
+ // you provide access to all the views for a data item in a view holder
+ private static final class DocumentHolder extends RecyclerView.ViewHolder {
+ // each data item is just a string in this case
+ public View view;
+ public String docId; // The stable document id.
+ public DocumentHolder(View view) {
+ super(view);
+ this.view = view;
+ }
+ }
+
+ private final class DocumentsAdapter extends RecyclerView.Adapter {
+
+ private final Context mContext;
+ private final LayoutInflater mInflater;
+ // TODO: Bring back support for footers.
+ private final List