Loading packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java +0 −19 Original line number Diff line number Diff line Loading @@ -466,25 +466,6 @@ abstract class BaseActivity extends Activity { } } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { if (DEBUG) Log.d(mTag, "onKeyUp: keycode = " + keyCode); // TODO: Support for RecentsCreateFragment. DirectoryFragment fragment = DirectoryFragment.get(getFragmentManager()); if (fragment != null) { switch (keyCode) { case KeyEvent.KEYCODE_MOVE_HOME: fragment.focusFirstFile(); return true; case KeyEvent.KEYCODE_MOVE_END: fragment.focusLastFile(); return true; } } return super.onKeyUp(keyCode, event); } public void onStackPicked(DocumentStack stack) { try { // Update the restored stack to ensure we have freshest data Loading packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java +39 −104 Original line number Diff line number Diff line Loading @@ -17,7 +17,6 @@ package com.android.documentsui; import static com.android.documentsui.Shared.DEBUG; import static com.android.documentsui.Shared.TAG; import static com.android.documentsui.State.ACTION_BROWSE; import static com.android.documentsui.State.ACTION_CREATE; import static com.android.documentsui.State.ACTION_MANAGE; Loading Loading @@ -79,6 +78,7 @@ import android.util.TypedValue; import android.view.ActionMode; import android.view.DragEvent; import android.view.GestureDetector; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; Loading @@ -97,7 +97,6 @@ import com.android.documentsui.RecentsProvider.StateColumns; import com.android.documentsui.model.DocumentInfo; import com.android.documentsui.model.DocumentStack; import com.android.documentsui.model.RootInfo; import com.google.common.collect.Lists; import java.util.ArrayList; Loading @@ -109,6 +108,8 @@ import java.util.List; */ public class DirectoryFragment extends Fragment { public static final String TAG = "DirectoryFragment"; public static final int TYPE_NORMAL = 1; public static final int TYPE_SEARCH = 2; public static final int TYPE_RECENT_OPEN = 3; Loading @@ -130,6 +131,7 @@ public class DirectoryFragment extends Fragment { private static final String EXTRA_IGNORE_STATE = "ignoreState"; private Model mModel; private MultiSelectManager mSelectionManager; private Model.UpdateListener mModelUpdateListener = new ModelUpdateListener(); private View mEmptyView; Loading Loading @@ -268,7 +270,7 @@ public class DirectoryFragment extends Fragment { } // Clear any outstanding selection mModel.clearSelection(); mSelectionManager.clearSelection(); } @Override Loading Loading @@ -299,16 +301,16 @@ public class DirectoryFragment extends Fragment { // 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( // into the selection manager. mSelectionManager = new MultiSelectManager( mRecView, listener, state.allowMultiple ? MultiSelectManager.MODE_MULTIPLE : MultiSelectManager.MODE_SINGLE); selMgr.addCallback(new SelectionModeListener()); mSelectionManager.addCallback(new SelectionModeListener()); mModel = new Model(context, selMgr, mAdapter); mModel = new Model(context, mAdapter); mModel.addUpdateListener(mModelUpdateListener); mType = getArguments().getInt(EXTRA_TYPE); Loading Loading @@ -430,7 +432,7 @@ public class DirectoryFragment extends Fragment { } private boolean onSingleTapUp(MotionEvent e) { if (Events.isTouchEvent(e) && mModel.getSelection().isEmpty()) { if (Events.isTouchEvent(e) && mSelectionManager.getSelection().isEmpty()) { int position = getEventAdapterPosition(e); if (position != RecyclerView.NO_POSITION) { return handleViewItem(position); Loading Loading @@ -458,7 +460,7 @@ public class DirectoryFragment extends Fragment { if (isDocumentEnabled(docMimeType, docFlags)) { final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor); ((BaseActivity) getActivity()).onDocumentPicked(doc, mModel); mModel.clearSelection(); mSelectionManager.clearSelection(); return true; } return false; Loading Loading @@ -564,7 +566,7 @@ public class DirectoryFragment extends Fragment { mRecView.setLayoutManager(layout); // TODO: Once b/23691541 is resolved, use a listener within MultiSelectManager instead of // imperatively calling this function. mModel.mSelectionManager.handleLayoutChanged(); mSelectionManager.handleLayoutChanged(); // setting layout manager automatically invalidates existing ViewHolders. mThumbSize = new Point(thumbSize, thumbSize); } Loading Loading @@ -620,7 +622,7 @@ public class DirectoryFragment extends Fragment { @Override public void onSelectionChanged() { mModel.getSelection(mSelected); mSelectionManager.getSelection(mSelected); TypedValue color = new TypedValue(); if (mSelected.size() > 0) { if (DEBUG) Log.d(TAG, "Maybe starting action mode."); Loading @@ -628,8 +630,7 @@ public class DirectoryFragment extends Fragment { if (DEBUG) Log.d(TAG, "Yeah. Starting action mode."); mActionMode = getActivity().startActionMode(this); } getActivity().getTheme().resolveAttribute( R.attr.colorActionMode, color, true); getActivity().getTheme().resolveAttribute(R.attr.colorActionMode, color, true); updateActionMenu(); } else { if (DEBUG) Log.d(TAG, "Finishing action mode."); Loading @@ -652,16 +653,17 @@ public class DirectoryFragment extends Fragment { if (DEBUG) Log.d(TAG, "Handling action mode destroyed."); mActionMode = null; // clear selection mModel.clearSelection(); mSelectionManager.clearSelection(); mSelected.clear(); mNoDeleteCount = 0; } @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { int size = mSelectionManager.getSelection().size(); mode.getMenuInflater().inflate(R.menu.mode_directory, menu); mode.setTitle(TextUtils.formatSelectedCount(mModel.getSelection().size())); return mModel.getSelection().size() > 0; mode.setTitle(TextUtils.formatSelectedCount(size)); return (size > 0); } @Override Loading @@ -681,7 +683,7 @@ public class DirectoryFragment extends Fragment { @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { Selection selection = mModel.getSelection(new Selection()); Selection selection = mSelectionManager.getSelection(new Selection()); final int id = item.getItemId(); if (id == R.id.menu_open) { Loading Loading @@ -920,15 +922,22 @@ public class DirectoryFragment extends Fragment { public DocumentHolder onCreateViewHolder(ViewGroup parent, int viewType) { final State state = getDisplayState(DirectoryFragment.this); final LayoutInflater inflater = LayoutInflater.from(getContext()); View item = null; switch (state.derivedMode) { case MODE_GRID: return new DocumentHolder(inflater.inflate(R.layout.item_doc_grid, parent, false)); item = inflater.inflate(R.layout.item_doc_grid, parent, false); break; case MODE_LIST: return new DocumentHolder(inflater.inflate(R.layout.item_doc_list, parent, false)); item = inflater.inflate(R.layout.item_doc_list, parent, false); break; case MODE_UNKNOWN: default: throw new IllegalStateException("Unsupported layout mode."); } // Key event bubbling doesn't work properly, so instead of setting one key listener on // the RecyclerView, we have to set it on each Item. See b/24865023. item.setOnKeyListener(mSelectionManager); return new DocumentHolder(item); } @Override Loading Loading @@ -957,7 +966,7 @@ public class DirectoryFragment extends Fragment { holder.docId = docId; final View itemView = holder.view; itemView.setActivated(mModel.isSelected(position)); itemView.setActivated(isSelected(position)); final View line1 = itemView.findViewById(R.id.line1); final View line2 = itemView.findViewById(R.id.line2); Loading Loading @@ -1289,7 +1298,7 @@ public class DirectoryFragment extends Fragment { } void copySelectedToClipboard() { Selection sel = mModel.getSelection(new Selection()); Selection sel = mSelectionManager.getSelection(new Selection()); copySelectionToClipboard(sel); } Loading Loading @@ -1338,51 +1347,12 @@ public class DirectoryFragment extends Fragment { } void selectAllFiles() { boolean changed = mModel.selectAll(); boolean changed = mSelectionManager.setItemsSelected(0, mModel.getItemCount(), true); if (changed) { updateDisplayState(); } } /** * Scrolls to the top of the file list and focuses the first file. */ void focusFirstFile() { focusFile(0); } /** * Scrolls to the bottom of the file list and focuses the last file. */ void focusLastFile() { focusFile(mAdapter.getItemCount() - 1); } /** * Scrolls to and then focuses on the file at the given position. */ private void focusFile(final int pos) { // Don't smooth scroll; that taxes the system unnecessarily and makes the scroll handling // logic below more complicated. mRecView.scrollToPosition(pos); // If the item is already in view, focus it; otherwise, set a one-time listener to focus it // when the scroll is completed. RecyclerView.ViewHolder vh = mRecView.findViewHolderForAdapterPosition(pos); if (vh != null) { vh.itemView.requestFocus(); } else { mRecView.addOnScrollListener( new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView view, int dx, int dy) { view.findViewHolderForAdapterPosition(pos).itemView.requestFocus(); view.removeOnScrollListener(this); } }); } } private void setupDragAndDropOnDirectoryView(View view) { // Listen for drops on non-directory items and empty space. view.setOnDragListener(mOnDragListener); Loading Loading @@ -1472,9 +1442,10 @@ public class DirectoryFragment extends Fragment { return Collections.EMPTY_LIST; } final List<DocumentInfo> selectedDocs = mModel.getSelectedDocuments(); final List<DocumentInfo> selectedDocs = mModel.getDocuments(mSelectionManager.getSelection()); if (!selectedDocs.isEmpty()) { if (!mModel.isSelected(position)) { if (!isSelected(position)) { // There is a selection that does not include the current item, drag nothing. return Collections.EMPTY_LIST; } Loading Loading @@ -1694,12 +1665,15 @@ public class DirectoryFragment extends Fragment { public void afterActivityCreated(DirectoryFragment fragment) {} } boolean isSelected(int position) { return mSelectionManager.getSelection().contains(position); } /** * The data model for the current loaded directory. */ @VisibleForTesting public static final class Model implements DocumentContext { private MultiSelectManager mSelectionManager; private RecyclerView.Adapter<?> mViewAdapter; private Context mContext; private int mCursorCount; Loading @@ -1710,45 +1684,11 @@ public class DirectoryFragment extends Fragment { @Nullable private String info; @Nullable private String error; Model(Context context, MultiSelectManager selectionManager, RecyclerView.Adapter<?> viewAdapter) { Model(Context context, RecyclerView.Adapter<?> viewAdapter) { mContext = context; mSelectionManager = selectionManager; mViewAdapter = viewAdapter; } /** * 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."); Loading Loading @@ -1821,11 +1761,6 @@ public class DirectoryFragment extends Fragment { return mIsLoading; } private List<DocumentInfo> getSelectedDocuments() { Selection sel = getSelection(new Selection()); return getDocuments(sel); } List<DocumentInfo> getDocuments(Selection items) { final int size = (items != null) ? items.size() : 0; Loading packages/DocumentsUI/src/com/android/documentsui/MultiSelectManager.java +116 −15 Original line number Diff line number Diff line Loading @@ -39,6 +39,7 @@ import android.util.SparseIntArray; import android.view.GestureDetector; import android.view.GestureDetector.OnDoubleTapListener; import android.view.GestureDetector.OnGestureListener; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; Loading @@ -56,7 +57,7 @@ import java.util.List; * Additionally it can be configured to restrict selection to a single element, @see * #setSelectMode. */ public final class MultiSelectManager { public final class MultiSelectManager implements View.OnKeyListener { /** Selection mode for multiple select. **/ public static final int MODE_MULTIPLE = 0; Loading @@ -72,6 +73,8 @@ public final class MultiSelectManager { private Selection mIntermediateSelection; private Range mRanger; private SelectionEnvironment mEnvironment; private final List<MultiSelectManager.Callback> mCallbacks = new ArrayList<>(1); private Adapter<?> mAdapter; Loading @@ -95,10 +98,10 @@ public final class MultiSelectManager { new RuntimeItemFinder(recyclerView), mode); mEnvironment = new RuntimeSelectionEnvironment(recyclerView); if (mode == MODE_MULTIPLE) { mBandManager = new BandController( mHelper, new RuntimeBandEnvironment(recyclerView)); mBandManager = new BandController(mHelper); } GestureDetector.SimpleOnGestureListener listener = Loading Loading @@ -900,7 +903,7 @@ public final class MultiSelectManager { * Provides functionality for BandController. Exists primarily to tests that are * fully isolated from RecyclerView. */ interface BandEnvironment { interface SelectionEnvironment { void showBand(Rect rect); void hideBand(); void addOnScrollListener(RecyclerView.OnScrollListener listener); Loading @@ -913,29 +916,39 @@ public final class MultiSelectManager { Point createAbsolutePoint(Point relativePoint); Rect getAbsoluteRectForChildViewAt(int index); int getAdapterPositionAt(int index); int getAdapterPositionForChildView(View view); int getColumnCount(); int getRowCount(); int getChildCount(); int getVisibleChildCount(); void focusItem(int position); } /** RvFacade implementation backed by good ol' RecyclerView. */ private static final class RuntimeBandEnvironment implements BandEnvironment { private static final class RuntimeSelectionEnvironment implements SelectionEnvironment { private final RecyclerView mView; private final Drawable mBand; private boolean mIsOverlayShown = false; RuntimeBandEnvironment(RecyclerView rv) { RuntimeSelectionEnvironment(RecyclerView rv) { mView = rv; mBand = mView.getContext().getTheme().getDrawable(R.drawable.band_select_overlay); } @Override public int getAdapterPositionForChildView(View view) { if (view.getParent() == mView) { return mView.getChildAdapterPosition(view); } else { return RecyclerView.NO_POSITION; } } @Override public int getAdapterPositionAt(int index) { View child = mView.getChildAt(index); return mView.getChildViewHolder(child).getAdapterPosition(); return getAdapterPositionForChildView(mView.getChildAt(index)); } @Override Loading Loading @@ -1032,6 +1045,28 @@ public final class MultiSelectManager { public void hideBand() { mView.getOverlay().remove(mBand); } @Override public void focusItem(final int pos) { // If the item is already in view, focus it; otherwise, scroll to it and focus it. RecyclerView.ViewHolder vh = mView.findViewHolderForAdapterPosition(pos); if (vh != null) { vh.itemView.requestFocus(); } else { // Don't smooth scroll; that taxes the system unnecessarily and makes the scroll // handling logic below more complicated. See b/24865658. mView.scrollToPosition(pos); // Set a one-time listener to request focus when the scroll has completed. mView.addOnScrollListener( new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView view, int dx, int dy) { view.findViewHolderForAdapterPosition(pos).itemView.requestFocus(); view.removeOnScrollListener(this); } }); } } } public interface Callback { Loading Loading @@ -1175,7 +1210,6 @@ public final class MultiSelectManager { private static final int NOT_SET = -1; private final ItemFinder mItemFinder; private final BandEnvironment mEnvironment; private final Runnable mModelBuilder; @Nullable private Rect mBounds; Loading @@ -1188,15 +1222,14 @@ public final class MultiSelectManager { private long mScrollStartTime = NOT_SET; private final Runnable mViewScroller = new ViewScroller(); public BandController(ItemFinder finder, final BandEnvironment environment) { public BandController(ItemFinder finder) { mItemFinder = finder; mEnvironment = environment; mEnvironment.addOnScrollListener(this); mModelBuilder = new Runnable() { @Override public void run() { mModel = new GridModel(environment); mModel = new GridModel(mEnvironment); mModel.addOnSelectionChangedListener(BandController.this); } }; Loading Loading @@ -1459,7 +1492,7 @@ public final class MultiSelectManager { private static final int LOWER_LEFT = LOWER | LEFT; private static final int LOWER_RIGHT = LOWER | RIGHT; private final BandEnvironment mHelper; private final SelectionEnvironment mHelper; private final List<OnSelectionChangedListener> mOnSelectionChangedListeners = new ArrayList<>(); Loading Loading @@ -1497,7 +1530,7 @@ public final class MultiSelectManager { // should expand from when Shift+click is used. private int mPositionNearestOrigin = NOT_SET; GridModel(BandEnvironment helper) { GridModel(SelectionEnvironment helper) { mHelper = helper; mHelper.addOnScrollListener(this); } Loading Loading @@ -2041,4 +2074,72 @@ public final class MultiSelectManager { return true; } } // TODO: Might have to move this to a more global level. e.g. What should happen if the // user taps a file and then presses shift-down? Currently the RecyclerView never even sees // the key event. Perhaps install a global key handler to catch those events while in // selection mode? @Override public boolean onKey(View view, int keyCode, KeyEvent event) { // Listen for key-down events. This allows the handler to respond appropriately when // the user holds down the arrow keys for navigation. if (event.getAction() != KeyEvent.ACTION_DOWN) { return false; } int target = RecyclerView.NO_POSITION; if (keyCode == KeyEvent.KEYCODE_MOVE_HOME) { target = 0; } else if (keyCode == KeyEvent.KEYCODE_MOVE_END) { target = mAdapter.getItemCount() - 1; } else { // Find a navigation target based on the arrow key that the user pressed. Ignore // navigation targets that aren't items in the recycler view. int searchDir = -1; switch (keyCode) { case KeyEvent.KEYCODE_DPAD_UP: searchDir = View.FOCUS_UP; break; case KeyEvent.KEYCODE_DPAD_DOWN: searchDir = View.FOCUS_DOWN; break; case KeyEvent.KEYCODE_DPAD_LEFT: searchDir = View.FOCUS_LEFT; break; case KeyEvent.KEYCODE_DPAD_RIGHT: searchDir = View.FOCUS_RIGHT; break; } if (searchDir != -1) { View targetView = view.focusSearch(searchDir); target = mEnvironment.getAdapterPositionForChildView(targetView); } } if (target == RecyclerView.NO_POSITION) { // If there is no valid navigation target, don't handle the keypress. return false; } // Focus the new file. mEnvironment.focusItem(target); if (event.isShiftPressed()) { if (mSelection.isEmpty()) { // If there is no selection, start a selection when the user presses shift-arrow. toggleSelection(mEnvironment.getAdapterPositionForChildView(view)); } else { // Deal with b/24802917 (selected items can't be focused) by adjusting the // selection sorted the focused item isn't in the selection. target -= Integer.signum(target - mRanger.mBegin); mRanger.snapSelection(target); } } else if (!event.isShiftPressed() && !mSelection.isEmpty()) { // If there is a selection, clear it if the user presses arrow with no shift. clearSelection(); } return true; } } packages/DocumentsUI/tests/src/com/android/documentsui/DirectoryFragmentModelTest.java +1 −1 Original line number Diff line number Diff line Loading @@ -64,7 +64,7 @@ public class DirectoryFragmentModelTest extends AndroidTestCase { r.cursor = cursor; // Instantiate the model with a dummy view adapter and listener that (for now) do nothing. model = new Model(mContext, null, new DummyAdapter()); model = new Model(mContext, new DummyAdapter()); model.addUpdateListener(new DummyListener()); model.update(r); } Loading packages/DocumentsUI/tests/src/com/android/documentsui/MultiSelectManager_GridModelTest.java +19 −8 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ import android.graphics.Rect; import android.support.v7.widget.RecyclerView.OnScrollListener; import android.test.AndroidTestCase; import android.util.SparseBooleanArray; import android.view.View; import com.android.documentsui.MultiSelectManager.GridModel; Loading @@ -34,14 +35,14 @@ public class MultiSelectManager_GridModelTest extends AndroidTestCase { private static final int VIEWPORT_HEIGHT = 500; private static GridModel model; private static TestHelper helper; private static TestEnvironment env; private static SparseBooleanArray lastSelection; private static int viewWidth; private static void setUp(int numChildren, int numColumns) { helper = new TestHelper(numChildren, numColumns); env = new TestEnvironment(numChildren, numColumns); viewWidth = VIEW_PADDING_PX + numColumns * (VIEW_PADDING_PX + CHILD_VIEW_EDGE_PX); model = new GridModel(helper); model = new GridModel(env); model.addOnSelectionChangedListener( new GridModel.OnSelectionChangedListener() { @Override Loading @@ -54,7 +55,7 @@ public class MultiSelectManager_GridModelTest extends AndroidTestCase { @Override public void tearDown() { model = null; helper = null; env = null; lastSelection = null; } Loading Loading @@ -176,12 +177,12 @@ public class MultiSelectManager_GridModelTest extends AndroidTestCase { } private static void scroll(int dy) { assertTrue(helper.verticalOffset + VIEWPORT_HEIGHT + dy <= helper.getTotalHeight()); helper.verticalOffset += dy; assertTrue(env.verticalOffset + VIEWPORT_HEIGHT + dy <= env.getTotalHeight()); env.verticalOffset += dy; model.onScrolled(null, 0, dy); } private static final class TestHelper implements MultiSelectManager.BandEnvironment { private static final class TestEnvironment implements MultiSelectManager.SelectionEnvironment { public int horizontalOffset = 0; public int verticalOffset = 0; Loading @@ -189,7 +190,7 @@ public class MultiSelectManager_GridModelTest extends AndroidTestCase { private final int mNumRows; private final int mNumChildren; public TestHelper(int numChildren, int numColumns) { public TestEnvironment(int numChildren, int numColumns) { mNumChildren = numChildren; mNumColumns = numColumns; mNumRows = (int) Math.ceil((double) numChildren / mNumColumns); Loading Loading @@ -307,5 +308,15 @@ public class MultiSelectManager_GridModelTest extends AndroidTestCase { public void removeCallback(Runnable r) { throw new UnsupportedOperationException(); } @Override public int getAdapterPositionForChildView(View view) { throw new UnsupportedOperationException(); } @Override public void focusItem(int i) { throw new UnsupportedOperationException(); } } } Loading
packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java +0 −19 Original line number Diff line number Diff line Loading @@ -466,25 +466,6 @@ abstract class BaseActivity extends Activity { } } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { if (DEBUG) Log.d(mTag, "onKeyUp: keycode = " + keyCode); // TODO: Support for RecentsCreateFragment. DirectoryFragment fragment = DirectoryFragment.get(getFragmentManager()); if (fragment != null) { switch (keyCode) { case KeyEvent.KEYCODE_MOVE_HOME: fragment.focusFirstFile(); return true; case KeyEvent.KEYCODE_MOVE_END: fragment.focusLastFile(); return true; } } return super.onKeyUp(keyCode, event); } public void onStackPicked(DocumentStack stack) { try { // Update the restored stack to ensure we have freshest data Loading
packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java +39 −104 Original line number Diff line number Diff line Loading @@ -17,7 +17,6 @@ package com.android.documentsui; import static com.android.documentsui.Shared.DEBUG; import static com.android.documentsui.Shared.TAG; import static com.android.documentsui.State.ACTION_BROWSE; import static com.android.documentsui.State.ACTION_CREATE; import static com.android.documentsui.State.ACTION_MANAGE; Loading Loading @@ -79,6 +78,7 @@ import android.util.TypedValue; import android.view.ActionMode; import android.view.DragEvent; import android.view.GestureDetector; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; Loading @@ -97,7 +97,6 @@ import com.android.documentsui.RecentsProvider.StateColumns; import com.android.documentsui.model.DocumentInfo; import com.android.documentsui.model.DocumentStack; import com.android.documentsui.model.RootInfo; import com.google.common.collect.Lists; import java.util.ArrayList; Loading @@ -109,6 +108,8 @@ import java.util.List; */ public class DirectoryFragment extends Fragment { public static final String TAG = "DirectoryFragment"; public static final int TYPE_NORMAL = 1; public static final int TYPE_SEARCH = 2; public static final int TYPE_RECENT_OPEN = 3; Loading @@ -130,6 +131,7 @@ public class DirectoryFragment extends Fragment { private static final String EXTRA_IGNORE_STATE = "ignoreState"; private Model mModel; private MultiSelectManager mSelectionManager; private Model.UpdateListener mModelUpdateListener = new ModelUpdateListener(); private View mEmptyView; Loading Loading @@ -268,7 +270,7 @@ public class DirectoryFragment extends Fragment { } // Clear any outstanding selection mModel.clearSelection(); mSelectionManager.clearSelection(); } @Override Loading Loading @@ -299,16 +301,16 @@ public class DirectoryFragment extends Fragment { // 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( // into the selection manager. mSelectionManager = new MultiSelectManager( mRecView, listener, state.allowMultiple ? MultiSelectManager.MODE_MULTIPLE : MultiSelectManager.MODE_SINGLE); selMgr.addCallback(new SelectionModeListener()); mSelectionManager.addCallback(new SelectionModeListener()); mModel = new Model(context, selMgr, mAdapter); mModel = new Model(context, mAdapter); mModel.addUpdateListener(mModelUpdateListener); mType = getArguments().getInt(EXTRA_TYPE); Loading Loading @@ -430,7 +432,7 @@ public class DirectoryFragment extends Fragment { } private boolean onSingleTapUp(MotionEvent e) { if (Events.isTouchEvent(e) && mModel.getSelection().isEmpty()) { if (Events.isTouchEvent(e) && mSelectionManager.getSelection().isEmpty()) { int position = getEventAdapterPosition(e); if (position != RecyclerView.NO_POSITION) { return handleViewItem(position); Loading Loading @@ -458,7 +460,7 @@ public class DirectoryFragment extends Fragment { if (isDocumentEnabled(docMimeType, docFlags)) { final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor); ((BaseActivity) getActivity()).onDocumentPicked(doc, mModel); mModel.clearSelection(); mSelectionManager.clearSelection(); return true; } return false; Loading Loading @@ -564,7 +566,7 @@ public class DirectoryFragment extends Fragment { mRecView.setLayoutManager(layout); // TODO: Once b/23691541 is resolved, use a listener within MultiSelectManager instead of // imperatively calling this function. mModel.mSelectionManager.handleLayoutChanged(); mSelectionManager.handleLayoutChanged(); // setting layout manager automatically invalidates existing ViewHolders. mThumbSize = new Point(thumbSize, thumbSize); } Loading Loading @@ -620,7 +622,7 @@ public class DirectoryFragment extends Fragment { @Override public void onSelectionChanged() { mModel.getSelection(mSelected); mSelectionManager.getSelection(mSelected); TypedValue color = new TypedValue(); if (mSelected.size() > 0) { if (DEBUG) Log.d(TAG, "Maybe starting action mode."); Loading @@ -628,8 +630,7 @@ public class DirectoryFragment extends Fragment { if (DEBUG) Log.d(TAG, "Yeah. Starting action mode."); mActionMode = getActivity().startActionMode(this); } getActivity().getTheme().resolveAttribute( R.attr.colorActionMode, color, true); getActivity().getTheme().resolveAttribute(R.attr.colorActionMode, color, true); updateActionMenu(); } else { if (DEBUG) Log.d(TAG, "Finishing action mode."); Loading @@ -652,16 +653,17 @@ public class DirectoryFragment extends Fragment { if (DEBUG) Log.d(TAG, "Handling action mode destroyed."); mActionMode = null; // clear selection mModel.clearSelection(); mSelectionManager.clearSelection(); mSelected.clear(); mNoDeleteCount = 0; } @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { int size = mSelectionManager.getSelection().size(); mode.getMenuInflater().inflate(R.menu.mode_directory, menu); mode.setTitle(TextUtils.formatSelectedCount(mModel.getSelection().size())); return mModel.getSelection().size() > 0; mode.setTitle(TextUtils.formatSelectedCount(size)); return (size > 0); } @Override Loading @@ -681,7 +683,7 @@ public class DirectoryFragment extends Fragment { @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { Selection selection = mModel.getSelection(new Selection()); Selection selection = mSelectionManager.getSelection(new Selection()); final int id = item.getItemId(); if (id == R.id.menu_open) { Loading Loading @@ -920,15 +922,22 @@ public class DirectoryFragment extends Fragment { public DocumentHolder onCreateViewHolder(ViewGroup parent, int viewType) { final State state = getDisplayState(DirectoryFragment.this); final LayoutInflater inflater = LayoutInflater.from(getContext()); View item = null; switch (state.derivedMode) { case MODE_GRID: return new DocumentHolder(inflater.inflate(R.layout.item_doc_grid, parent, false)); item = inflater.inflate(R.layout.item_doc_grid, parent, false); break; case MODE_LIST: return new DocumentHolder(inflater.inflate(R.layout.item_doc_list, parent, false)); item = inflater.inflate(R.layout.item_doc_list, parent, false); break; case MODE_UNKNOWN: default: throw new IllegalStateException("Unsupported layout mode."); } // Key event bubbling doesn't work properly, so instead of setting one key listener on // the RecyclerView, we have to set it on each Item. See b/24865023. item.setOnKeyListener(mSelectionManager); return new DocumentHolder(item); } @Override Loading Loading @@ -957,7 +966,7 @@ public class DirectoryFragment extends Fragment { holder.docId = docId; final View itemView = holder.view; itemView.setActivated(mModel.isSelected(position)); itemView.setActivated(isSelected(position)); final View line1 = itemView.findViewById(R.id.line1); final View line2 = itemView.findViewById(R.id.line2); Loading Loading @@ -1289,7 +1298,7 @@ public class DirectoryFragment extends Fragment { } void copySelectedToClipboard() { Selection sel = mModel.getSelection(new Selection()); Selection sel = mSelectionManager.getSelection(new Selection()); copySelectionToClipboard(sel); } Loading Loading @@ -1338,51 +1347,12 @@ public class DirectoryFragment extends Fragment { } void selectAllFiles() { boolean changed = mModel.selectAll(); boolean changed = mSelectionManager.setItemsSelected(0, mModel.getItemCount(), true); if (changed) { updateDisplayState(); } } /** * Scrolls to the top of the file list and focuses the first file. */ void focusFirstFile() { focusFile(0); } /** * Scrolls to the bottom of the file list and focuses the last file. */ void focusLastFile() { focusFile(mAdapter.getItemCount() - 1); } /** * Scrolls to and then focuses on the file at the given position. */ private void focusFile(final int pos) { // Don't smooth scroll; that taxes the system unnecessarily and makes the scroll handling // logic below more complicated. mRecView.scrollToPosition(pos); // If the item is already in view, focus it; otherwise, set a one-time listener to focus it // when the scroll is completed. RecyclerView.ViewHolder vh = mRecView.findViewHolderForAdapterPosition(pos); if (vh != null) { vh.itemView.requestFocus(); } else { mRecView.addOnScrollListener( new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView view, int dx, int dy) { view.findViewHolderForAdapterPosition(pos).itemView.requestFocus(); view.removeOnScrollListener(this); } }); } } private void setupDragAndDropOnDirectoryView(View view) { // Listen for drops on non-directory items and empty space. view.setOnDragListener(mOnDragListener); Loading Loading @@ -1472,9 +1442,10 @@ public class DirectoryFragment extends Fragment { return Collections.EMPTY_LIST; } final List<DocumentInfo> selectedDocs = mModel.getSelectedDocuments(); final List<DocumentInfo> selectedDocs = mModel.getDocuments(mSelectionManager.getSelection()); if (!selectedDocs.isEmpty()) { if (!mModel.isSelected(position)) { if (!isSelected(position)) { // There is a selection that does not include the current item, drag nothing. return Collections.EMPTY_LIST; } Loading Loading @@ -1694,12 +1665,15 @@ public class DirectoryFragment extends Fragment { public void afterActivityCreated(DirectoryFragment fragment) {} } boolean isSelected(int position) { return mSelectionManager.getSelection().contains(position); } /** * The data model for the current loaded directory. */ @VisibleForTesting public static final class Model implements DocumentContext { private MultiSelectManager mSelectionManager; private RecyclerView.Adapter<?> mViewAdapter; private Context mContext; private int mCursorCount; Loading @@ -1710,45 +1684,11 @@ public class DirectoryFragment extends Fragment { @Nullable private String info; @Nullable private String error; Model(Context context, MultiSelectManager selectionManager, RecyclerView.Adapter<?> viewAdapter) { Model(Context context, RecyclerView.Adapter<?> viewAdapter) { mContext = context; mSelectionManager = selectionManager; mViewAdapter = viewAdapter; } /** * 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."); Loading Loading @@ -1821,11 +1761,6 @@ public class DirectoryFragment extends Fragment { return mIsLoading; } private List<DocumentInfo> getSelectedDocuments() { Selection sel = getSelection(new Selection()); return getDocuments(sel); } List<DocumentInfo> getDocuments(Selection items) { final int size = (items != null) ? items.size() : 0; Loading
packages/DocumentsUI/src/com/android/documentsui/MultiSelectManager.java +116 −15 Original line number Diff line number Diff line Loading @@ -39,6 +39,7 @@ import android.util.SparseIntArray; import android.view.GestureDetector; import android.view.GestureDetector.OnDoubleTapListener; import android.view.GestureDetector.OnGestureListener; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; Loading @@ -56,7 +57,7 @@ import java.util.List; * Additionally it can be configured to restrict selection to a single element, @see * #setSelectMode. */ public final class MultiSelectManager { public final class MultiSelectManager implements View.OnKeyListener { /** Selection mode for multiple select. **/ public static final int MODE_MULTIPLE = 0; Loading @@ -72,6 +73,8 @@ public final class MultiSelectManager { private Selection mIntermediateSelection; private Range mRanger; private SelectionEnvironment mEnvironment; private final List<MultiSelectManager.Callback> mCallbacks = new ArrayList<>(1); private Adapter<?> mAdapter; Loading @@ -95,10 +98,10 @@ public final class MultiSelectManager { new RuntimeItemFinder(recyclerView), mode); mEnvironment = new RuntimeSelectionEnvironment(recyclerView); if (mode == MODE_MULTIPLE) { mBandManager = new BandController( mHelper, new RuntimeBandEnvironment(recyclerView)); mBandManager = new BandController(mHelper); } GestureDetector.SimpleOnGestureListener listener = Loading Loading @@ -900,7 +903,7 @@ public final class MultiSelectManager { * Provides functionality for BandController. Exists primarily to tests that are * fully isolated from RecyclerView. */ interface BandEnvironment { interface SelectionEnvironment { void showBand(Rect rect); void hideBand(); void addOnScrollListener(RecyclerView.OnScrollListener listener); Loading @@ -913,29 +916,39 @@ public final class MultiSelectManager { Point createAbsolutePoint(Point relativePoint); Rect getAbsoluteRectForChildViewAt(int index); int getAdapterPositionAt(int index); int getAdapterPositionForChildView(View view); int getColumnCount(); int getRowCount(); int getChildCount(); int getVisibleChildCount(); void focusItem(int position); } /** RvFacade implementation backed by good ol' RecyclerView. */ private static final class RuntimeBandEnvironment implements BandEnvironment { private static final class RuntimeSelectionEnvironment implements SelectionEnvironment { private final RecyclerView mView; private final Drawable mBand; private boolean mIsOverlayShown = false; RuntimeBandEnvironment(RecyclerView rv) { RuntimeSelectionEnvironment(RecyclerView rv) { mView = rv; mBand = mView.getContext().getTheme().getDrawable(R.drawable.band_select_overlay); } @Override public int getAdapterPositionForChildView(View view) { if (view.getParent() == mView) { return mView.getChildAdapterPosition(view); } else { return RecyclerView.NO_POSITION; } } @Override public int getAdapterPositionAt(int index) { View child = mView.getChildAt(index); return mView.getChildViewHolder(child).getAdapterPosition(); return getAdapterPositionForChildView(mView.getChildAt(index)); } @Override Loading Loading @@ -1032,6 +1045,28 @@ public final class MultiSelectManager { public void hideBand() { mView.getOverlay().remove(mBand); } @Override public void focusItem(final int pos) { // If the item is already in view, focus it; otherwise, scroll to it and focus it. RecyclerView.ViewHolder vh = mView.findViewHolderForAdapterPosition(pos); if (vh != null) { vh.itemView.requestFocus(); } else { // Don't smooth scroll; that taxes the system unnecessarily and makes the scroll // handling logic below more complicated. See b/24865658. mView.scrollToPosition(pos); // Set a one-time listener to request focus when the scroll has completed. mView.addOnScrollListener( new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView view, int dx, int dy) { view.findViewHolderForAdapterPosition(pos).itemView.requestFocus(); view.removeOnScrollListener(this); } }); } } } public interface Callback { Loading Loading @@ -1175,7 +1210,6 @@ public final class MultiSelectManager { private static final int NOT_SET = -1; private final ItemFinder mItemFinder; private final BandEnvironment mEnvironment; private final Runnable mModelBuilder; @Nullable private Rect mBounds; Loading @@ -1188,15 +1222,14 @@ public final class MultiSelectManager { private long mScrollStartTime = NOT_SET; private final Runnable mViewScroller = new ViewScroller(); public BandController(ItemFinder finder, final BandEnvironment environment) { public BandController(ItemFinder finder) { mItemFinder = finder; mEnvironment = environment; mEnvironment.addOnScrollListener(this); mModelBuilder = new Runnable() { @Override public void run() { mModel = new GridModel(environment); mModel = new GridModel(mEnvironment); mModel.addOnSelectionChangedListener(BandController.this); } }; Loading Loading @@ -1459,7 +1492,7 @@ public final class MultiSelectManager { private static final int LOWER_LEFT = LOWER | LEFT; private static final int LOWER_RIGHT = LOWER | RIGHT; private final BandEnvironment mHelper; private final SelectionEnvironment mHelper; private final List<OnSelectionChangedListener> mOnSelectionChangedListeners = new ArrayList<>(); Loading Loading @@ -1497,7 +1530,7 @@ public final class MultiSelectManager { // should expand from when Shift+click is used. private int mPositionNearestOrigin = NOT_SET; GridModel(BandEnvironment helper) { GridModel(SelectionEnvironment helper) { mHelper = helper; mHelper.addOnScrollListener(this); } Loading Loading @@ -2041,4 +2074,72 @@ public final class MultiSelectManager { return true; } } // TODO: Might have to move this to a more global level. e.g. What should happen if the // user taps a file and then presses shift-down? Currently the RecyclerView never even sees // the key event. Perhaps install a global key handler to catch those events while in // selection mode? @Override public boolean onKey(View view, int keyCode, KeyEvent event) { // Listen for key-down events. This allows the handler to respond appropriately when // the user holds down the arrow keys for navigation. if (event.getAction() != KeyEvent.ACTION_DOWN) { return false; } int target = RecyclerView.NO_POSITION; if (keyCode == KeyEvent.KEYCODE_MOVE_HOME) { target = 0; } else if (keyCode == KeyEvent.KEYCODE_MOVE_END) { target = mAdapter.getItemCount() - 1; } else { // Find a navigation target based on the arrow key that the user pressed. Ignore // navigation targets that aren't items in the recycler view. int searchDir = -1; switch (keyCode) { case KeyEvent.KEYCODE_DPAD_UP: searchDir = View.FOCUS_UP; break; case KeyEvent.KEYCODE_DPAD_DOWN: searchDir = View.FOCUS_DOWN; break; case KeyEvent.KEYCODE_DPAD_LEFT: searchDir = View.FOCUS_LEFT; break; case KeyEvent.KEYCODE_DPAD_RIGHT: searchDir = View.FOCUS_RIGHT; break; } if (searchDir != -1) { View targetView = view.focusSearch(searchDir); target = mEnvironment.getAdapterPositionForChildView(targetView); } } if (target == RecyclerView.NO_POSITION) { // If there is no valid navigation target, don't handle the keypress. return false; } // Focus the new file. mEnvironment.focusItem(target); if (event.isShiftPressed()) { if (mSelection.isEmpty()) { // If there is no selection, start a selection when the user presses shift-arrow. toggleSelection(mEnvironment.getAdapterPositionForChildView(view)); } else { // Deal with b/24802917 (selected items can't be focused) by adjusting the // selection sorted the focused item isn't in the selection. target -= Integer.signum(target - mRanger.mBegin); mRanger.snapSelection(target); } } else if (!event.isShiftPressed() && !mSelection.isEmpty()) { // If there is a selection, clear it if the user presses arrow with no shift. clearSelection(); } return true; } }
packages/DocumentsUI/tests/src/com/android/documentsui/DirectoryFragmentModelTest.java +1 −1 Original line number Diff line number Diff line Loading @@ -64,7 +64,7 @@ public class DirectoryFragmentModelTest extends AndroidTestCase { r.cursor = cursor; // Instantiate the model with a dummy view adapter and listener that (for now) do nothing. model = new Model(mContext, null, new DummyAdapter()); model = new Model(mContext, new DummyAdapter()); model.addUpdateListener(new DummyListener()); model.update(r); } Loading
packages/DocumentsUI/tests/src/com/android/documentsui/MultiSelectManager_GridModelTest.java +19 −8 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ import android.graphics.Rect; import android.support.v7.widget.RecyclerView.OnScrollListener; import android.test.AndroidTestCase; import android.util.SparseBooleanArray; import android.view.View; import com.android.documentsui.MultiSelectManager.GridModel; Loading @@ -34,14 +35,14 @@ public class MultiSelectManager_GridModelTest extends AndroidTestCase { private static final int VIEWPORT_HEIGHT = 500; private static GridModel model; private static TestHelper helper; private static TestEnvironment env; private static SparseBooleanArray lastSelection; private static int viewWidth; private static void setUp(int numChildren, int numColumns) { helper = new TestHelper(numChildren, numColumns); env = new TestEnvironment(numChildren, numColumns); viewWidth = VIEW_PADDING_PX + numColumns * (VIEW_PADDING_PX + CHILD_VIEW_EDGE_PX); model = new GridModel(helper); model = new GridModel(env); model.addOnSelectionChangedListener( new GridModel.OnSelectionChangedListener() { @Override Loading @@ -54,7 +55,7 @@ public class MultiSelectManager_GridModelTest extends AndroidTestCase { @Override public void tearDown() { model = null; helper = null; env = null; lastSelection = null; } Loading Loading @@ -176,12 +177,12 @@ public class MultiSelectManager_GridModelTest extends AndroidTestCase { } private static void scroll(int dy) { assertTrue(helper.verticalOffset + VIEWPORT_HEIGHT + dy <= helper.getTotalHeight()); helper.verticalOffset += dy; assertTrue(env.verticalOffset + VIEWPORT_HEIGHT + dy <= env.getTotalHeight()); env.verticalOffset += dy; model.onScrolled(null, 0, dy); } private static final class TestHelper implements MultiSelectManager.BandEnvironment { private static final class TestEnvironment implements MultiSelectManager.SelectionEnvironment { public int horizontalOffset = 0; public int verticalOffset = 0; Loading @@ -189,7 +190,7 @@ public class MultiSelectManager_GridModelTest extends AndroidTestCase { private final int mNumRows; private final int mNumChildren; public TestHelper(int numChildren, int numColumns) { public TestEnvironment(int numChildren, int numColumns) { mNumChildren = numChildren; mNumColumns = numColumns; mNumRows = (int) Math.ceil((double) numChildren / mNumColumns); Loading Loading @@ -307,5 +308,15 @@ public class MultiSelectManager_GridModelTest extends AndroidTestCase { public void removeCallback(Runnable r) { throw new UnsupportedOperationException(); } @Override public int getAdapterPositionForChildView(View view) { throw new UnsupportedOperationException(); } @Override public void focusItem(int i) { throw new UnsupportedOperationException(); } } }