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:
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user