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