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
diff --git a/packages/DocumentsUI/src/com/android/documentsui/Events.java b/packages/DocumentsUI/src/com/android/documentsui/Events.java
index 691f95a..2d0dbe8 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/Events.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/Events.java
@@ -115,7 +115,8 @@
* 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 @@
/** 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 @@
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 @@
}
@Override
+ public boolean isTouchEvent() {
+ return Events.isTouchEvent(mEvent);
+ }
+
+ @Override
public boolean isMouseEvent() {
return Events.isMouseEvent(mEvent);
}
@@ -250,6 +262,16 @@
}
@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 7320dc0..8f52036 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 @@
}
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 ce4e993..3980cfc 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.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.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.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.Collections;
import java.util.List;
import java.util.Objects;
+import java.util.function.Function;
import javax.annotation.Nullable;
@@ -136,9 +140,9 @@
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 @@
@Override
public void onDestroyView() {
- mSelectionManager.clearSelection();
+ mSelectionMgr.clearSelection();
// Cancel any outstanding thumbnail requests
final int count = mRecView.getChildCount();
@@ -296,46 +300,49 @@
// 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 void retainState(RetainedState state) {
- state.selection = mSelectionManager.getSelection(new Selection());
+ state.selection = mSelectionMgr.getSelection(new Selection());
}
@Override
@@ -419,49 +426,37 @@
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 @@
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 @@
@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 @@
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 @@
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 @@
@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 @@
}
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 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 @@
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 @@
@Override
public void initDocumentHolder(DocumentHolder holder) {
- holder.addEventListener(mItemEventListener);
+ holder.addKeyEventListener(mInputHandler);
holder.itemView.setOnFocusChangeListener(mFocusManager);
}
@@ -1186,11 +1204,11 @@
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 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 @@
}
// 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 @@
void dragStopped(boolean result) {
if (result) {
- mSelectionManager.clearSelection();
+ mSelectionMgr.clearSelection();
}
}
@@ -1363,19 +1381,7 @@
}
}
- /**
- * 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 @@
@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 @@
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 @@
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 @@
}
};
+ 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 @@
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 2288fe74..c2b0bf2 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.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 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 @@
@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);
+ assert(mKeyEventListener != null);
+ return mKeyEventListener.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;
+ /**
+ * 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 void addOnKeyListener(View.OnKeyListener listener) {
- // Just handle one for now; switch to a list if necessary.
- assert(mKeyListener == null);
- mKeyListener = listener;
+ @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());
}
- 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;
- }
-
- 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 @@
/**
* 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 0000000..ba26d65
--- /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 f274df3..1be2f65 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 @@
/**
* 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 @@
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 @@
}
}
- /**
- * 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 @@
}
}
- /**
- * @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 1af26d0..0000000
--- 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 cffba8d..0000000
--- 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 50e595d..85ff6ed 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 @@
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 @@
@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 e0fc541..e58971a 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 @@
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 0000000..943815c
--- /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 a215488..36e7c1b 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 boolean actionDown;
public boolean actionUp;
public Point location;
+ public Point rawLocation;
public int position = Integer.MIN_VALUE;
public TestInputEvent() {}
@@ -21,6 +22,11 @@
}
@Override
+ public boolean isTouchEvent() {
+ return !mouseEvent;
+ }
+
+ @Override
public boolean isMouseEvent() {
return mouseEvent;
}
@@ -66,6 +72,16 @@
}
@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 @@
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 87cd42f..949f6b7 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.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 @@
DocumentHolder mHolder;
TestListener mListener;
+ @Override
public void setUp() throws Exception {
Context context = getContext();
LayoutInflater inflater = LayoutInflater.from(context);
@@ -46,28 +48,20 @@
};
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 @@
);
}
- 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 7864e98..7eb3c2e 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.google.common.collect.Lists;
-import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -34,13 +33,7 @@
@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 0000000..5c1d987
--- /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 0000000..d808fe8
--- /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 0000000..f8ee21e
--- /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;
+ }
+}