Loading src/com/android/documentsui/dirlist/DirectoryFragment.java +2 −2 Original line number Diff line number Diff line Loading @@ -142,7 +142,7 @@ public class DirectoryFragment extends Fragment private Model mModel; private MultiSelectManager mSelectionMgr; private Model.UpdateListener mModelUpdateListener = new ModelUpdateListener(); private UserInputHandler mInputHandler; private UserInputHandler<InputEvent> mInputHandler; private SelectionModeListener mSelectionModeListener; private FocusManager mFocusManager; Loading Loading @@ -280,7 +280,7 @@ public class DirectoryFragment extends Fragment // Make sure this is done after the RecyclerView is set up. mFocusManager = new FocusManager(context, mRecView, mModel); mInputHandler = new UserInputHandler( mInputHandler = new UserInputHandler<>( mSelectionMgr, mFocusManager, new Function<MotionEvent, InputEvent>() { Loading src/com/android/documentsui/dirlist/MultiSelectManager.java +12 −68 Original line number Diff line number Diff line Loading @@ -21,13 +21,10 @@ import static com.android.documentsui.Shared.DEBUG; import android.annotation.IntDef; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.support.v7.widget.RecyclerView; import android.util.Log; import com.android.documentsui.Events.InputEvent; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; Loading @@ -39,6 +36,8 @@ import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.Nullable; /** * MultiSelectManager provides support traditional multi-item selection support to RecyclerView. * Additionally it can be configured to restrict selection to a single element, @see Loading @@ -62,7 +61,7 @@ public final class MultiSelectManager { private final DocumentsAdapter mAdapter; private final List<MultiSelectManager.Callback> mCallbacks = new ArrayList<>(1); private Range mRanger; private @Nullable Range mRanger; private boolean mSingleSelect; public MultiSelectManager(DocumentsAdapter adapter, @SelectionMode int mode) { Loading Loading @@ -223,70 +222,13 @@ public final class MultiSelectManager { } } @VisibleForTesting void onLongPress(InputEvent input) { if (DEBUG) Log.d(TAG, "Handling long press event."); if (!input.isOverItem()) { if (DEBUG) Log.i(TAG, "Cannot handle tap. No adapter position available."); } handleAdapterEvent(input); } boolean onSingleTapUp(InputEvent input) { if (DEBUG) Log.d(TAG, "Processing tap event."); if (!hasSelection()) { // No selection active - do nothing. return false; } if (!input.isOverItem()) { if (DEBUG) Log.d(TAG, "Activity has no position. Canceling selection."); clearSelection(); return false; } handleAdapterEvent(input); return true; } /** * Handles a change caused by a click on the item with the given position. If the Shift key is * held down, this performs a range select; otherwise, it simply toggles the item's selection * state. */ private void handleAdapterEvent(InputEvent input) { if (mRanger != null && input.isShiftKeyDown()) { mRanger.snapSelection(input.getItemPosition()); void snapSelection(int position) { mRanger.snapSelection(position); // We're being lazy here notifying even when something might not have changed. // To make this more correct, we'd need to update the Ranger class to return // information about what has changed. notifySelectionChanged(); } else { int position = input.getItemPosition(); toggleSelection(position); setSelectionRangeBegin(position); } } /** * A convenience method for toggling selection by adapter position. * * @param position Adapter position to toggle. */ private void toggleSelection(int position) { // Position may be special "no position" during certain // transitional phases. If so, skip handling of the event. if (position == RecyclerView.NO_POSITION) { if (DEBUG) Log.d(TAG, "Ignoring toggle for element with no position."); return; } String id = mAdapter.getModelId(position); if (id != null) { toggleSelection(id); } } /** Loading Loading @@ -329,7 +271,9 @@ public final class MultiSelectManager { * @param pos The new end position for the selection range. */ void snapRangeSelection(int pos) { assert(mRanger != null); if (!isRangeSelectionActive()) { throw new IllegalStateException("Range start point not set."); } mRanger.snapSelection(pos); notifySelectionChanged(); Loading src/com/android/documentsui/dirlist/UserInputHandler.java +209 −123 Original line number Diff line number Diff line Loading @@ -16,6 +16,10 @@ package com.android.documentsui.dirlist; import static com.android.documentsui.Shared.DEBUG; import android.support.annotation.VisibleForTesting; import android.util.Log; import android.view.GestureDetector; import android.view.KeyEvent; import android.view.MotionEvent; Loading @@ -30,25 +34,29 @@ import java.util.function.Predicate; /** * Grand unified-ish gesture/event listener for items in the directory list. */ final class UserInputHandler extends GestureDetector.SimpleOnGestureListener public final class UserInputHandler<T extends InputEvent> extends GestureDetector.SimpleOnGestureListener implements KeyboardEventListener { private static final String TAG = "UserInputHandler"; private final MultiSelectManager mSelectionMgr; private final FocusHandler mFocusHandler; private final Function<MotionEvent, InputEvent> mEventConverter; private final Function<InputEvent, DocumentDetails> mDocFinder; private final Function<MotionEvent, T> mEventConverter; private final Function<T, DocumentDetails> mDocFinder; private final Predicate<DocumentDetails> mSelectable; private final EventHandler mRightClickHandler; private final DocumentHandler mActivateHandler; private final DocumentHandler mDeleteHandler; private final TouchInputDelegate mTouchDelegate; private final MouseInputDelegate mMouseDelegate; private final KeyInputHandler mKeyListener; public UserInputHandler( MultiSelectManager selectionMgr, FocusHandler focusHandler, Function<MotionEvent, InputEvent> eventConverter, Function<InputEvent, DocumentDetails> docFinder, Function<MotionEvent, T> eventConverter, Function<T, DocumentDetails> docFinder, Predicate<DocumentDetails> selectable, EventHandler rightClickHandler, DocumentHandler activateHandler, Loading @@ -65,55 +73,116 @@ final class UserInputHandler extends GestureDetector.SimpleOnGestureListener mTouchDelegate = new TouchInputDelegate(); mMouseDelegate = new MouseInputDelegate(); mKeyListener = new KeyInputHandler(); } @Override public boolean onSingleTapUp(MotionEvent e) { try (InputEvent event = mEventConverter.apply(e)) { try (T event = mEventConverter.apply(e)) { return onSingleTapUp(event); } } @VisibleForTesting boolean onSingleTapUp(T event) { return event.isMouseEvent() ? mMouseDelegate.onSingleTapUp(event) : mTouchDelegate.onSingleTapUp(event); } } @Override public boolean onSingleTapConfirmed(MotionEvent e) { try (InputEvent event = mEventConverter.apply(e)) { try (T event = mEventConverter.apply(e)) { return onSingleTapConfirmed(event); } } @VisibleForTesting boolean onSingleTapConfirmed(T event) { return event.isMouseEvent() ? mMouseDelegate.onSingleTapConfirmed(event) : mTouchDelegate.onSingleTapConfirmed(event); } } @Override public boolean onDoubleTap(MotionEvent e) { try (InputEvent event = mEventConverter.apply(e)) { try (T event = mEventConverter.apply(e)) { return onDoubleTap(event); } } @VisibleForTesting boolean onDoubleTap(T event) { return event.isMouseEvent() ? mMouseDelegate.onDoubleTap(event) : mTouchDelegate.onDoubleTap(event); } } @Override public void onLongPress(MotionEvent e) { try (InputEvent event = mEventConverter.apply(e)) { try (T event = mEventConverter.apply(e)) { onLongPress(event); } } @VisibleForTesting void onLongPress(T event) { if (event.isMouseEvent()) { mMouseDelegate.onLongPress(event); } mTouchDelegate.onLongPress(event); } public boolean onSingleRightClickUp(MotionEvent e) { try (T event = mEventConverter.apply(e)) { return mMouseDelegate.onSingleRightClickUp(event); } } @Override public boolean onKey(DocumentHolder doc, int keyCode, KeyEvent event) { return mKeyListener.onKey(doc, keyCode, event); } // TODO: Isolate this hack...see if we can't get this solved at the platform level. public void setLastButtonState(int state) { mMouseDelegate.setLastButtonState(state); } private boolean activateDocument(DocumentDetails doc) { return mActivateHandler.accept(doc); } private boolean onSelect(DocumentDetails doc) { private boolean selectDocument(DocumentDetails doc) { assert(doc != null); mSelectionMgr.toggleSelection(doc.getModelId()); mSelectionMgr.setSelectionRangeBegin(doc.getAdapterPosition()); return true; } boolean isRangeExtension(T event) { return event.isShiftKeyDown() && mSelectionMgr.isRangeSelectionActive(); } private void extendSelectionRange(T event) { mSelectionMgr.snapSelection(event.getItemPosition()); } private final class TouchInputDelegate { public boolean onSingleTapUp(InputEvent event) { if (mSelectionMgr.onSingleTapUp(event)) { boolean onSingleTapUp(T event) { if (!event.isOverItem()) { if (DEBUG) Log.d(TAG, "Tap on non-item. Clearing selection."); mSelectionMgr.clearSelection(); return false; } if (mSelectionMgr.hasSelection()) { if (isRangeExtension(event)) { mSelectionMgr.snapSelection(event.getItemPosition()); } else { selectDocument(mDocFinder.apply(event)); } return true; } Loading @@ -123,23 +192,31 @@ final class UserInputHandler extends GestureDetector.SimpleOnGestureListener // Touch events select if they occur in the selection hotspot, // otherwise they activate. return doc.isInSelectionHotspot(event) ? onSelect(doc) : mActivateHandler.accept(doc); ? selectDocument(doc) : activateDocument(doc); } return false; } public boolean onSingleTapConfirmed(InputEvent event) { boolean onSingleTapConfirmed(T event) { return false; } public boolean onDoubleTap(InputEvent event) { boolean onDoubleTap(T event) { return false; } public void onLongPress(InputEvent event) { mSelectionMgr.onLongPress(event); final void onLongPress(T event) { if (!event.isOverItem()) { return; } if (isRangeExtension(event)) { extendSelectionRange(event); } else { selectDocument(mDocFinder.apply(event)); } } } Loading @@ -155,18 +232,28 @@ final class UserInputHandler extends GestureDetector.SimpleOnGestureListener private int mLastButtonState = -1; // true when the previous event has consumed a right click motion event private boolean ateRightClick; private boolean mAteRightClick; // The event has been handled in onSingleTapUp private boolean handledTapUp; private boolean mHandledTapUp; public boolean onSingleTapUp(InputEvent event) { boolean onSingleTapUp(T event) { if (eatRightClick()) { return onSingleRightClickUp(event); } if (mSelectionMgr.onSingleTapUp(event)) { handledTapUp = true; if (!event.isOverItem()) { mSelectionMgr.clearSelection(); return false; } if (mSelectionMgr.hasSelection()) { if (isRangeExtension(event)) { extendSelectionRange(event); } else { selectDocument(mDocFinder.apply(event)); } mHandledTapUp = true; return true; } Loading @@ -181,17 +268,17 @@ final class UserInputHandler extends GestureDetector.SimpleOnGestureListener return false; } handledTapUp = true; return onSelect(doc); mHandledTapUp = true; return selectDocument(doc); } public boolean onSingleTapConfirmed(InputEvent event) { if (ateRightClick) { ateRightClick = false; boolean onSingleTapConfirmed(T event) { if (mAteRightClick) { mAteRightClick = false; return false; } if (handledTapUp) { handledTapUp = false; if (mHandledTapUp) { mHandledTapUp = false; return false; } Loading @@ -204,25 +291,33 @@ final class UserInputHandler extends GestureDetector.SimpleOnGestureListener return false; } return onSelect(doc); return selectDocument(doc); } public boolean onDoubleTap(InputEvent event) { handledTapUp = false; boolean onDoubleTap(T event) { mHandledTapUp = false; DocumentDetails doc = mDocFinder.apply(event); if (doc != null) { return mSelectionMgr.hasSelection() ? onSelect(doc) : mActivateHandler.accept(doc); ? selectDocument(doc) : activateDocument(doc); } return false; } public void onLongPress(InputEvent event) { mSelectionMgr.onLongPress(event); final void onLongPress(T event) { if (!event.isOverItem()) { return; } if (isRangeExtension(event)) { extendSelectionRange(event); } else { selectDocument(mDocFinder.apply(event)); } } private boolean onSingleRightClickUp(InputEvent event) { private boolean onSingleRightClickUp(T event) { return mRightClickHandler.apply(event); } Loading @@ -234,28 +329,18 @@ final class UserInputHandler extends GestureDetector.SimpleOnGestureListener private boolean eatRightClick() { if (mLastButtonState == MotionEvent.BUTTON_SECONDARY) { mLastButtonState = -1; ateRightClick = true; mAteRightClick = true; return true; } return false; } } public boolean onSingleRightClickUp(MotionEvent e) { try (InputEvent event = mEventConverter.apply(e)) { return mMouseDelegate.onSingleRightClickUp(event); } } // TODO: Isolate this hack...see if we can't get this solved at the platform level. public void setLastButtonState(int state) { mMouseDelegate.setLastButtonState(state); } private final class KeyInputHandler { // TODO: Refactor FocusManager to depend only on DocumentDetails so we can eliminate // difficult to test dependency on DocumentHolder. @Override public boolean onKey(DocumentHolder doc, int keyCode, KeyEvent event) { boolean onKey(DocumentHolder doc, int keyCode, KeyEvent event) { // Only handle key-down events. This is simpler, consistent with most other UIs, and // enables the handling of repeated key events from holding down a key. if (event.getAction() != KeyEvent.ACTION_DOWN) { Loading Loading @@ -287,12 +372,12 @@ final class UserInputHandler extends GestureDetector.SimpleOnGestureListener switch (keyCode) { case KeyEvent.KEYCODE_ENTER: if (event.isShiftPressed()) { onSelect(doc); selectDocument(doc); } // For non-shifted enter keypresses, fall through. case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_BUTTON_A: return mActivateHandler.accept(doc); return activateDocument(doc); case KeyEvent.KEYCODE_FORWARD_DEL: // This has to be handled here instead of in a keyboard shortcut, because // keyboard shortcuts all have to be modified with the 'Ctrl' key. Loading @@ -315,15 +400,6 @@ final class UserInputHandler extends GestureDetector.SimpleOnGestureListener return mSelectable.test(doc); } @FunctionalInterface interface EventHandler { boolean apply(InputEvent input); } @FunctionalInterface interface DocumentHandler { boolean accept(DocumentDetails doc); } /** Loading @@ -334,4 +410,14 @@ final class UserInputHandler extends GestureDetector.SimpleOnGestureListener int getAdapterPosition(); boolean isInSelectionHotspot(InputEvent event); } @FunctionalInterface interface EventHandler { boolean apply(InputEvent event); } @FunctionalInterface interface DocumentHandler { boolean accept(DocumentDetails doc); } } tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java +34 −232 File changed.Preview size limit exceeded, changes collapsed. Show changes tests/src/com/android/documentsui/dirlist/MultiSelectManager_SingleSelectTest.java 0 → 100644 +66 −0 Original line number Diff line number Diff line /* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.documentsui.dirlist; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.SmallTest; import com.android.documentsui.testing.dirlist.SelectionProbe; import com.android.documentsui.testing.dirlist.TestSelectionListener; import java.util.List; @SmallTest public class MultiSelectManager_SingleSelectTest extends AndroidTestCase { private static final List<String> ITEMS = TestData.create(100); private MultiSelectManager mManager; private TestSelectionListener mCallback; private TestDocumentsAdapter mAdapter; private SelectionProbe mSelection; @Override public void setUp() throws Exception { mCallback = new TestSelectionListener(); mAdapter = new TestDocumentsAdapter(ITEMS); mManager = new MultiSelectManager(mAdapter, MultiSelectManager.MODE_SINGLE); mManager.addCallback(mCallback); mSelection = new SelectionProbe(mManager); } public void testSimpleSelect() { mManager.toggleSelection(ITEMS.get(3)); mManager.toggleSelection(ITEMS.get(4)); mCallback.assertSelectionChanged(); mSelection.assertSelection(4); } public void testRangeSelectionNotEstablished() { mManager.toggleSelection(ITEMS.get(3)); mCallback.reset(); try { mManager.snapRangeSelection(10); fail("Should have thrown."); } catch (Exception expected) {} mCallback.assertSelectionUnchanged(); mSelection.assertSelection(3); } } Loading
src/com/android/documentsui/dirlist/DirectoryFragment.java +2 −2 Original line number Diff line number Diff line Loading @@ -142,7 +142,7 @@ public class DirectoryFragment extends Fragment private Model mModel; private MultiSelectManager mSelectionMgr; private Model.UpdateListener mModelUpdateListener = new ModelUpdateListener(); private UserInputHandler mInputHandler; private UserInputHandler<InputEvent> mInputHandler; private SelectionModeListener mSelectionModeListener; private FocusManager mFocusManager; Loading Loading @@ -280,7 +280,7 @@ public class DirectoryFragment extends Fragment // Make sure this is done after the RecyclerView is set up. mFocusManager = new FocusManager(context, mRecView, mModel); mInputHandler = new UserInputHandler( mInputHandler = new UserInputHandler<>( mSelectionMgr, mFocusManager, new Function<MotionEvent, InputEvent>() { Loading
src/com/android/documentsui/dirlist/MultiSelectManager.java +12 −68 Original line number Diff line number Diff line Loading @@ -21,13 +21,10 @@ import static com.android.documentsui.Shared.DEBUG; import android.annotation.IntDef; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.support.v7.widget.RecyclerView; import android.util.Log; import com.android.documentsui.Events.InputEvent; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; Loading @@ -39,6 +36,8 @@ import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.Nullable; /** * MultiSelectManager provides support traditional multi-item selection support to RecyclerView. * Additionally it can be configured to restrict selection to a single element, @see Loading @@ -62,7 +61,7 @@ public final class MultiSelectManager { private final DocumentsAdapter mAdapter; private final List<MultiSelectManager.Callback> mCallbacks = new ArrayList<>(1); private Range mRanger; private @Nullable Range mRanger; private boolean mSingleSelect; public MultiSelectManager(DocumentsAdapter adapter, @SelectionMode int mode) { Loading Loading @@ -223,70 +222,13 @@ public final class MultiSelectManager { } } @VisibleForTesting void onLongPress(InputEvent input) { if (DEBUG) Log.d(TAG, "Handling long press event."); if (!input.isOverItem()) { if (DEBUG) Log.i(TAG, "Cannot handle tap. No adapter position available."); } handleAdapterEvent(input); } boolean onSingleTapUp(InputEvent input) { if (DEBUG) Log.d(TAG, "Processing tap event."); if (!hasSelection()) { // No selection active - do nothing. return false; } if (!input.isOverItem()) { if (DEBUG) Log.d(TAG, "Activity has no position. Canceling selection."); clearSelection(); return false; } handleAdapterEvent(input); return true; } /** * Handles a change caused by a click on the item with the given position. If the Shift key is * held down, this performs a range select; otherwise, it simply toggles the item's selection * state. */ private void handleAdapterEvent(InputEvent input) { if (mRanger != null && input.isShiftKeyDown()) { mRanger.snapSelection(input.getItemPosition()); void snapSelection(int position) { mRanger.snapSelection(position); // We're being lazy here notifying even when something might not have changed. // To make this more correct, we'd need to update the Ranger class to return // information about what has changed. notifySelectionChanged(); } else { int position = input.getItemPosition(); toggleSelection(position); setSelectionRangeBegin(position); } } /** * A convenience method for toggling selection by adapter position. * * @param position Adapter position to toggle. */ private void toggleSelection(int position) { // Position may be special "no position" during certain // transitional phases. If so, skip handling of the event. if (position == RecyclerView.NO_POSITION) { if (DEBUG) Log.d(TAG, "Ignoring toggle for element with no position."); return; } String id = mAdapter.getModelId(position); if (id != null) { toggleSelection(id); } } /** Loading Loading @@ -329,7 +271,9 @@ public final class MultiSelectManager { * @param pos The new end position for the selection range. */ void snapRangeSelection(int pos) { assert(mRanger != null); if (!isRangeSelectionActive()) { throw new IllegalStateException("Range start point not set."); } mRanger.snapSelection(pos); notifySelectionChanged(); Loading
src/com/android/documentsui/dirlist/UserInputHandler.java +209 −123 Original line number Diff line number Diff line Loading @@ -16,6 +16,10 @@ package com.android.documentsui.dirlist; import static com.android.documentsui.Shared.DEBUG; import android.support.annotation.VisibleForTesting; import android.util.Log; import android.view.GestureDetector; import android.view.KeyEvent; import android.view.MotionEvent; Loading @@ -30,25 +34,29 @@ import java.util.function.Predicate; /** * Grand unified-ish gesture/event listener for items in the directory list. */ final class UserInputHandler extends GestureDetector.SimpleOnGestureListener public final class UserInputHandler<T extends InputEvent> extends GestureDetector.SimpleOnGestureListener implements KeyboardEventListener { private static final String TAG = "UserInputHandler"; private final MultiSelectManager mSelectionMgr; private final FocusHandler mFocusHandler; private final Function<MotionEvent, InputEvent> mEventConverter; private final Function<InputEvent, DocumentDetails> mDocFinder; private final Function<MotionEvent, T> mEventConverter; private final Function<T, DocumentDetails> mDocFinder; private final Predicate<DocumentDetails> mSelectable; private final EventHandler mRightClickHandler; private final DocumentHandler mActivateHandler; private final DocumentHandler mDeleteHandler; private final TouchInputDelegate mTouchDelegate; private final MouseInputDelegate mMouseDelegate; private final KeyInputHandler mKeyListener; public UserInputHandler( MultiSelectManager selectionMgr, FocusHandler focusHandler, Function<MotionEvent, InputEvent> eventConverter, Function<InputEvent, DocumentDetails> docFinder, Function<MotionEvent, T> eventConverter, Function<T, DocumentDetails> docFinder, Predicate<DocumentDetails> selectable, EventHandler rightClickHandler, DocumentHandler activateHandler, Loading @@ -65,55 +73,116 @@ final class UserInputHandler extends GestureDetector.SimpleOnGestureListener mTouchDelegate = new TouchInputDelegate(); mMouseDelegate = new MouseInputDelegate(); mKeyListener = new KeyInputHandler(); } @Override public boolean onSingleTapUp(MotionEvent e) { try (InputEvent event = mEventConverter.apply(e)) { try (T event = mEventConverter.apply(e)) { return onSingleTapUp(event); } } @VisibleForTesting boolean onSingleTapUp(T event) { return event.isMouseEvent() ? mMouseDelegate.onSingleTapUp(event) : mTouchDelegate.onSingleTapUp(event); } } @Override public boolean onSingleTapConfirmed(MotionEvent e) { try (InputEvent event = mEventConverter.apply(e)) { try (T event = mEventConverter.apply(e)) { return onSingleTapConfirmed(event); } } @VisibleForTesting boolean onSingleTapConfirmed(T event) { return event.isMouseEvent() ? mMouseDelegate.onSingleTapConfirmed(event) : mTouchDelegate.onSingleTapConfirmed(event); } } @Override public boolean onDoubleTap(MotionEvent e) { try (InputEvent event = mEventConverter.apply(e)) { try (T event = mEventConverter.apply(e)) { return onDoubleTap(event); } } @VisibleForTesting boolean onDoubleTap(T event) { return event.isMouseEvent() ? mMouseDelegate.onDoubleTap(event) : mTouchDelegate.onDoubleTap(event); } } @Override public void onLongPress(MotionEvent e) { try (InputEvent event = mEventConverter.apply(e)) { try (T event = mEventConverter.apply(e)) { onLongPress(event); } } @VisibleForTesting void onLongPress(T event) { if (event.isMouseEvent()) { mMouseDelegate.onLongPress(event); } mTouchDelegate.onLongPress(event); } public boolean onSingleRightClickUp(MotionEvent e) { try (T event = mEventConverter.apply(e)) { return mMouseDelegate.onSingleRightClickUp(event); } } @Override public boolean onKey(DocumentHolder doc, int keyCode, KeyEvent event) { return mKeyListener.onKey(doc, keyCode, event); } // TODO: Isolate this hack...see if we can't get this solved at the platform level. public void setLastButtonState(int state) { mMouseDelegate.setLastButtonState(state); } private boolean activateDocument(DocumentDetails doc) { return mActivateHandler.accept(doc); } private boolean onSelect(DocumentDetails doc) { private boolean selectDocument(DocumentDetails doc) { assert(doc != null); mSelectionMgr.toggleSelection(doc.getModelId()); mSelectionMgr.setSelectionRangeBegin(doc.getAdapterPosition()); return true; } boolean isRangeExtension(T event) { return event.isShiftKeyDown() && mSelectionMgr.isRangeSelectionActive(); } private void extendSelectionRange(T event) { mSelectionMgr.snapSelection(event.getItemPosition()); } private final class TouchInputDelegate { public boolean onSingleTapUp(InputEvent event) { if (mSelectionMgr.onSingleTapUp(event)) { boolean onSingleTapUp(T event) { if (!event.isOverItem()) { if (DEBUG) Log.d(TAG, "Tap on non-item. Clearing selection."); mSelectionMgr.clearSelection(); return false; } if (mSelectionMgr.hasSelection()) { if (isRangeExtension(event)) { mSelectionMgr.snapSelection(event.getItemPosition()); } else { selectDocument(mDocFinder.apply(event)); } return true; } Loading @@ -123,23 +192,31 @@ final class UserInputHandler extends GestureDetector.SimpleOnGestureListener // Touch events select if they occur in the selection hotspot, // otherwise they activate. return doc.isInSelectionHotspot(event) ? onSelect(doc) : mActivateHandler.accept(doc); ? selectDocument(doc) : activateDocument(doc); } return false; } public boolean onSingleTapConfirmed(InputEvent event) { boolean onSingleTapConfirmed(T event) { return false; } public boolean onDoubleTap(InputEvent event) { boolean onDoubleTap(T event) { return false; } public void onLongPress(InputEvent event) { mSelectionMgr.onLongPress(event); final void onLongPress(T event) { if (!event.isOverItem()) { return; } if (isRangeExtension(event)) { extendSelectionRange(event); } else { selectDocument(mDocFinder.apply(event)); } } } Loading @@ -155,18 +232,28 @@ final class UserInputHandler extends GestureDetector.SimpleOnGestureListener private int mLastButtonState = -1; // true when the previous event has consumed a right click motion event private boolean ateRightClick; private boolean mAteRightClick; // The event has been handled in onSingleTapUp private boolean handledTapUp; private boolean mHandledTapUp; public boolean onSingleTapUp(InputEvent event) { boolean onSingleTapUp(T event) { if (eatRightClick()) { return onSingleRightClickUp(event); } if (mSelectionMgr.onSingleTapUp(event)) { handledTapUp = true; if (!event.isOverItem()) { mSelectionMgr.clearSelection(); return false; } if (mSelectionMgr.hasSelection()) { if (isRangeExtension(event)) { extendSelectionRange(event); } else { selectDocument(mDocFinder.apply(event)); } mHandledTapUp = true; return true; } Loading @@ -181,17 +268,17 @@ final class UserInputHandler extends GestureDetector.SimpleOnGestureListener return false; } handledTapUp = true; return onSelect(doc); mHandledTapUp = true; return selectDocument(doc); } public boolean onSingleTapConfirmed(InputEvent event) { if (ateRightClick) { ateRightClick = false; boolean onSingleTapConfirmed(T event) { if (mAteRightClick) { mAteRightClick = false; return false; } if (handledTapUp) { handledTapUp = false; if (mHandledTapUp) { mHandledTapUp = false; return false; } Loading @@ -204,25 +291,33 @@ final class UserInputHandler extends GestureDetector.SimpleOnGestureListener return false; } return onSelect(doc); return selectDocument(doc); } public boolean onDoubleTap(InputEvent event) { handledTapUp = false; boolean onDoubleTap(T event) { mHandledTapUp = false; DocumentDetails doc = mDocFinder.apply(event); if (doc != null) { return mSelectionMgr.hasSelection() ? onSelect(doc) : mActivateHandler.accept(doc); ? selectDocument(doc) : activateDocument(doc); } return false; } public void onLongPress(InputEvent event) { mSelectionMgr.onLongPress(event); final void onLongPress(T event) { if (!event.isOverItem()) { return; } if (isRangeExtension(event)) { extendSelectionRange(event); } else { selectDocument(mDocFinder.apply(event)); } } private boolean onSingleRightClickUp(InputEvent event) { private boolean onSingleRightClickUp(T event) { return mRightClickHandler.apply(event); } Loading @@ -234,28 +329,18 @@ final class UserInputHandler extends GestureDetector.SimpleOnGestureListener private boolean eatRightClick() { if (mLastButtonState == MotionEvent.BUTTON_SECONDARY) { mLastButtonState = -1; ateRightClick = true; mAteRightClick = true; return true; } return false; } } public boolean onSingleRightClickUp(MotionEvent e) { try (InputEvent event = mEventConverter.apply(e)) { return mMouseDelegate.onSingleRightClickUp(event); } } // TODO: Isolate this hack...see if we can't get this solved at the platform level. public void setLastButtonState(int state) { mMouseDelegate.setLastButtonState(state); } private final class KeyInputHandler { // TODO: Refactor FocusManager to depend only on DocumentDetails so we can eliminate // difficult to test dependency on DocumentHolder. @Override public boolean onKey(DocumentHolder doc, int keyCode, KeyEvent event) { boolean onKey(DocumentHolder doc, int keyCode, KeyEvent event) { // Only handle key-down events. This is simpler, consistent with most other UIs, and // enables the handling of repeated key events from holding down a key. if (event.getAction() != KeyEvent.ACTION_DOWN) { Loading Loading @@ -287,12 +372,12 @@ final class UserInputHandler extends GestureDetector.SimpleOnGestureListener switch (keyCode) { case KeyEvent.KEYCODE_ENTER: if (event.isShiftPressed()) { onSelect(doc); selectDocument(doc); } // For non-shifted enter keypresses, fall through. case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_BUTTON_A: return mActivateHandler.accept(doc); return activateDocument(doc); case KeyEvent.KEYCODE_FORWARD_DEL: // This has to be handled here instead of in a keyboard shortcut, because // keyboard shortcuts all have to be modified with the 'Ctrl' key. Loading @@ -315,15 +400,6 @@ final class UserInputHandler extends GestureDetector.SimpleOnGestureListener return mSelectable.test(doc); } @FunctionalInterface interface EventHandler { boolean apply(InputEvent input); } @FunctionalInterface interface DocumentHandler { boolean accept(DocumentDetails doc); } /** Loading @@ -334,4 +410,14 @@ final class UserInputHandler extends GestureDetector.SimpleOnGestureListener int getAdapterPosition(); boolean isInSelectionHotspot(InputEvent event); } @FunctionalInterface interface EventHandler { boolean apply(InputEvent event); } @FunctionalInterface interface DocumentHandler { boolean accept(DocumentDetails doc); } }
tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java +34 −232 File changed.Preview size limit exceeded, changes collapsed. Show changes
tests/src/com/android/documentsui/dirlist/MultiSelectManager_SingleSelectTest.java 0 → 100644 +66 −0 Original line number Diff line number Diff line /* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.documentsui.dirlist; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.SmallTest; import com.android.documentsui.testing.dirlist.SelectionProbe; import com.android.documentsui.testing.dirlist.TestSelectionListener; import java.util.List; @SmallTest public class MultiSelectManager_SingleSelectTest extends AndroidTestCase { private static final List<String> ITEMS = TestData.create(100); private MultiSelectManager mManager; private TestSelectionListener mCallback; private TestDocumentsAdapter mAdapter; private SelectionProbe mSelection; @Override public void setUp() throws Exception { mCallback = new TestSelectionListener(); mAdapter = new TestDocumentsAdapter(ITEMS); mManager = new MultiSelectManager(mAdapter, MultiSelectManager.MODE_SINGLE); mManager.addCallback(mCallback); mSelection = new SelectionProbe(mManager); } public void testSimpleSelect() { mManager.toggleSelection(ITEMS.get(3)); mManager.toggleSelection(ITEMS.get(4)); mCallback.assertSelectionChanged(); mSelection.assertSelection(4); } public void testRangeSelectionNotEstablished() { mManager.toggleSelection(ITEMS.get(3)); mCallback.reset(); try { mManager.snapRangeSelection(10); fail("Should have thrown."); } catch (Exception expected) {} mCallback.assertSelectionUnchanged(); mSelection.assertSelection(3); } }