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