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
This commit is contained in:
Steve McKay
2016-06-30 21:03:06 -07:00
parent 5ba81e88ad
commit 74c287706b
17 changed files with 820 additions and 469 deletions

View File

@@ -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);
@@ -204,6 +211,11 @@ public final class Events {
recycle();
}
@Override
public boolean isTouchEvent() {
return Events.isTouchEvent(mEvent);
}
@Override
public boolean isMouseEvent() {
return Events.isMouseEvent(mEvent);
@@ -249,6 +261,16 @@ public final class Events {
return mEvent.getY();
}
@Override
public float getRawX() {
return mEvent.getRawX();
}
@Override
public float getRawY() {
return mEvent.getRawY();
}
@Override
public boolean isOverItem() {
return getItemPosition() != RecyclerView.NO_POSITION;

View File

@@ -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.

View File

@@ -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.

View File

@@ -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);
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 @@ 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.

View File

@@ -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();
}

View File

@@ -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;
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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.

View File

@@ -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);
}
}

View File

@@ -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() {}
@@ -20,6 +21,11 @@ public class TestInputEvent implements Events.InputEvent {
this.position = position;
}
@Override
public boolean isTouchEvent() {
return !mouseEvent;
}
@Override
public boolean isMouseEvent() {
return mouseEvent;
@@ -65,6 +71,16 @@ public class TestInputEvent implements Events.InputEvent {
return location.y;
}
@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);
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}