1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.documentsui.dirlist;
18 
19 import static com.android.documentsui.base.Shared.DEBUG;
20 import static com.android.documentsui.base.Shared.VERBOSE;
21 
22 import android.support.annotation.VisibleForTesting;
23 import android.util.Log;
24 import android.view.GestureDetector;
25 import android.view.KeyEvent;
26 import android.view.MotionEvent;
27 
28 import com.android.documentsui.ActionHandler;
29 import com.android.documentsui.base.EventHandler;
30 import com.android.documentsui.base.Events;
31 import com.android.documentsui.base.Events.InputEvent;
32 import com.android.documentsui.selection.SelectionManager;
33 
34 import java.util.Collections;
35 import java.util.function.Function;
36 import java.util.function.Predicate;
37 
38 import javax.annotation.Nullable;
39 
40 /**
41  * Grand unified-ish gesture/event listener for items in the directory list.
42  */
43 public final class UserInputHandler<T extends InputEvent>
44         extends GestureDetector.SimpleOnGestureListener
45         implements DocumentHolder.KeyboardEventListener {
46 
47     private static final String TAG = "UserInputHandler";
48 
49     private ActionHandler mActions;
50     private final FocusHandler mFocusHandler;
51     private final SelectionManager mSelectionMgr;
52     private final Function<MotionEvent, T> mEventConverter;
53     private final Predicate<DocumentDetails> mSelectable;
54 
55     private final EventHandler<InputEvent> mContextMenuClickHandler;
56 
57     private final EventHandler<InputEvent> mTouchDragListener;
58     private final EventHandler<InputEvent> mGestureSelectHandler;
59     private final Runnable mPerformHapticFeedback;
60 
61     private final TouchInputDelegate mTouchDelegate;
62     private final MouseInputDelegate mMouseDelegate;
63     private final KeyInputHandler mKeyListener;
64 
UserInputHandler( ActionHandler actions, FocusHandler focusHandler, SelectionManager selectionMgr, Function<MotionEvent, T> eventConverter, Predicate<DocumentDetails> selectable, EventHandler<InputEvent> contextMenuClickHandler, EventHandler<InputEvent> touchDragListener, EventHandler<InputEvent> gestureSelectHandler, Runnable performHapticFeedback)65     public UserInputHandler(
66             ActionHandler actions,
67             FocusHandler focusHandler,
68             SelectionManager selectionMgr,
69             Function<MotionEvent, T> eventConverter,
70             Predicate<DocumentDetails> selectable,
71             EventHandler<InputEvent> contextMenuClickHandler,
72             EventHandler<InputEvent> touchDragListener,
73             EventHandler<InputEvent> gestureSelectHandler,
74             Runnable performHapticFeedback) {
75 
76         mActions = actions;
77         mFocusHandler = focusHandler;
78         mSelectionMgr = selectionMgr;
79         mEventConverter = eventConverter;
80         mSelectable = selectable;
81         mContextMenuClickHandler = contextMenuClickHandler;
82         mTouchDragListener = touchDragListener;
83         mGestureSelectHandler = gestureSelectHandler;
84         mPerformHapticFeedback = performHapticFeedback;
85 
86         mTouchDelegate = new TouchInputDelegate();
87         mMouseDelegate = new MouseInputDelegate();
88         mKeyListener = new KeyInputHandler();
89     }
90 
91     @Override
onDown(MotionEvent e)92     public boolean onDown(MotionEvent e) {
93         try (T event = mEventConverter.apply(e)) {
94             return onDown(event);
95         }
96     }
97 
98     @VisibleForTesting
onDown(T event)99     boolean onDown(T event) {
100         return event.isMouseEvent()
101                 ? mMouseDelegate.onDown(event)
102                 : mTouchDelegate.onDown(event);
103     }
104 
105     @Override
onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)106     public boolean onScroll(MotionEvent e1, MotionEvent e2,
107             float distanceX, float distanceY) {
108         try (T event = mEventConverter.apply(e2)) {
109             return onScroll(event);
110         }
111     }
112 
113     @VisibleForTesting
onScroll(T event)114     boolean onScroll(T event) {
115         return event.isMouseEvent()
116                 ? mMouseDelegate.onScroll(event)
117                 : mTouchDelegate.onScroll(event);
118     }
119 
120     @Override
onSingleTapUp(MotionEvent e)121     public boolean onSingleTapUp(MotionEvent e) {
122         try (T event = mEventConverter.apply(e)) {
123             return onSingleTapUp(event);
124         }
125     }
126 
127     @VisibleForTesting
onSingleTapUp(T event)128     boolean onSingleTapUp(T event) {
129         return event.isMouseEvent()
130                 ? mMouseDelegate.onSingleTapUp(event)
131                 : mTouchDelegate.onSingleTapUp(event);
132     }
133 
134     @Override
onSingleTapConfirmed(MotionEvent e)135     public boolean onSingleTapConfirmed(MotionEvent e) {
136         try (T event = mEventConverter.apply(e)) {
137             return onSingleTapConfirmed(event);
138         }
139     }
140 
141     @VisibleForTesting
onSingleTapConfirmed(T event)142     boolean onSingleTapConfirmed(T event) {
143         return event.isMouseEvent()
144                 ? mMouseDelegate.onSingleTapConfirmed(event)
145                 : mTouchDelegate.onSingleTapConfirmed(event);
146     }
147 
148     @Override
onDoubleTap(MotionEvent e)149     public boolean onDoubleTap(MotionEvent e) {
150         try (T event = mEventConverter.apply(e)) {
151             return onDoubleTap(event);
152         }
153     }
154 
155     @VisibleForTesting
onDoubleTap(T event)156     boolean onDoubleTap(T event) {
157         return event.isMouseEvent()
158                 ? mMouseDelegate.onDoubleTap(event)
159                 : mTouchDelegate.onDoubleTap(event);
160     }
161 
162     @Override
onLongPress(MotionEvent e)163     public void onLongPress(MotionEvent e) {
164         try (T event = mEventConverter.apply(e)) {
165             onLongPress(event);
166         }
167     }
168 
169     @VisibleForTesting
onLongPress(T event)170     void onLongPress(T event) {
171         if (event.isMouseEvent()) {
172             mMouseDelegate.onLongPress(event);
173         } else {
174             mTouchDelegate.onLongPress(event);
175         }
176     }
177 
178     // Only events from RecyclerView are fed into UserInputHandler#onDown.
179     // ListeningGestureDetector#onTouch directly calls this method to support context menu in empty
180     // view
onRightClick(MotionEvent e)181     boolean onRightClick(MotionEvent e) {
182         try (T event = mEventConverter.apply(e)) {
183             return mMouseDelegate.onRightClick(event);
184         }
185     }
186 
187     @Override
onKey(DocumentHolder doc, int keyCode, KeyEvent event)188     public boolean onKey(DocumentHolder doc, int keyCode, KeyEvent event) {
189         return mKeyListener.onKey(doc, keyCode, event);
190     }
191 
selectDocument(DocumentDetails doc)192     private boolean selectDocument(DocumentDetails doc) {
193         assert(doc != null);
194         assert(doc.hasModelId());
195         mSelectionMgr.toggleSelection(doc.getModelId());
196         mSelectionMgr.setSelectionRangeBegin(doc.getAdapterPosition());
197         return true;
198     }
199 
extendSelectionRange(DocumentDetails doc)200     private void extendSelectionRange(DocumentDetails doc) {
201         mSelectionMgr.snapRangeSelection(doc.getAdapterPosition());
202     }
203 
isRangeExtension(T event)204     boolean isRangeExtension(T event) {
205         return event.isShiftKeyDown() && mSelectionMgr.isRangeSelectionActive();
206     }
207 
shouldClearSelection(T event, DocumentDetails doc)208     private boolean shouldClearSelection(T event, DocumentDetails doc) {
209         return !event.isCtrlKeyDown()
210                 && !doc.isInSelectionHotspot(event)
211                 && !mSelectionMgr.getSelection().contains(doc.getModelId());
212     }
213 
214     private static final String TTAG = "TouchInputDelegate";
215     private final class TouchInputDelegate {
216 
onDown(T event)217         boolean onDown(T event) {
218             if (VERBOSE) Log.v(TTAG, "Delegated onDown event.");
219             return false;
220         }
221 
222         // Don't consume so the RecyclerView will get the event and will get touch-based scrolling
onScroll(T event)223         boolean onScroll(T event) {
224             if (VERBOSE) Log.v(TTAG, "Delegated onScroll event.");
225             return false;
226         }
227 
onSingleTapUp(T event)228         boolean onSingleTapUp(T event) {
229             if (VERBOSE) Log.v(TTAG, "Delegated onSingleTapUp event.");
230             if (!event.isOverModelItem()) {
231                 if (DEBUG) Log.d(TTAG, "Tap not associated w/ model item. Clearing selection.");
232                 mSelectionMgr.clearSelection();
233                 return false;
234             }
235 
236             DocumentDetails doc = event.getDocumentDetails();
237             if (mSelectionMgr.hasSelection()) {
238                 if (isRangeExtension(event)) {
239                     extendSelectionRange(doc);
240                 } else {
241                     selectDocument(doc);
242                 }
243                 return true;
244             }
245 
246             // Touch events select if they occur in the selection hotspot,
247             // otherwise they activate.
248             return doc.isInSelectionHotspot(event)
249                     ? selectDocument(doc)
250                     : mActions.openDocument(doc, ActionHandler.VIEW_TYPE_PREVIEW,
251                             ActionHandler.VIEW_TYPE_REGULAR);
252         }
253 
onSingleTapConfirmed(T event)254         boolean onSingleTapConfirmed(T event) {
255             if (VERBOSE) Log.v(TTAG, "Delegated onSingleTapConfirmed event.");
256             return false;
257         }
258 
onDoubleTap(T event)259         boolean onDoubleTap(T event) {
260             if (VERBOSE) Log.v(TTAG, "Delegated onDoubleTap event.");
261             return false;
262         }
263 
onLongPress(T event)264         final void onLongPress(T event) {
265             if (VERBOSE) Log.v(TTAG, "Delegated onLongPress event.");
266             if (!event.isOverModelItem()) {
267                 if (DEBUG) Log.d(TTAG, "Ignoring LongPress on non-model-backed item.");
268                 return;
269             }
270 
271             DocumentDetails doc = event.getDocumentDetails();
272             boolean handled = false;
273             if (isRangeExtension(event)) {
274                 extendSelectionRange(doc);
275                 handled = true;
276             } else {
277                 if (!mSelectionMgr.getSelection().contains(doc.getModelId())) {
278                     selectDocument(doc);
279                     // If we cannot select it, we didn't apply anchoring - therefore should not
280                     // start gesture selection
281                     if (mSelectable.test(doc)) {
282                         mGestureSelectHandler.accept(event);
283                         handled = true;
284                     }
285                 } else {
286                     // We only initiate drag and drop on long press for touch to allow regular
287                     // touch-based scrolling
288                     mTouchDragListener.accept(event);
289                     handled = true;
290                 }
291             }
292             if (handled) {
293                 mPerformHapticFeedback.run();
294             }
295         }
296     }
297 
298     private static final String MTAG = "MouseInputDelegate";
299     private final class MouseInputDelegate {
300         // The event has been handled in onSingleTapUp
301         private boolean mHandledTapUp;
302         // true when the previous event has consumed a right click motion event
303         private boolean mHandledOnDown;
304 
onDown(T event)305         boolean onDown(T event) {
306             if (VERBOSE) Log.v(MTAG, "Delegated onDown event.");
307             if (event.isSecondaryButtonPressed()
308                     || (event.isAltKeyDown() && event.isPrimaryButtonPressed())) {
309                 mHandledOnDown = true;
310                 return onRightClick(event);
311             }
312 
313             return false;
314         }
315 
316         // Don't scroll content window in response to mouse drag
onScroll(T event)317         boolean onScroll(T event) {
318             if (VERBOSE) Log.v(MTAG, "Delegated onScroll event.");
319             // If it's two-finger trackpad scrolling, we want to scroll
320             return !event.isTouchpadScroll();
321         }
322 
onSingleTapUp(T event)323         boolean onSingleTapUp(T event) {
324             if (VERBOSE) Log.v(MTAG, "Delegated onSingleTapUp event.");
325 
326             // See b/27377794. Since we don't get a button state back from UP events, we have to
327             // explicitly save this state to know whether something was previously handled by
328             // DOWN events or not.
329             if (mHandledOnDown) {
330                 if (VERBOSE) Log.v(MTAG, "Ignoring onSingleTapUp, previously handled in onDown.");
331                 mHandledOnDown = false;
332                 return false;
333             }
334 
335             if (!event.isOverModelItem()) {
336                 if (DEBUG) Log.d(MTAG, "Tap not associated w/ model item. Clearing selection.");
337                 mSelectionMgr.clearSelection();
338                 return false;
339             }
340 
341             if (event.isTertiaryButtonPressed()) {
342                 if (DEBUG) Log.d(MTAG, "Ignoring middle click");
343                 return false;
344             }
345 
346             DocumentDetails doc = event.getDocumentDetails();
347             if (mSelectionMgr.hasSelection()) {
348                 if (isRangeExtension(event)) {
349                     extendSelectionRange(doc);
350                 } else {
351                     if (shouldClearSelection(event, doc)) {
352                         mSelectionMgr.clearSelection();
353                     }
354                     selectDocument(doc);
355                 }
356                 mHandledTapUp = true;
357                 return true;
358             }
359 
360             return false;
361         }
362 
onSingleTapConfirmed(T event)363         boolean onSingleTapConfirmed(T event) {
364             if (VERBOSE) Log.v(MTAG, "Delegated onSingleTapConfirmed event.");
365             if (mHandledTapUp) {
366                 if (VERBOSE) Log.v(MTAG, "Ignoring onSingleTapConfirmed, previously handled in onSingleTapUp.");
367                 mHandledTapUp = false;
368                 return false;
369             }
370 
371             if (mSelectionMgr.hasSelection()) {
372                 return false;  // should have been handled by onSingleTapUp.
373             }
374 
375             if (!event.isOverItem()) {
376                 if (DEBUG) Log.d(MTAG, "Ignoring Confirmed Tap on non-item.");
377                 return false;
378             }
379 
380             if (event.isTertiaryButtonPressed()) {
381                 if (DEBUG) Log.d(MTAG, "Ignoring middle click");
382                 return false;
383             }
384 
385             @Nullable DocumentDetails doc = event.getDocumentDetails();
386             if (doc == null || !doc.hasModelId()) {
387                 Log.w(MTAG, "Ignoring Confirmed Tap. No document details associated w/ event.");
388                 return false;
389             }
390 
391             if (mFocusHandler.hasFocusedItem() && event.isShiftKeyDown()) {
392                 mSelectionMgr.formNewSelectionRange(mFocusHandler.getFocusPosition(),
393                         doc.getAdapterPosition());
394                 return true;
395             } else {
396                 return selectDocument(doc);
397             }
398         }
399 
onDoubleTap(T event)400         boolean onDoubleTap(T event) {
401             if (VERBOSE) Log.v(MTAG, "Delegated onDoubleTap event.");
402             mHandledTapUp = false;
403 
404             if (!event.isOverModelItem()) {
405                 if (DEBUG) Log.d(MTAG, "Ignoring DoubleTap on non-model-backed item.");
406                 return false;
407             }
408 
409             if (event.isTertiaryButtonPressed()) {
410                 if (DEBUG) Log.d(MTAG, "Ignoring middle click");
411                 return false;
412             }
413 
414             DocumentDetails doc = event.getDocumentDetails();
415             return mActions.openDocument(doc, ActionHandler.VIEW_TYPE_REGULAR,
416                     ActionHandler.VIEW_TYPE_PREVIEW);
417         }
418 
onLongPress(T event)419         final void onLongPress(T event) {
420             if (VERBOSE) Log.v(MTAG, "Delegated onLongPress event.");
421             return;
422         }
423 
onRightClick(T event)424         private boolean onRightClick(T event) {
425             if (VERBOSE) Log.v(MTAG, "Delegated onRightClick event.");
426             if (event.isOverModelItem()) {
427                 DocumentDetails doc = event.getDocumentDetails();
428                 if (!mSelectionMgr.getSelection().contains(doc.getModelId())) {
429                     mSelectionMgr.replaceSelection(Collections.singleton(doc.getModelId()));
430                     mSelectionMgr.setSelectionRangeBegin(doc.getAdapterPosition());
431                 }
432             }
433 
434             // We always delegate final handling of the event,
435             // since the handler might want to show a context menu
436             // in an empty area or some other weirdo view.
437             return mContextMenuClickHandler.accept(event);
438         }
439     }
440 
441     private final class KeyInputHandler {
442         // TODO: Refactor FocusManager to depend only on DocumentDetails so we can eliminate
443         // difficult to test dependency on DocumentHolder.
444 
onKey(@ullable DocumentHolder doc, int keyCode, KeyEvent event)445         boolean onKey(@Nullable DocumentHolder doc, int keyCode, KeyEvent event) {
446             // Only handle key-down events. This is simpler, consistent with most other UIs, and
447             // enables the handling of repeated key events from holding down a key.
448             if (event.getAction() != KeyEvent.ACTION_DOWN) {
449                 return false;
450             }
451 
452             // Ignore tab key events.  Those should be handled by the top-level key handler.
453             if (keyCode == KeyEvent.KEYCODE_TAB) {
454                 return false;
455             }
456 
457             // Ignore events sent to Addon Holders.
458             if (doc != null) {
459                 int itemType = doc.getItemViewType();
460                 if (itemType == DocumentsAdapter.ITEM_TYPE_HEADER_MESSAGE
461                         || itemType == DocumentsAdapter.ITEM_TYPE_INFLATED_MESSAGE
462                         || itemType == DocumentsAdapter.ITEM_TYPE_SECTION_BREAK) {
463                     return false;
464                 }
465             }
466 
467             if (mFocusHandler.handleKey(doc, keyCode, event)) {
468                 // Handle range selection adjustments. Extending the selection will adjust the
469                 // bounds of the in-progress range selection. Each time an unshifted navigation
470                 // event is received, the range selection is restarted.
471                 if (shouldExtendSelection(doc, event)) {
472                     if (!mSelectionMgr.isRangeSelectionActive()) {
473                         // Start a range selection if one isn't active
474                         mSelectionMgr.startRangeSelection(doc.getAdapterPosition());
475                     }
476                     mSelectionMgr.snapRangeSelection(mFocusHandler.getFocusPosition());
477                 } else {
478                     mSelectionMgr.endRangeSelection();
479                     mSelectionMgr.clearSelection();
480                 }
481                 return true;
482             }
483 
484             // Handle enter key events
485             switch (keyCode) {
486                 case KeyEvent.KEYCODE_ENTER:
487                     if (event.isShiftPressed()) {
488                         selectDocument(doc);
489                     }
490                     // For non-shifted enter keypresses, fall through.
491                 case KeyEvent.KEYCODE_DPAD_CENTER:
492                 case KeyEvent.KEYCODE_BUTTON_A:
493                     return mActions.openDocument(doc, ActionHandler.VIEW_TYPE_REGULAR,
494                             ActionHandler.VIEW_TYPE_PREVIEW);
495                 case KeyEvent.KEYCODE_SPACE:
496                     return mActions.openDocument(doc, ActionHandler.VIEW_TYPE_PREVIEW,
497                             ActionHandler.VIEW_TYPE_NONE);
498             }
499 
500             return false;
501         }
502 
shouldExtendSelection(DocumentDetails doc, KeyEvent event)503         private boolean shouldExtendSelection(DocumentDetails doc, KeyEvent event) {
504             if (!Events.isNavigationKeyCode(event.getKeyCode()) || !event.isShiftPressed()) {
505                 return false;
506             }
507 
508             return mSelectable.test(doc);
509         }
510     }
511 }
512