summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSteve McKay <smckay@google.com>2016-06-30 21:03:06 -0700
committerSteve McKay <smckay@google.com>2016-07-12 03:24:06 +0000
commit74c287706b4cd38e1c71938a49efc234ddec46ed (patch)
tree642357b8b1293b8278593174fc58fdf9321e4ae4
parent5ba81e88adf0306cbcc1589e9fe537c21c71b37a (diff)
Consolidate user input handling in single class.
But separate mouse and touch handling into independent (internal) handlers. Ensure we don't do band select on right click + drag. Bug: 29575607, 29548676 Change-Id: I247e3ba002751f2cda010125e0e7b4bdd745ac23
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/Events.java26
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/dirlist/BandController.java5
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java174
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/dirlist/DocumentHolder.java102
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/dirlist/FocusHandler.java51
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/dirlist/FocusManager.java20
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/dirlist/GestureListener.java118
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/dirlist/ItemEventListener.java132
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/dirlist/ListeningGestureDetector.java15
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java5
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/dirlist/UserInputHandler.java337
-rw-r--r--packages/DocumentsUI/tests/src/com/android/documentsui/TestInputEvent.java19
-rw-r--r--packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/DocumentHolderTest.java51
-rw-r--r--packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java9
-rw-r--r--packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/TestData.java30
-rw-r--r--packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/UserInputHandler_MouseTest.java148
-rw-r--r--packages/DocumentsUI/tests/src/com/android/documentsui/testing/TestPredicate.java47
17 files changed, 820 insertions, 469 deletions
diff --git a/packages/DocumentsUI/src/com/android/documentsui/Events.java b/packages/DocumentsUI/src/com/android/documentsui/Events.java
index 691f95a67af7..2d0dbe8ce321 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/Events.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/Events.java
@@ -115,7 +115,8 @@ public final class Events {
* A facade over MotionEvent primarily designed to permit for unit testing
* of related code.
*/
- public interface InputEvent {
+ public interface InputEvent extends AutoCloseable {
+ boolean isTouchEvent();
boolean isMouseEvent();
boolean isPrimaryButtonPressed();
boolean isSecondaryButtonPressed();
@@ -127,9 +128,15 @@ public final class Events {
/** Returns true if the action is the final release of a mouse or touch. */
boolean isActionUp();
+ // Eliminate the checked Exception from Autoclosable.
+ @Override
+ public void close();
+
Point getOrigin();
float getX();
float getY();
+ float getRawX();
+ float getRawY();
/** Returns true if the there is an item under the finger/cursor. */
boolean isOverItem();
@@ -138,7 +145,7 @@ public final class Events {
int getItemPosition();
}
- public static final class MotionInputEvent implements InputEvent, AutoCloseable {
+ public static final class MotionInputEvent implements InputEvent {
private static final String TAG = "MotionInputEvent";
private static final Pools.SimplePool<MotionInputEvent> sPool = new Pools.SimplePool<>(1);
@@ -205,6 +212,11 @@ public final class Events {
}
@Override
+ public boolean isTouchEvent() {
+ return Events.isTouchEvent(mEvent);
+ }
+
+ @Override
public boolean isMouseEvent() {
return Events.isMouseEvent(mEvent);
}
@@ -250,6 +262,16 @@ public final class Events {
}
@Override
+ public float getRawX() {
+ return mEvent.getRawX();
+ }
+
+ @Override
+ public float getRawY() {
+ return mEvent.getRawY();
+ }
+
+ @Override
public boolean isOverItem() {
return getItemPosition() != RecyclerView.NO_POSITION;
}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/BandController.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/BandController.java
index 7320dc0ef98f..8f520367e602 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/BandController.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/BandController.java
@@ -178,6 +178,11 @@ public class BandController extends RecyclerView.OnScrollListener {
}
private boolean handleEvent(MotionInputEvent e) {
+ // Don't start, or extend bands on right click.
+ if (e.isSecondaryButtonPressed()) {
+ return false;
+ }
+
if (!e.isMouseEvent() && isActive()) {
// Weird things happen if we keep up band select
// when touch events happen.
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
index ce4e9937e7b8..3980cfcd5525 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -63,6 +63,7 @@ import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
+import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
@@ -75,6 +76,7 @@ import com.android.documentsui.DirectoryResult;
import com.android.documentsui.DocumentClipper;
import com.android.documentsui.DocumentsActivity;
import com.android.documentsui.DocumentsApplication;
+import com.android.documentsui.Events.InputEvent;
import com.android.documentsui.Events.MotionInputEvent;
import com.android.documentsui.ItemDragListener;
import com.android.documentsui.MenuManager;
@@ -92,6 +94,7 @@ import com.android.documentsui.State;
import com.android.documentsui.State.ViewMode;
import com.android.documentsui.UrisSupplier;
import com.android.documentsui.dirlist.MultiSelectManager.Selection;
+import com.android.documentsui.dirlist.UserInputHandler.DocumentDetails;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.RootInfo;
import com.android.documentsui.services.FileOperation;
@@ -106,6 +109,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
+import java.util.function.Function;
import javax.annotation.Nullable;
@@ -136,9 +140,9 @@ public class DirectoryFragment extends Fragment
private static final int LOADER_ID = 42;
private Model mModel;
- private MultiSelectManager mSelectionManager;
+ private MultiSelectManager mSelectionMgr;
private Model.UpdateListener mModelUpdateListener = new ModelUpdateListener();
- private ItemEventListener mItemEventListener;
+ private UserInputHandler mInputHandler;
private SelectionModeListener mSelectionModeListener;
private FocusManager mFocusManager;
@@ -240,7 +244,7 @@ public class DirectoryFragment extends Fragment
@Override
public void onDestroyView() {
- mSelectionManager.clearSelection();
+ mSelectionMgr.clearSelection();
// Cancel any outstanding thumbnail requests
final int count = mRecView.getChildCount();
@@ -296,46 +300,49 @@ 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.
- mSelectionManager = new MultiSelectManager(
+ mSelectionMgr = new MultiSelectManager(
mAdapter,
state.allowMultiple
? MultiSelectManager.MODE_MULTIPLE
: MultiSelectManager.MODE_SINGLE);
- GestureListener gestureListener = new GestureListener(
- mSelectionManager,
- mRecView,
+ // Make sure this is done after the RecyclerView is set up.
+ mFocusManager = new FocusManager(context, mRecView, mModel);
+
+ mInputHandler = new UserInputHandler(
+ mSelectionMgr,
+ mFocusManager,
+ new Function<MotionEvent, InputEvent>() {
+ @Override
+ public InputEvent apply(MotionEvent t) {
+ return MotionInputEvent.obtain(t, mRecView);
+ }
+ },
this::getTarget,
- this::onDoubleTap,
- this::onRightClick);
+ this::canSelect,
+ this::onRightClick,
+ this::onActivate,
+ (DocumentDetails ignored) -> {
+ return onDeleteSelectedDocuments();
+ });
mGestureDetector =
- new ListeningGestureDetector(this.getContext(), mDragHelper, gestureListener);
+ new ListeningGestureDetector(this.getContext(), mDragHelper, mInputHandler);
mRecView.addOnItemTouchListener(mGestureDetector);
mEmptyView.setOnTouchListener(mGestureDetector);
if (state.allowMultiple) {
- mBandController = new BandController(mRecView, mAdapter, mSelectionManager);
+ mBandController = new BandController(mRecView, mAdapter, mSelectionMgr);
}
mSelectionModeListener = new SelectionModeListener();
- mSelectionManager.addCallback(mSelectionModeListener);
+ mSelectionMgr.addCallback(mSelectionModeListener);
mModel = new Model();
mModel.addUpdateListener(mAdapter);
mModel.addUpdateListener(mModelUpdateListener);
- // Make sure this is done after the RecyclerView is set up.
- mFocusManager = new FocusManager(context, mRecView, mModel);
-
- mItemEventListener = new ItemEventListener(
- mSelectionManager,
- mFocusManager,
- this::handleViewItem,
- this::deleteDocuments,
- this::canSelect);
-
final BaseActivity activity = getBaseActivity();
mTuner = activity.createFragmentTuner();
mMenuManager = activity.getMenuManager();
@@ -351,7 +358,7 @@ public class DirectoryFragment extends Fragment
}
public void retainState(RetainedState state) {
- state.selection = mSelectionManager.getSelection(new Selection());
+ state.selection = mSelectionMgr.getSelection(new Selection());
}
@Override
@@ -419,49 +426,37 @@ public class DirectoryFragment extends Fragment
FileOperations.start(getContext(), operation, mFileOpCallback);
}
- protected boolean onDoubleTap(MotionInputEvent event) {
- if (event.isMouseEvent()) {
- String id = getModelId(event);
- if (id != null) {
- return handleViewItem(id);
- }
- }
- return false;
- }
-
- protected boolean onRightClick(MotionInputEvent e) {
+ protected boolean onRightClick(InputEvent e) {
if (e.getItemPosition() != RecyclerView.NO_POSITION) {
- final DocumentHolder holder = getTarget(e);
- String modelId = getModelId(holder.itemView);
- if (!mSelectionManager.getSelection().contains(modelId)) {
- mSelectionManager.clearSelection();
- // Set selection on the one single item
- List<String> ids = Collections.singletonList(modelId);
- mSelectionManager.setItemsSelected(ids, true);
+ final DocumentHolder doc = getTarget(e);
+ if (!mSelectionMgr.getSelection().contains(doc.modelId)) {
+ mSelectionMgr.replaceSelection(Collections.singleton(doc.modelId));
}
// We are registering for context menu here so long-press doesn't trigger this
// floating context menu, and then quickly unregister right afterwards
- registerForContextMenu(holder.itemView);
- mRecView.showContextMenuForChild(holder.itemView,
- e.getX() - holder.itemView.getLeft(), e.getY() - holder.itemView.getTop());
- unregisterForContextMenu(holder.itemView);
+ registerForContextMenu(doc.itemView);
+ mRecView.showContextMenuForChild(doc.itemView,
+ e.getX() - doc.itemView.getLeft(), e.getY() - doc.itemView.getTop());
+ unregisterForContextMenu(doc.itemView);
+ return true;
}
+
// If there was no corresponding item pos, that means user right-clicked on the blank
// pane
// We would want to show different options then, and not select any item
// The blank pane could be the recyclerView or the emptyView, so we need to register
// according to whichever one is visible
- else if (mEmptyView.getVisibility() == View.VISIBLE) {
+ if (mEmptyView.getVisibility() == View.VISIBLE) {
registerForContextMenu(mEmptyView);
mEmptyView.showContextMenu(e.getX(), e.getY());
unregisterForContextMenu(mEmptyView);
return true;
- } else {
- registerForContextMenu(mRecView);
- mRecView.showContextMenu(e.getX(), e.getY());
- unregisterForContextMenu(mRecView);
}
+
+ registerForContextMenu(mRecView);
+ mRecView.showContextMenu(e.getX(), e.getY());
+ unregisterForContextMenu(mRecView);
return true;
}
@@ -478,7 +473,7 @@ public class DirectoryFragment extends Fragment
if (mTuner.isDocumentEnabled(docMimeType, docFlags)) {
final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
getBaseActivity().onDocumentPicked(doc, mModel);
- mSelectionManager.clearSelection();
+ mSelectionMgr.clearSelection();
return true;
}
return false;
@@ -643,7 +638,7 @@ public class DirectoryFragment extends Fragment
@Override
public void onSelectionChanged() {
- mSelectionManager.getSelection(mSelected);
+ mSelectionMgr.getSelection(mSelected);
if (mSelected.size() > 0) {
if (DEBUG) Log.d(TAG, "Maybe starting action mode.");
if (mActionMode == null) {
@@ -673,7 +668,7 @@ public class DirectoryFragment extends Fragment
if (DEBUG) Log.d(TAG, "Handling action mode destroyed.");
mActionMode = null;
// clear selection
- mSelectionManager.clearSelection();
+ mSelectionMgr.clearSelection();
mSelected.clear();
mDirectoryCount = 0;
@@ -704,7 +699,7 @@ public class DirectoryFragment extends Fragment
mRecView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
}
- int size = mSelectionManager.getSelection().size();
+ int size = mSelectionMgr.getSelection().size();
mode.getMenuInflater().inflate(R.menu.mode_directory, menu);
mode.setTitle(TextUtils.formatSelectedCount(size));
@@ -752,7 +747,7 @@ public class DirectoryFragment extends Fragment
@Override
public boolean canRename() {
- return mNoRenameCount == 0 && mSelectionManager.getSelection().size() == 1;
+ return mNoRenameCount == 0 && mSelectionMgr.getSelection().size() == 1;
}
private void updateActionMenu() {
@@ -768,7 +763,7 @@ public class DirectoryFragment extends Fragment
}
private boolean handleMenuItemClick(MenuItem item) {
- Selection selection = mSelectionManager.getSelection(new Selection());
+ Selection selection = mSelectionMgr.getSelection(new Selection());
switch (item.getItemId()) {
case R.id.menu_open:
@@ -835,9 +830,9 @@ public class DirectoryFragment extends Fragment
}
public final boolean onBackPressed() {
- if (mSelectionManager.hasSelection()) {
+ if (mSelectionMgr.hasSelection()) {
if (DEBUG) Log.d(TAG, "Clearing selection on selection manager.");
- mSelectionManager.clearSelection();
+ mSelectionMgr.clearSelection();
return true;
}
return false;
@@ -949,6 +944,29 @@ public class DirectoryFragment extends Fragment
return message;
}
+ private boolean onDeleteSelectedDocuments() {
+ if (mSelectionMgr.hasSelection()) {
+ deleteDocuments(mSelectionMgr.getSelection(new Selection()));
+ }
+ return false;
+ }
+
+ private boolean onActivate(DocumentDetails doc) {
+ // Toggle selection if we're in selection mode, othewise, view item.
+ if (mSelectionMgr.hasSelection()) {
+ mSelectionMgr.toggleSelection(doc.getModelId());
+ } else {
+ handleViewItem(doc.getModelId());
+ }
+ return true;
+ }
+
+// private boolean onSelect(DocumentDetails doc) {
+// mSelectionMgr.toggleSelection(doc.getModelId());
+// mSelectionMgr.setSelectionRangeBegin(doc.getAdapterPosition());
+// return true;
+// }
+
private void deleteDocuments(final Selection selected) {
Metrics.logUserAction(getContext(), Metrics.USER_ACTION_DELETE);
@@ -1100,7 +1118,7 @@ public class DirectoryFragment extends Fragment
@Override
public void initDocumentHolder(DocumentHolder holder) {
- holder.addEventListener(mItemEventListener);
+ holder.addKeyEventListener(mInputHandler);
holder.itemView.setOnFocusChangeListener(mFocusManager);
}
@@ -1186,11 +1204,11 @@ public class DirectoryFragment extends Fragment
public void copySelectedToClipboard() {
Metrics.logUserAction(getContext(), Metrics.USER_ACTION_COPY_CLIPBOARD);
- Selection selection = mSelectionManager.getSelection(new Selection());
+ Selection selection = mSelectionMgr.getSelection(new Selection());
if (selection.isEmpty()) {
return;
}
- mSelectionManager.clearSelection();
+ mSelectionMgr.clearSelection();
mClipper.clipDocumentsForCopy(mModel::getItemUri, selection);
@@ -1200,11 +1218,11 @@ public class DirectoryFragment extends Fragment
public void cutSelectedToClipboard() {
Metrics.logUserAction(getContext(), Metrics.USER_ACTION_CUT_CLIPBOARD);
- Selection selection = mSelectionManager.getSelection(new Selection());
+ Selection selection = mSelectionMgr.getSelection(new Selection());
if (selection.isEmpty()) {
return;
}
- mSelectionManager.clearSelection();
+ mSelectionMgr.clearSelection();
mClipper.clipDocumentsForCut(mModel::getItemUri, selection, getDisplayState().stack.peek());
@@ -1239,7 +1257,7 @@ public class DirectoryFragment extends Fragment
}
// Only select things currently visible in the adapter.
- boolean changed = mSelectionManager.setItemsSelected(enabled, true);
+ boolean changed = mSelectionMgr.setItemsSelected(enabled, true);
if (changed) {
updateDisplayState();
}
@@ -1277,7 +1295,7 @@ public class DirectoryFragment extends Fragment
void dragStopped(boolean result) {
if (result) {
- mSelectionManager.clearSelection();
+ mSelectionMgr.clearSelection();
}
}
@@ -1363,19 +1381,7 @@ public class DirectoryFragment extends Fragment
}
}
- /**
- * Gets the model ID for a given motion event (using the event position)
- */
- private String getModelId(MotionInputEvent e) {
- RecyclerView.ViewHolder vh = getTarget(e);
- if (vh instanceof DocumentHolder) {
- return ((DocumentHolder) vh).modelId;
- } else {
- return null;
- }
- }
-
- private @Nullable DocumentHolder getTarget(MotionInputEvent e) {
+ private @Nullable DocumentHolder getTarget(InputEvent e) {
View childView = mRecView.findChildViewUnder(e.getX(), e.getY());
if (childView != null) {
return (DocumentHolder) mRecView.getChildViewHolder(childView);
@@ -1423,7 +1429,7 @@ public class DirectoryFragment extends Fragment
@Override
public boolean isSelected(String modelId) {
- return mSelectionManager.getSelection().contains(modelId);
+ return mSelectionMgr.getSelection().contains(modelId);
}
private final class ModelUpdateListener implements Model.UpdateListener {
@@ -1480,7 +1486,7 @@ public class DirectoryFragment extends Fragment
private DocumentInfo getSingleSelectedDocument(Selection selection) {
assert (selection.size() == 1);
- final List<DocumentInfo> docs = mModel.getDocuments(mSelectionManager.getSelection());
+ final List<DocumentInfo> docs = mModel.getDocuments(mSelectionMgr.getSelection());
assert (docs.size() == 1);
return docs.get(0);
}
@@ -1489,7 +1495,7 @@ public class DirectoryFragment extends Fragment
new DragStartHelper.OnDragStartListener() {
@Override
public boolean onDragStart(View v, DragStartHelper helper) {
- Selection selection = mSelectionManager.getSelection();
+ Selection selection = mSelectionMgr.getSelection();
if (v == null) {
Log.d(TAG, "Ignoring drag event, null view");
@@ -1532,6 +1538,10 @@ public class DirectoryFragment extends Fragment
}
};
+ private boolean canSelect(DocumentDetails doc) {
+ return canSelect(doc.getModelId());
+ }
+
private boolean canSelect(String modelId) {
// TODO: Combine this method with onBeforeItemStateChange, as both of them are almost
@@ -1662,7 +1672,7 @@ public class DirectoryFragment extends Fragment
updateLayout(state.derivedMode);
if (mRestoredSelection != null) {
- mSelectionManager.restoreSelection(mRestoredSelection);
+ mSelectionMgr.restoreSelection(mRestoredSelection);
// Note, we'll take care of cleaning up retained selection
// in the selection handler where we already have some
// specialized code to handle when selection was restored.
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DocumentHolder.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DocumentHolder.java
index 2288fe74184f..c2b0bf21ff74 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DocumentHolder.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DocumentHolder.java
@@ -24,28 +24,31 @@ import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.view.KeyEvent;
import android.view.LayoutInflater;
-import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
-import com.android.documentsui.Events;
+import com.android.documentsui.Events.InputEvent;
import com.android.documentsui.R;
import com.android.documentsui.State;
+import com.android.documentsui.dirlist.UserInputHandler.DocumentDetails;
public abstract class DocumentHolder
extends RecyclerView.ViewHolder
- implements View.OnKeyListener {
+ implements View.OnKeyListener,
+ DocumentDetails {
static final float DISABLED_ALPHA = 0.3f;
+ @Deprecated // Public access is deprecated, use #getModelId.
public @Nullable String modelId;
final Context mContext;
final @ColorInt int mDefaultBgColor;
final @ColorInt int mSelectedBgColor;
- DocumentHolder.EventListener mEventListener;
- private View.OnKeyListener mKeyListener;
+ // See #addKeyEventListener for details on the need for this field.
+ KeyboardEventListener mKeyEventListener;
+
private View mSelectionHotspot;
@@ -74,6 +77,11 @@ public abstract class DocumentHolder
*/
public abstract void bind(Cursor cursor, String modelId, State state);
+ @Override
+ public String getModelId() {
+ return modelId;
+ }
+
/**
* Makes the associated item view appear selected. Note that this merely affects the appearance
* of the view, it doesn't actually select the item.
@@ -107,54 +115,36 @@ public abstract class DocumentHolder
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
- // Event listener should always be set.
- assert(mEventListener != null);
-
- return mEventListener.onKey(this, keyCode, event);
- }
-
- public void addEventListener(DocumentHolder.EventListener listener) {
- // Just handle one for now; switch to a list if necessary.
- assert(mEventListener == null);
- mEventListener = listener;
+ assert(mKeyEventListener != null);
+ return mKeyEventListener.onKey(this, keyCode, event);
}
- public void addOnKeyListener(View.OnKeyListener listener) {
- // Just handle one for now; switch to a list if necessary.
- assert(mKeyListener == null);
- mKeyListener = listener;
+ /**
+ * Installs a delegate to receive keyboard input events. This arrangement is necessitated
+ * by the fact that a single listener cannot listen to all keyboard events
+ * on RecyclerView (our parent view). Not sure why this is, but have been
+ * assured it is the case.
+ *
+ * <p>Ideally we'd not involve DocumentHolder in propagation of events like this.
+ */
+ public void addKeyEventListener(KeyboardEventListener listener) {
+ assert(mKeyEventListener == null);
+ mKeyEventListener = listener;
}
- public boolean onSingleTapUp(MotionEvent event) {
- if (Events.isMouseEvent(event)) {
- // Mouse clicks select.
- // TODO: && input.isPrimaryButtonPressed(), but it is returning false.
- if (mEventListener != null) {
- return mEventListener.onSelect(this);
- }
- } else if (Events.isTouchEvent(event)) {
- // Touch events select if they occur in the selection hotspot, otherwise they activate.
- if (mEventListener == null) {
- return false;
- }
-
- // Do everything in global coordinates - it makes things simpler.
- int[] coords = new int[2];
- mSelectionHotspot.getLocationOnScreen(coords);
- Rect rect = new Rect(coords[0], coords[1], coords[0] + mSelectionHotspot.getWidth(),
- coords[1] + mSelectionHotspot.getHeight());
-
- // If the tap occurred within the icon rect, consider it a selection.
- if (rect.contains((int) event.getRawX(), (int) event.getRawY())) {
- return mEventListener.onSelect(this);
- } else {
- return mEventListener.onActivate(this);
- }
- }
- return false;
+ @Override
+ public boolean isInSelectionHotspot(InputEvent event) {
+ // Do everything in global coordinates - it makes things simpler.
+ int[] coords = new int[2];
+ mSelectionHotspot.getLocationOnScreen(coords);
+ Rect rect = new Rect(coords[0], coords[1], coords[0] + mSelectionHotspot.getWidth(),
+ coords[1] + mSelectionHotspot.getHeight());
+
+ // If the tap occurred within the icon rect, consider it a selection.
+ return rect.contains((int) event.getRawX(), (int) event.getRawY());
}
- static void setEnabledRecursive(View itemView, boolean enabled) {
+ static void setEnabledRecursive(View itemView, boolean enabled) {
if (itemView == null) return;
if (itemView.isEnabled() == enabled) return;
itemView.setEnabled(enabled);
@@ -174,23 +164,9 @@ public abstract class DocumentHolder
/**
* Implement this in order to be able to respond to events coming from DocumentHolders.
+ * TODO: Make this bubble up logic events rather than having imperative commands.
*/
- interface EventListener {
- /**
- * Handles activation events on the document holder.
- *
- * @param doc The target DocumentHolder
- * @return Whether the event was handled.
- */
- public boolean onActivate(DocumentHolder doc);
-
- /**
- * Handles selection events on the document holder.
- *
- * @param doc The target DocumentHolder
- * @return Whether the event was handled.
- */
- public boolean onSelect(DocumentHolder doc);
+ interface KeyboardEventListener {
/**
* Handles key events on the document holder.
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/FocusHandler.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/FocusHandler.java
new file mode 100644
index 000000000000..ba26d65065f2
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/FocusHandler.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2016 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.view.KeyEvent;
+import android.view.View;
+
+/**
+ * A class that handles navigation and focus within the DirectoryFragment.
+ */
+interface FocusHandler extends View.OnFocusChangeListener {
+
+ /**
+ * Handles navigation (setting focus, adjusting selection if needed) arising from incoming key
+ * events.
+ *
+ * @param doc The DocumentHolder receiving the key event.
+ * @param keyCode
+ * @param event
+ * @return Whether the event was handled.
+ */
+ boolean handleKey(DocumentHolder doc, int keyCode, KeyEvent event);
+
+ @Override
+ void onFocusChange(View v, boolean hasFocus);
+
+ /**
+ * Requests focus on the item that last had focus. Scrolls to that item if necessary.
+ */
+ void restoreLastFocus();
+
+ /**
+ * @return The adapter position of the last focused item.
+ */
+ int getFocusPosition();
+
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/FocusManager.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/FocusManager.java
index f274df37f916..1be2f65796f4 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/FocusManager.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/FocusManager.java
@@ -49,7 +49,7 @@ import java.util.TimerTask;
/**
* A class that handles navigation and focus within the DirectoryFragment.
*/
-class FocusManager implements View.OnFocusChangeListener {
+final class FocusManager implements FocusHandler {
private static final String TAG = "FocusManager";
private RecyclerView mView;
@@ -70,15 +70,7 @@ class FocusManager implements View.OnFocusChangeListener {
mSearchHelper = new TitleSearchHelper(context);
}
- /**
- * Handles navigation (setting focus, adjusting selection if needed) arising from incoming key
- * events.
- *
- * @param doc The DocumentHolder receiving the key event.
- * @param keyCode
- * @param event
- * @return Whether the event was handled.
- */
+ @Override
public boolean handleKey(DocumentHolder doc, int keyCode, KeyEvent event) {
// Search helper gets first crack, for doing type-to-focus.
if (mSearchHelper.handleKey(doc, keyCode, event)) {
@@ -116,9 +108,7 @@ class FocusManager implements View.OnFocusChangeListener {
}
}
- /**
- * Requests focus on the item that last had focus. Scrolls to that item if necessary.
- */
+ @Override
public void restoreLastFocus() {
if (mAdapter.getItemCount() == 0) {
// Nothing to focus.
@@ -134,9 +124,7 @@ class FocusManager implements View.OnFocusChangeListener {
}
}
- /**
- * @return The adapter position of the last focused item.
- */
+ @Override
public int getFocusPosition() {
return mLastFocusPosition;
}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/GestureListener.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/GestureListener.java
deleted file mode 100644
index 1af26d0e7064..000000000000
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/GestureListener.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright (C) 2016 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.support.v7.widget.RecyclerView;
-import android.view.GestureDetector;
-import android.view.MotionEvent;
-
-import com.android.documentsui.Events;
-import com.android.documentsui.Events.MotionInputEvent;
-
-import java.util.function.Function;
-import java.util.function.Predicate;
-
-/**
- * The gesture listener for items in the directly list, interprets gestures, and sends the
- * events to the target DocumentHolder, whence they are routed to the appropriate listener.
- */
-final class GestureListener extends GestureDetector.SimpleOnGestureListener {
- // From the RecyclerView, we get two events sent to
- // ListeningGestureDetector#onInterceptTouchEvent on a mouse click; we first get an
- // ACTION_DOWN Event for clicking on the mouse, and then an ACTION_UP event from releasing
- // the mouse click. ACTION_UP event doesn't have information regarding the button (primary
- // vs. secondary), so we have to save that somewhere first from ACTION_DOWN, and then reuse
- // it later. The ACTION_DOWN event doesn't get forwarded to GestureListener, so we have open
- // up a public set method to set it.
- private int mLastButtonState = -1;
- private MultiSelectManager mSelectionMgr;
- private RecyclerView mRecView;
- private Function<MotionInputEvent, DocumentHolder> mDocFinder;
- private Predicate<MotionInputEvent> mDoubleTapHandler;
- private Predicate<MotionInputEvent> mRightClickHandler;
-
- public GestureListener(
- MultiSelectManager selectionMgr,
- RecyclerView recView,
- Function<MotionInputEvent, DocumentHolder> docFinder,
- Predicate<MotionInputEvent> doubleTapHandler,
- Predicate<MotionInputEvent> rightClickHandler) {
- mSelectionMgr = selectionMgr;
- mRecView = recView;
- mDocFinder = docFinder;
- mDoubleTapHandler = doubleTapHandler;
- mRightClickHandler = rightClickHandler;
- }
-
- public void setLastButtonState(int state) {
- mLastButtonState = state;
- }
-
- @Override
- public boolean onSingleTapUp(MotionEvent e) {
- // Single tap logic:
- // We first see if it's a mouse event, and if it was right click by checking on
- // @{code ListeningGestureDetector#mLastButtonState}
- // If the selection manager is active, it gets first whack at handling tap
- // events. Otherwise, tap events are routed to the target DocumentHolder.
- if (Events.isMouseEvent(e) && mLastButtonState == MotionEvent.BUTTON_SECONDARY) {
- mLastButtonState = -1;
- return onRightClick(e);
- }
-
- try (MotionInputEvent event = MotionInputEvent.obtain(e, mRecView)) {
- boolean handled = mSelectionMgr.onSingleTapUp(event);
-
- if (handled) {
- return handled;
- }
-
- // Give the DocumentHolder a crack at the event.
- DocumentHolder holder = mDocFinder.apply(event);
- if (holder != null) {
- handled = holder.onSingleTapUp(e);
- }
-
- return handled;
- }
- }
-
- @Override
- public void onLongPress(MotionEvent e) {
- // Long-press events get routed directly to the selection manager. They can be
- // changed to route through the DocumentHolder if necessary.
- try (MotionInputEvent event = MotionInputEvent.obtain(e, mRecView)) {
- mSelectionMgr.onLongPress(event);
- }
- }
-
- @Override
- public boolean onDoubleTap(MotionEvent e) {
- // Double-tap events are handled directly by the DirectoryFragment. They can be changed
- // to route through the DocumentHolder if necessary.
-
- try (MotionInputEvent event = MotionInputEvent.obtain(e, mRecView)) {
- return mDoubleTapHandler.test(event);
- }
- }
-
- public boolean onRightClick(MotionEvent e) {
- try (MotionInputEvent event = MotionInputEvent.obtain(e, mRecView)) {
- return mRightClickHandler.test(event);
- }
- }
-} \ No newline at end of file
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/ItemEventListener.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/ItemEventListener.java
deleted file mode 100644
index cffba8d581d1..000000000000
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/ItemEventListener.java
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * Copyright (C) 2016 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.view.KeyEvent;
-
-import com.android.documentsui.Events;
-import com.android.documentsui.dirlist.MultiSelectManager.Selection;
-
-import java.util.function.Consumer;
-import java.util.function.Predicate;
-
-/**
- * Handles click/tap/key events on individual DocumentHolders.
- */
-class ItemEventListener implements DocumentHolder.EventListener {
- private MultiSelectManager mSelectionManager;
- private FocusManager mFocusManager;
-
- private Consumer<String> mViewItemCallback;
- private Consumer<Selection> mDeleteDocumentsCallback;
- private Predicate<String> mCanSelectPredicate;
-
- public ItemEventListener(
- MultiSelectManager selectionManager,
- FocusManager focusManager,
- Consumer<String> viewItemCallback,
- Consumer<Selection> deleteDocumentsCallback,
- Predicate<String> canSelectPredicate) {
-
- mSelectionManager = selectionManager;
- mFocusManager = focusManager;
- mViewItemCallback = viewItemCallback;
- mDeleteDocumentsCallback = deleteDocumentsCallback;
- mCanSelectPredicate = canSelectPredicate;
- }
-
- @Override
- public boolean onActivate(DocumentHolder doc) {
- // Toggle selection if we're in selection mode, othewise, view item.
- if (mSelectionManager.hasSelection()) {
- mSelectionManager.toggleSelection(doc.modelId);
- } else {
- mViewItemCallback.accept(doc.modelId);
- }
- return true;
- }
-
- @Override
- public boolean onSelect(DocumentHolder doc) {
- mSelectionManager.toggleSelection(doc.modelId);
- mSelectionManager.setSelectionRangeBegin(doc.getAdapterPosition());
- return true;
- }
-
- @Override
- public 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) {
- return false;
- }
-
- // Ignore tab key events. Those should be handled by the top-level key handler.
- if (keyCode == KeyEvent.KEYCODE_TAB) {
- return false;
- }
-
- if (mFocusManager.handleKey(doc, keyCode, event)) {
- // Handle range selection adjustments. Extending the selection will adjust the
- // bounds of the in-progress range selection. Each time an unshifted navigation
- // event is received, the range selection is restarted.
- if (shouldExtendSelection(doc, event)) {
- if (!mSelectionManager.isRangeSelectionActive()) {
- // Start a range selection if one isn't active
- mSelectionManager.startRangeSelection(doc.getAdapterPosition());
- }
- mSelectionManager.snapRangeSelection(mFocusManager.getFocusPosition());
- } else {
- mSelectionManager.endRangeSelection();
- }
- return true;
- }
-
- // Handle enter key events
- switch (keyCode) {
- case KeyEvent.KEYCODE_ENTER:
- if (event.isShiftPressed()) {
- return onSelect(doc);
- }
- // For non-shifted enter keypresses, fall through.
- case KeyEvent.KEYCODE_DPAD_CENTER:
- case KeyEvent.KEYCODE_BUTTON_A:
- return onActivate(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.
- if (mSelectionManager.hasSelection()) {
- Selection selection = mSelectionManager.getSelection(new Selection());
- mDeleteDocumentsCallback.accept(selection);
- }
- // Always handle the key, even if there was nothing to delete. This is a
- // precaution to prevent other handlers from potentially picking up the event
- // and triggering extra behaviours.
- return true;
- }
-
- return false;
- }
-
- private boolean shouldExtendSelection(DocumentHolder doc, KeyEvent event) {
- if (!Events.isNavigationKeyCode(event.getKeyCode()) || !event.isShiftPressed()) {
- return false;
- }
-
- return mCanSelectPredicate.test(doc.modelId);
- }
-}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/ListeningGestureDetector.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/ListeningGestureDetector.java
index 50e595d39605..85ff6ed44119 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/ListeningGestureDetector.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/ListeningGestureDetector.java
@@ -34,20 +34,21 @@ final class ListeningGestureDetector extends GestureDetector
implements OnItemTouchListener, OnTouchListener {
private DragStartHelper mDragHelper;
- private GestureListener mGestureListener;
+ private UserInputHandler mInputHandler;
public ListeningGestureDetector(
- Context context, DragStartHelper dragHelper, GestureListener listener) {
- super(context, listener);
+ Context context, DragStartHelper dragHelper, UserInputHandler handler) {
+ super(context, handler);
mDragHelper = dragHelper;
- mGestureListener = listener;
- setOnDoubleTapListener(listener);
+ mInputHandler = handler;
+ setOnDoubleTapListener(handler);
}
@Override
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
+ // TODO: If possible, move this into UserInputHandler.
if (e.getAction() == MotionEvent.ACTION_DOWN && Events.isMouseEvent(e)) {
- mGestureListener.setLastButtonState(e.getButtonState());
+ mInputHandler.setLastButtonState(e.getButtonState());
}
// Detect drag events. When a drag is detected, intercept the rest of the gesture.
@@ -78,7 +79,7 @@ final class ListeningGestureDetector extends GestureDetector
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getButtonState() == MotionEvent.BUTTON_SECONDARY) {
- return mGestureListener.onRightClick(event);
+ return mInputHandler.onSingleRightClickUp(event);
}
return false;
}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java
index e0fc5414fa92..e58971a4f443 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java
@@ -158,6 +158,11 @@ public final class MultiSelectManager {
return dest;
}
+ public void replaceSelection(Iterable<String> ids) {
+ clearSelection();
+ setItemsSelected(ids, true);
+ }
+
/**
* Returns an unordered array of selected positions, including any
* provisional selection currently in effect.
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/UserInputHandler.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/UserInputHandler.java
new file mode 100644
index 000000000000..943815c8dc41
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/UserInputHandler.java
@@ -0,0 +1,337 @@
+/*
+ * Copyright (C) 2016 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.view.GestureDetector;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+import com.android.documentsui.Events;
+import com.android.documentsui.Events.InputEvent;
+import com.android.documentsui.dirlist.DocumentHolder.KeyboardEventListener;
+
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+/**
+ * Grand unified-ish gesture/event listener for items in the directory list.
+ */
+final class UserInputHandler extends GestureDetector.SimpleOnGestureListener
+ implements KeyboardEventListener {
+
+ private final MultiSelectManager mSelectionMgr;
+ private final FocusHandler mFocusHandler;
+ private final Function<MotionEvent, InputEvent> mEventConverter;
+ private final Function<InputEvent, 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;
+
+ public UserInputHandler(
+ MultiSelectManager selectionMgr,
+ FocusHandler focusHandler,
+ Function<MotionEvent, InputEvent> eventConverter,
+ Function<InputEvent, DocumentDetails> docFinder,
+ Predicate<DocumentDetails> selectable,
+ EventHandler rightClickHandler,
+ DocumentHandler activateHandler,
+ DocumentHandler deleteHandler) {
+
+ mSelectionMgr = selectionMgr;
+ mFocusHandler = focusHandler;
+ mEventConverter = eventConverter;
+ mDocFinder = docFinder;
+ mSelectable = selectable;
+ mRightClickHandler = rightClickHandler;
+ mActivateHandler = activateHandler;
+ mDeleteHandler = deleteHandler;
+
+ mTouchDelegate = new TouchInputDelegate();
+ mMouseDelegate = new MouseInputDelegate();
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ try (InputEvent event = mEventConverter.apply(e)) {
+ return event.isMouseEvent()
+ ? mMouseDelegate.onSingleTapUp(event)
+ : mTouchDelegate.onSingleTapUp(event);
+ }
+ }
+
+ @Override
+ public boolean onSingleTapConfirmed(MotionEvent e) {
+ try (InputEvent event = mEventConverter.apply(e)) {
+ return event.isMouseEvent()
+ ? mMouseDelegate.onSingleTapConfirmed(event)
+ : mTouchDelegate.onSingleTapConfirmed(event);
+ }
+ }
+
+ @Override
+ public boolean onDoubleTap(MotionEvent e) {
+ try (InputEvent event = mEventConverter.apply(e)) {
+ return event.isMouseEvent()
+ ? mMouseDelegate.onDoubleTap(event)
+ : mTouchDelegate.onDoubleTap(event);
+ }
+ }
+
+ @Override
+ public void onLongPress(MotionEvent e) {
+ try (InputEvent event = mEventConverter.apply(e)) {
+ if (event.isMouseEvent()) {
+ mMouseDelegate.onLongPress(event);
+ }
+ mTouchDelegate.onLongPress(event);
+ }
+ }
+
+ private boolean onSelect(DocumentDetails doc) {
+ mSelectionMgr.toggleSelection(doc.getModelId());
+ mSelectionMgr.setSelectionRangeBegin(doc.getAdapterPosition());
+ return true;
+ }
+
+ private final class TouchInputDelegate {
+
+ public boolean onSingleTapUp(InputEvent event) {
+ if (mSelectionMgr.onSingleTapUp(event)) {
+ return true;
+ }
+
+ // Give the DocumentHolder a crack at the event.
+ DocumentDetails doc = mDocFinder.apply(event);
+ if (doc != null) {
+ // Touch events select if they occur in the selection hotspot,
+ // otherwise they activate.
+ return doc.isInSelectionHotspot(event)
+ ? onSelect(doc)
+ : mActivateHandler.accept(doc);
+ }
+
+ return false;
+ }
+
+ public boolean onSingleTapConfirmed(InputEvent event) {
+ return false;
+ }
+
+ public boolean onDoubleTap(InputEvent event) {
+ return false;
+ }
+
+ public void onLongPress(InputEvent event) {
+ mSelectionMgr.onLongPress(event);
+ }
+ }
+
+ private final class MouseInputDelegate {
+
+ // From the RecyclerView, we get two events sent to
+ // ListeningGestureDetector#onInterceptTouchEvent on a mouse click; we first get an
+ // ACTION_DOWN Event for clicking on the mouse, and then an ACTION_UP event from releasing
+ // the mouse click. ACTION_UP event doesn't have information regarding the button (primary
+ // vs. secondary), so we have to save that somewhere first from ACTION_DOWN, and then reuse
+ // it later. The ACTION_DOWN event doesn't get forwarded to UserInputListener,
+ // so we have open up a public set method to set it.
+ private int mLastButtonState = -1;
+
+ // true when the previous event has consumed a right click motion event
+ private boolean ateRightClick;
+
+ // The event has been handled in onSingleTapUp
+ private boolean handledTapUp;
+
+ public boolean onSingleTapUp(InputEvent event) {
+ if (eatRightClick()) {
+ return onSingleRightClickUp(event);
+ }
+
+ if (mSelectionMgr.onSingleTapUp(event)) {
+ handledTapUp = true;
+ return true;
+ }
+
+ // We'll toggle selection in onSingleTapConfirmed
+ // This avoids flickering on/off action mode when an item is double clicked.
+ if (!mSelectionMgr.hasSelection()) {
+ return false;
+ }
+
+ DocumentDetails doc = mDocFinder.apply(event);
+ if (doc == null) {
+ return false;
+ }
+
+ handledTapUp = true;
+ return onSelect(doc);
+ }
+
+ public boolean onSingleTapConfirmed(InputEvent event) {
+ if (ateRightClick) {
+ ateRightClick = false;
+ return false;
+ }
+ if (handledTapUp) {
+ handledTapUp = false;
+ return false;
+ }
+
+ if (mSelectionMgr.hasSelection()) {
+ return false; // should have been handled by onSingleTapUp.
+ }
+
+ DocumentDetails doc = mDocFinder.apply(event);
+ if (doc == null) {
+ return false;
+ }
+
+ return onSelect(doc);
+ }
+
+ public boolean onDoubleTap(InputEvent event) {
+ handledTapUp = false;
+ DocumentDetails doc = mDocFinder.apply(event);
+ if (doc != null) {
+ return mSelectionMgr.hasSelection()
+ ? onSelect(doc)
+ : mActivateHandler.accept(doc);
+ }
+ return false;
+ }
+
+ public void onLongPress(InputEvent event) {
+ mSelectionMgr.onLongPress(event);
+ }
+
+ private boolean onSingleRightClickUp(InputEvent event) {
+ return mRightClickHandler.apply(event);
+ }
+
+ // hack alert from here through end of class.
+ private void setLastButtonState(int state) {
+ mLastButtonState = state;
+ }
+
+ private boolean eatRightClick() {
+ if (mLastButtonState == MotionEvent.BUTTON_SECONDARY) {
+ mLastButtonState = -1;
+ ateRightClick = 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);
+ }
+
+ // 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) {
+ // 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) {
+ return false;
+ }
+
+ // Ignore tab key events. Those should be handled by the top-level key handler.
+ if (keyCode == KeyEvent.KEYCODE_TAB) {
+ return false;
+ }
+
+ if (mFocusHandler.handleKey(doc, keyCode, event)) {
+ // Handle range selection adjustments. Extending the selection will adjust the
+ // bounds of the in-progress range selection. Each time an unshifted navigation
+ // event is received, the range selection is restarted.
+ if (shouldExtendSelection(doc, event)) {
+ if (!mSelectionMgr.isRangeSelectionActive()) {
+ // Start a range selection if one isn't active
+ mSelectionMgr.startRangeSelection(doc.getAdapterPosition());
+ }
+ mSelectionMgr.snapRangeSelection(mFocusHandler.getFocusPosition());
+ } else {
+ mSelectionMgr.endRangeSelection();
+ }
+ return true;
+ }
+
+ // Handle enter key events
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_ENTER:
+ if (event.isShiftPressed()) {
+ onSelect(doc);
+ }
+ // For non-shifted enter keypresses, fall through.
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_BUTTON_A:
+ return mActivateHandler.accept(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.
+ if (mSelectionMgr.hasSelection()) {
+ mDeleteHandler.accept(doc);
+ }
+ // Always handle the key, even if there was nothing to delete. This is a
+ // precaution to prevent other handlers from potentially picking up the event
+ // and triggering extra behaviors.
+ return true;
+ }
+
+ return false;
+ }
+
+ private boolean shouldExtendSelection(DocumentDetails doc, KeyEvent event) {
+ if (!Events.isNavigationKeyCode(event.getKeyCode()) || !event.isShiftPressed()) {
+ return false;
+ }
+
+ return mSelectable.test(doc);
+ }
+
+ @FunctionalInterface
+ interface EventHandler {
+ boolean apply(InputEvent input);
+ }
+
+ @FunctionalInterface
+ interface DocumentHandler {
+ boolean accept(DocumentDetails doc);
+ }
+
+ /**
+ * Class providing limited access to document view info.
+ */
+ public interface DocumentDetails {
+ String getModelId();
+ int getAdapterPosition();
+ boolean isInSelectionHotspot(InputEvent event);
+ }
+}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/TestInputEvent.java b/packages/DocumentsUI/tests/src/com/android/documentsui/TestInputEvent.java
index a21548857a88..36e7c1bf7010 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/TestInputEvent.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/TestInputEvent.java
@@ -12,6 +12,7 @@ public class TestInputEvent implements Events.InputEvent {
public boolean actionDown;
public boolean actionUp;
public Point location;
+ public Point rawLocation;
public int position = Integer.MIN_VALUE;
public TestInputEvent() {}
@@ -21,6 +22,11 @@ public class TestInputEvent implements Events.InputEvent {
}
@Override
+ public boolean isTouchEvent() {
+ return !mouseEvent;
+ }
+
+ @Override
public boolean isMouseEvent() {
return mouseEvent;
}
@@ -66,6 +72,16 @@ public class TestInputEvent implements Events.InputEvent {
}
@Override
+ public float getRawX() {
+ return rawLocation.x;
+ }
+
+ @Override
+ public float getRawY() {
+ return rawLocation.y;
+ }
+
+ @Override
public boolean isOverItem() {
return position != Integer.MIN_VALUE && position != RecyclerView.NO_POSITION;
}
@@ -75,6 +91,9 @@ public class TestInputEvent implements Events.InputEvent {
return position;
}
+ @Override
+ public void close() {}
+
public static TestInputEvent tap(int position) {
return new TestInputEvent(position);
}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/DocumentHolderTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/DocumentHolderTest.java
index 87cd42f08079..949f6b746f47 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/DocumentHolderTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/DocumentHolderTest.java
@@ -20,6 +20,7 @@ import android.content.Context;
import android.database.Cursor;
import android.graphics.Rect;
import android.os.SystemClock;
+import android.support.test.filters.Suppress;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.SmallTest;
import android.view.KeyEvent;
@@ -37,6 +38,7 @@ public class DocumentHolderTest extends AndroidTestCase {
DocumentHolder mHolder;
TestListener mListener;
+ @Override
public void setUp() throws Exception {
Context context = getContext();
LayoutInflater inflater = LayoutInflater.from(context);
@@ -46,28 +48,20 @@ public class DocumentHolderTest extends AndroidTestCase {
};
mListener = new TestListener();
- mHolder.addEventListener(mListener);
+ mHolder.addKeyEventListener(mListener);
mHolder.itemView.requestLayout();
mHolder.itemView.invalidate();
}
- public void testClickActivates() {
- click();
- mListener.assertSelected();
+ @Suppress
+ public void testIsInSelectionHotspot() {
+ fail();
}
- public void testTapActivates() {
- tap();
- mListener.assertActivated();
- }
-
- public void click() {
- mHolder.onSingleTapUp(createEvent(MotionEvent.TOOL_TYPE_MOUSE));
- }
-
- public void tap() {
- mHolder.onSingleTapUp(createEvent(MotionEvent.TOOL_TYPE_FINGER));
+ @Suppress
+ public void testDelegatesKeyEvents() {
+ fail();
}
public MotionEvent createEvent(int tooltype) {
@@ -105,32 +99,7 @@ public class DocumentHolderTest extends AndroidTestCase {
);
}
- private class TestListener implements DocumentHolder.EventListener {
- private boolean mActivated = false;
- private boolean mSelected = false;
-
- public void assertActivated() {
- assertTrue(mActivated);
- assertFalse(mSelected);
- }
-
- public void assertSelected() {
- assertTrue(mSelected);
- assertFalse(mActivated);
- }
-
- @Override
- public boolean onActivate(DocumentHolder doc) {
- mActivated = true;
- return true;
- }
-
- @Override
- public boolean onSelect(DocumentHolder doc) {
- mSelected = true;
- return true;
- }
-
+ private class TestListener implements DocumentHolder.KeyboardEventListener {
@Override
public boolean onKey(DocumentHolder doc, int keyCode, KeyEvent event) {
return false;
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java
index 7864e98e419a..7eb3c2ebf248 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java
@@ -26,7 +26,6 @@ import com.android.documentsui.dirlist.MultiSelectManager.Selection;
import com.google.common.collect.Lists;
-import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -34,13 +33,7 @@ import java.util.Set;
@SmallTest
public class MultiSelectManagerTest extends AndroidTestCase {
- private static final List<String> items;
- static {
- items = new ArrayList<String>();
- for (int i = 0; i < 100; ++i) {
- items.add(Integer.toString(i));
- }
- }
+ private static final List<String> items = TestData.create(100);
private MultiSelectManager mManager;
private TestCallback mCallback;
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/TestData.java b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/TestData.java
new file mode 100644
index 000000000000..5c1d987d45f0
--- /dev/null
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/TestData.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2016 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 java.util.ArrayList;
+import java.util.List;
+
+public class TestData {
+ public static List<String> create(int num) {
+ List<String> items = new ArrayList<String>(num);
+ for (int i = 0; i < num; ++i) {
+ items.add(Integer.toString(i));
+ }
+ return items;
+ }
+}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/UserInputHandler_MouseTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/UserInputHandler_MouseTest.java
new file mode 100644
index 000000000000..d808fe8eb221
--- /dev/null
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/UserInputHandler_MouseTest.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2016 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 static org.junit.Assert.*;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.documentsui.Events.InputEvent;
+import com.android.documentsui.TestInputEvent;
+import com.android.documentsui.dirlist.MultiSelectManager.Selection;
+import com.android.documentsui.dirlist.UserInputHandler.DocumentDetails;
+import com.android.documentsui.testing.TestPredicate;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class UserInputHandler_MouseTest {
+
+ private static final List<String> ITEMS = TestData.create(100);
+
+ private TestDocumentsAdapter mAdapter;
+ private MultiSelectManager mSelectionMgr;
+ private TestPredicate<DocumentDetails> mCanSelect;
+ private TestPredicate<InputEvent> mRightClickHandler;
+ private TestPredicate<DocumentDetails> mActivateHandler;
+ private TestPredicate<DocumentDetails> mDeleteHandler;
+
+ private TestInputEvent mTestEvent;
+ private TestDocDetails mTestDoc;
+
+ private UserInputHandler mInputHandler;
+
+ @Before
+ public void setUp() {
+
+ mAdapter = new TestDocumentsAdapter(ITEMS);
+ mSelectionMgr = new MultiSelectManager(mAdapter, MultiSelectManager.MODE_MULTIPLE);
+ mCanSelect = new TestPredicate<>();
+ mRightClickHandler = new TestPredicate<>();
+ mActivateHandler = new TestPredicate<>();
+ mDeleteHandler = new TestPredicate<>();
+
+ mInputHandler = new UserInputHandler(
+ mSelectionMgr,
+ new TestFocusHandler(),
+ (MotionEvent event) -> {
+ return mTestEvent;
+ },
+ (InputEvent event) -> {
+ return mTestDoc;
+ },
+ mCanSelect,
+ mRightClickHandler::test,
+ mActivateHandler::test,
+ mDeleteHandler::test);
+
+ mTestEvent = new TestInputEvent();
+ mTestEvent.mouseEvent = true;
+ mTestDoc = new TestDocDetails();
+ }
+
+ @Test
+ public void testConfirmedClick_StartsSelection() {
+ mTestDoc.modelId = "11";
+ mInputHandler.onSingleTapConfirmed(null);
+ assertSelected("11");
+ }
+
+ @Test
+ public void testDoubleClick_Activates() {
+ mTestDoc.modelId = "11";
+ mInputHandler.onDoubleTap(null);
+ mActivateHandler.assertLastArgument(mTestDoc);
+ }
+
+ void assertSelected(String id) {
+ Selection sel = mSelectionMgr.getSelection();
+ assertTrue(sel.contains(id));
+ }
+
+ private final class TestDocDetails implements DocumentDetails {
+
+ private String modelId;
+ private int position;
+ private boolean inHotspot;
+
+ @Override
+ public String getModelId() {
+ return modelId;
+ }
+
+ @Override
+ public int getAdapterPosition() {
+ return position;
+ }
+
+ @Override
+ public boolean isInSelectionHotspot(InputEvent event) {
+ return inHotspot;
+ }
+
+ }
+
+ private final class TestFocusHandler implements FocusHandler {
+
+ @Override
+ public boolean handleKey(DocumentHolder doc, int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ }
+
+ @Override
+ public void restoreLastFocus() {
+ }
+
+ @Override
+ public int getFocusPosition() {
+ return 0;
+ }
+ }
+}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/testing/TestPredicate.java b/packages/DocumentsUI/tests/src/com/android/documentsui/testing/TestPredicate.java
new file mode 100644
index 000000000000..f8ee21e30413
--- /dev/null
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/testing/TestPredicate.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2016 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.testing;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.function.Predicate;
+
+import javax.annotation.Nullable;
+
+/**
+ * Test predicate that can be used to spy control responses and make
+ * assertions against values tested.
+ */
+public class TestPredicate<T> implements Predicate<T> {
+
+ private @Nullable T lastValue;
+ private boolean nextReturnValue;
+
+ @Override
+ public boolean test(T t) {
+ lastValue = t;
+ return nextReturnValue;
+ }
+
+ public void assertLastArgument(@Nullable T expected) {
+ assertEquals(expected, lastValue);
+ }
+
+ public void nextReturn(boolean value) {
+ nextReturnValue = value;
+ }
+}