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