blob: 07b0cd8f724818ee98ada04bdd9479113ab47b00 [file] [log] [blame]
Steve McKay84d66782016-07-29 09:39:52 -07001/*
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
17package com.android.documentsui.dirlist;
18
19import static com.android.documentsui.Shared.DEBUG;
20
21import android.support.annotation.VisibleForTesting;
22import android.util.Log;
23import android.view.GestureDetector;
24import android.view.KeyEvent;
25import android.view.MotionEvent;
26
27import com.android.documentsui.Events;
28import com.android.documentsui.Events.InputEvent;
29import com.android.documentsui.dirlist.DocumentHolder.KeyboardEventListener;
30
31import java.util.function.Function;
32import java.util.function.Predicate;
33
34/**
35 * Grand unified-ish gesture/event listener for items in the directory list.
36 */
37public final class UserInputHandler<T extends InputEvent>
38 extends GestureDetector.SimpleOnGestureListener
39 implements KeyboardEventListener {
40
41 private static final String TAG = "UserInputHandler";
42
43 private final MultiSelectManager mSelectionMgr;
44 private final FocusHandler mFocusHandler;
45 private final Function<MotionEvent, T> mEventConverter;
46 private final Function<T, DocumentDetails> mDocFinder;
47 private final Predicate<DocumentDetails> mSelectable;
48 private final EventHandler mRightClickHandler;
49 private final DocumentHandler mActivateHandler;
50 private final DocumentHandler mDeleteHandler;
51 private final TouchInputDelegate mTouchDelegate;
52 private final MouseInputDelegate mMouseDelegate;
53 private final KeyInputHandler mKeyListener;
54
55 public UserInputHandler(
56 MultiSelectManager selectionMgr,
57 FocusHandler focusHandler,
58 Function<MotionEvent, T> eventConverter,
59 Function<T, DocumentDetails> docFinder,
60 Predicate<DocumentDetails> selectable,
61 EventHandler rightClickHandler,
62 DocumentHandler activateHandler,
63 DocumentHandler deleteHandler) {
64
65 mSelectionMgr = selectionMgr;
66 mFocusHandler = focusHandler;
67 mEventConverter = eventConverter;
68 mDocFinder = docFinder;
69 mSelectable = selectable;
70 mRightClickHandler = rightClickHandler;
71 mActivateHandler = activateHandler;
72 mDeleteHandler = deleteHandler;
73
74 mTouchDelegate = new TouchInputDelegate();
75 mMouseDelegate = new MouseInputDelegate();
76 mKeyListener = new KeyInputHandler();
77 }
78
79 @Override
80 public boolean onSingleTapUp(MotionEvent e) {
81 try (T event = mEventConverter.apply(e)) {
82 return onSingleTapUp(event);
83 }
84 }
85
86 @VisibleForTesting
87 boolean onSingleTapUp(T event) {
88 return event.isMouseEvent()
89 ? mMouseDelegate.onSingleTapUp(event)
90 : mTouchDelegate.onSingleTapUp(event);
91 }
92
93 @Override
94 public boolean onSingleTapConfirmed(MotionEvent e) {
95 try (T event = mEventConverter.apply(e)) {
96 return onSingleTapConfirmed(event);
97 }
98 }
99
100 @VisibleForTesting
101 boolean onSingleTapConfirmed(T event) {
102 return event.isMouseEvent()
103 ? mMouseDelegate.onSingleTapConfirmed(event)
104 : mTouchDelegate.onSingleTapConfirmed(event);
105 }
106
107 @Override
108 public boolean onDoubleTap(MotionEvent e) {
109 try (T event = mEventConverter.apply(e)) {
110 return onDoubleTap(event);
111 }
112 }
113
114 @VisibleForTesting
115 boolean onDoubleTap(T event) {
116 return event.isMouseEvent()
117 ? mMouseDelegate.onDoubleTap(event)
118 : mTouchDelegate.onDoubleTap(event);
119 }
120
121 @Override
122 public void onLongPress(MotionEvent e) {
123 try (T event = mEventConverter.apply(e)) {
124 onLongPress(event);
125 }
126 }
127
128 @VisibleForTesting
129 void onLongPress(T event) {
130 if (event.isMouseEvent()) {
131 mMouseDelegate.onLongPress(event);
132 }
133 mTouchDelegate.onLongPress(event);
134 }
135
136 public boolean onSingleRightClickUp(MotionEvent e) {
137 try (T event = mEventConverter.apply(e)) {
138 return mMouseDelegate.onSingleRightClickUp(event);
139 }
140 }
141
142 @Override
143 public boolean onKey(DocumentHolder doc, int keyCode, KeyEvent event) {
144 return mKeyListener.onKey(doc, keyCode, event);
145 }
146
147 // TODO: Isolate this hack...see if we can't get this solved at the platform level.
148 public void setLastButtonState(int state) {
149 mMouseDelegate.setLastButtonState(state);
150 }
151
152 private boolean activateDocument(DocumentDetails doc) {
153 return mActivateHandler.accept(doc);
154 }
155
156 private boolean selectDocument(DocumentDetails doc) {
157 assert(doc != null);
158 mSelectionMgr.toggleSelection(doc.getModelId());
159 mSelectionMgr.setSelectionRangeBegin(doc.getAdapterPosition());
160 return true;
161 }
162
163 boolean isRangeExtension(T event) {
164 return event.isShiftKeyDown() && mSelectionMgr.isRangeSelectionActive();
165 }
166
167 private void extendSelectionRange(T event) {
168 mSelectionMgr.snapSelection(event.getItemPosition());
169 }
170
171 private final class TouchInputDelegate {
172
173 boolean onSingleTapUp(T event) {
174 if (!event.isOverItem()) {
175 if (DEBUG) Log.d(TAG, "Tap on non-item. Clearing selection.");
176 mSelectionMgr.clearSelection();
177 return false;
178 }
179
180 if (mSelectionMgr.hasSelection()) {
181 if (isRangeExtension(event)) {
182 mSelectionMgr.snapSelection(event.getItemPosition());
183 } else {
184 selectDocument(mDocFinder.apply(event));
185 }
186 return true;
187 }
188
189 // Give the DocumentHolder a crack at the event.
190 DocumentDetails doc = mDocFinder.apply(event);
191 if (doc != null) {
192 // Touch events select if they occur in the selection hotspot,
193 // otherwise they activate.
194 return doc.isInSelectionHotspot(event)
195 ? selectDocument(doc)
196 : activateDocument(doc);
197 }
198
199 return false;
200 }
201
202 boolean onSingleTapConfirmed(T event) {
203 return false;
204 }
205
206 boolean onDoubleTap(T event) {
207 return false;
208 }
209
210 final void onLongPress(T event) {
211 if (!event.isOverItem()) {
212 return;
213 }
214
215 if (isRangeExtension(event)) {
216 extendSelectionRange(event);
217 } else {
218 selectDocument(mDocFinder.apply(event));
219 }
220 }
221 }
222
223 private final class MouseInputDelegate {
224
225 // From the RecyclerView, we get two events sent to
226 // ListeningGestureDetector#onInterceptTouchEvent on a mouse click; we first get an
227 // ACTION_DOWN Event for clicking on the mouse, and then an ACTION_UP event from releasing
228 // the mouse click. ACTION_UP event doesn't have information regarding the button (primary
229 // vs. secondary), so we have to save that somewhere first from ACTION_DOWN, and then reuse
230 // it later. The ACTION_DOWN event doesn't get forwarded to UserInputListener,
231 // so we have open up a public set method to set it.
232 private int mLastButtonState = -1;
233
234 // true when the previous event has consumed a right click motion event
235 private boolean mAteRightClick;
236
237 // The event has been handled in onSingleTapUp
238 private boolean mHandledTapUp;
239
240 boolean onSingleTapUp(T event) {
241 if (eatRightClick()) {
242 return onSingleRightClickUp(event);
243 }
244
245 if (!event.isOverItem()) {
246 mSelectionMgr.clearSelection();
247 return false;
248 }
249
250 if (mSelectionMgr.hasSelection()) {
251 if (isRangeExtension(event)) {
252 extendSelectionRange(event);
253 } else {
254 selectDocument(mDocFinder.apply(event));
255 }
256 mHandledTapUp = true;
257 return true;
258 }
259
260 // We'll toggle selection in onSingleTapConfirmed
261 // This avoids flickering on/off action mode when an item is double clicked.
262 if (!mSelectionMgr.hasSelection()) {
263 return false;
264 }
265
266 DocumentDetails doc = mDocFinder.apply(event);
267 if (doc == null) {
268 return false;
269 }
270
271 mHandledTapUp = true;
272 return selectDocument(doc);
273 }
274
275 boolean onSingleTapConfirmed(T event) {
276 if (mAteRightClick) {
277 mAteRightClick = false;
278 return false;
279 }
280 if (mHandledTapUp) {
281 mHandledTapUp = false;
282 return false;
283 }
284
285 if (mSelectionMgr.hasSelection()) {
286 return false; // should have been handled by onSingleTapUp.
287 }
288
289 DocumentDetails doc = mDocFinder.apply(event);
290 if (doc == null) {
291 return false;
292 }
293
294 return selectDocument(doc);
295 }
296
297 boolean onDoubleTap(T event) {
298 mHandledTapUp = false;
299 DocumentDetails doc = mDocFinder.apply(event);
300 if (doc != null) {
301 return mSelectionMgr.hasSelection()
302 ? selectDocument(doc)
303 : activateDocument(doc);
304 }
305 return false;
306 }
307
308 final void onLongPress(T event) {
309 if (!event.isOverItem()) {
310 return;
311 }
312
313 if (isRangeExtension(event)) {
314 extendSelectionRange(event);
315 } else {
316 selectDocument(mDocFinder.apply(event));
317 }
318 }
319
320 private boolean onSingleRightClickUp(T event) {
321 return mRightClickHandler.apply(event);
322 }
323
324 // hack alert from here through end of class.
325 private void setLastButtonState(int state) {
326 mLastButtonState = state;
327 }
328
329 private boolean eatRightClick() {
330 if (mLastButtonState == MotionEvent.BUTTON_SECONDARY) {
331 mLastButtonState = -1;
332 mAteRightClick = true;
333 return true;
334 }
335 return false;
336 }
337 }
338
339 private final class KeyInputHandler {
340 // TODO: Refactor FocusManager to depend only on DocumentDetails so we can eliminate
341 // difficult to test dependency on DocumentHolder.
342
343 boolean onKey(DocumentHolder doc, int keyCode, KeyEvent event) {
344 // Only handle key-down events. This is simpler, consistent with most other UIs, and
345 // enables the handling of repeated key events from holding down a key.
346 if (event.getAction() != KeyEvent.ACTION_DOWN) {
347 return false;
348 }
349
350 // Ignore tab key events. Those should be handled by the top-level key handler.
351 if (keyCode == KeyEvent.KEYCODE_TAB) {
352 return false;
353 }
354
355 if (mFocusHandler.handleKey(doc, keyCode, event)) {
356 // Handle range selection adjustments. Extending the selection will adjust the
357 // bounds of the in-progress range selection. Each time an unshifted navigation
358 // event is received, the range selection is restarted.
359 if (shouldExtendSelection(doc, event)) {
360 if (!mSelectionMgr.isRangeSelectionActive()) {
361 // Start a range selection if one isn't active
362 mSelectionMgr.startRangeSelection(doc.getAdapterPosition());
363 }
364 mSelectionMgr.snapRangeSelection(mFocusHandler.getFocusPosition());
365 } else {
366 mSelectionMgr.endRangeSelection();
367 }
368 return true;
369 }
370
371 // Handle enter key events
372 switch (keyCode) {
373 case KeyEvent.KEYCODE_ENTER:
374 if (event.isShiftPressed()) {
375 selectDocument(doc);
376 }
377 // For non-shifted enter keypresses, fall through.
378 case KeyEvent.KEYCODE_DPAD_CENTER:
379 case KeyEvent.KEYCODE_BUTTON_A:
380 return activateDocument(doc);
381 case KeyEvent.KEYCODE_FORWARD_DEL:
382 // This has to be handled here instead of in a keyboard shortcut, because
383 // keyboard shortcuts all have to be modified with the 'Ctrl' key.
384 if (mSelectionMgr.hasSelection()) {
385 mDeleteHandler.accept(doc);
386 }
387 // Always handle the key, even if there was nothing to delete. This is a
388 // precaution to prevent other handlers from potentially picking up the event
389 // and triggering extra behaviors.
390 return true;
391 }
392
393 return false;
394 }
395
396 private boolean shouldExtendSelection(DocumentDetails doc, KeyEvent event) {
397 if (!Events.isNavigationKeyCode(event.getKeyCode()) || !event.isShiftPressed()) {
398 return false;
399 }
400
401 return mSelectable.test(doc);
402 }
403 }
404
405 /**
406 * Class providing limited access to document view info.
407 */
408 public interface DocumentDetails {
409 String getModelId();
410 int getAdapterPosition();
411 boolean isInSelectionHotspot(InputEvent event);
412 }
413
414 @FunctionalInterface
415 interface EventHandler {
416 boolean apply(InputEvent event);
417 }
418
419 @FunctionalInterface
420 interface DocumentHandler {
421 boolean accept(DocumentDetails doc);
422 }
423}