1 /*
2  * Copyright (C) 2015 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.base;
18 
19 import static com.android.documentsui.base.Shared.DEBUG;
20 
21 import android.graphics.Point;
22 import android.support.v7.widget.RecyclerView;
23 import android.util.Log;
24 import android.util.Pools;
25 import android.view.KeyEvent;
26 import android.view.MotionEvent;
27 import android.view.View;
28 
29 import com.android.documentsui.dirlist.DocumentDetails;
30 import com.android.documentsui.dirlist.DocumentHolder;
31 
32 import javax.annotation.Nullable;
33 
34 /**
35  * Utility code for dealing with MotionEvents.
36  */
37 public final class Events {
38 
39     /**
40      * Returns true if event was triggered by a mouse.
41      */
isMouseEvent(MotionEvent e)42     public static boolean isMouseEvent(MotionEvent e) {
43         int toolType = e.getToolType(0);
44         return toolType == MotionEvent.TOOL_TYPE_MOUSE;
45     }
46 
47     /**
48      * Returns true if event was triggered by a finger or stylus touch.
49      */
isActionDown(MotionEvent e)50     public static boolean isActionDown(MotionEvent e) {
51         return e.getActionMasked() == MotionEvent.ACTION_DOWN;
52     }
53 
54     /**
55      * Returns true if event was triggered by a finger or stylus touch.
56      */
isActionUp(MotionEvent e)57     public static boolean isActionUp(MotionEvent e) {
58         return e.getActionMasked() == MotionEvent.ACTION_UP;
59     }
60 
61     /**
62      * Returns true if the shift is pressed.
63      */
isShiftPressed(MotionEvent e)64     public boolean isShiftPressed(MotionEvent e) {
65         return hasShiftBit(e.getMetaState());
66     }
67 
68     /**
69      * Returns true if the event is a mouse drag event.
70      * @param e
71      * @return
72      */
isMouseDragEvent(InputEvent e)73     public static boolean isMouseDragEvent(InputEvent e) {
74         return e.isMouseEvent()
75                 && e.isActionMove()
76                 && e.isPrimaryButtonPressed()
77                 && e.isOverDragHotspot();
78     }
79 
80     /**
81      * Whether or not the given keyCode represents a navigation keystroke (e.g. up, down, home).
82      *
83      * @param keyCode
84      * @return
85      */
isNavigationKeyCode(int keyCode)86     public static boolean isNavigationKeyCode(int keyCode) {
87         switch (keyCode) {
88             case KeyEvent.KEYCODE_DPAD_UP:
89             case KeyEvent.KEYCODE_DPAD_DOWN:
90             case KeyEvent.KEYCODE_DPAD_LEFT:
91             case KeyEvent.KEYCODE_DPAD_RIGHT:
92             case KeyEvent.KEYCODE_MOVE_HOME:
93             case KeyEvent.KEYCODE_MOVE_END:
94             case KeyEvent.KEYCODE_PAGE_UP:
95             case KeyEvent.KEYCODE_PAGE_DOWN:
96                 return true;
97             default:
98                 return false;
99         }
100     }
101 
102 
103     /**
104      * Returns true if the "SHIFT" bit is set.
105      */
hasShiftBit(int metaState)106     public static boolean hasShiftBit(int metaState) {
107         return (metaState & KeyEvent.META_SHIFT_ON) != 0;
108     }
109 
hasCtrlBit(int metaState)110     public static boolean hasCtrlBit(int metaState) {
111         return (metaState & KeyEvent.META_CTRL_ON) != 0;
112     }
113 
hasAltBit(int metaState)114     public static boolean hasAltBit(int metaState) {
115         return (metaState & KeyEvent.META_ALT_ON) != 0;
116     }
117 
118     /**
119      * A facade over MotionEvent primarily designed to permit for unit testing
120      * of related code.
121      */
122     public interface InputEvent extends AutoCloseable {
isMouseEvent()123         boolean isMouseEvent();
isPrimaryButtonPressed()124         boolean isPrimaryButtonPressed();
isSecondaryButtonPressed()125         boolean isSecondaryButtonPressed();
isTertiaryButtonPressed()126         boolean isTertiaryButtonPressed();
isAltKeyDown()127         boolean isAltKeyDown();
isShiftKeyDown()128         boolean isShiftKeyDown();
isCtrlKeyDown()129         boolean isCtrlKeyDown();
130 
131         /** Returns true if the action is the initial press of a mouse or touch. */
isActionDown()132         boolean isActionDown();
133 
134         /** Returns true if the action is the final release of a mouse or touch. */
isActionUp()135         boolean isActionUp();
136 
137         /**
138          * Returns true when the action is the initial press of a non-primary (ex. second finger)
139          * pointer.
140          * See {@link MotionEvent#ACTION_POINTER_DOWN}.
141          */
isMultiPointerActionDown()142         boolean isMultiPointerActionDown();
143 
144         /**
145          * Returns true when the action is the final of a non-primary (ex. second finger)
146          * pointer.
147          * * See {@link MotionEvent#ACTION_POINTER_UP}.
148          */
isMultiPointerActionUp()149         boolean isMultiPointerActionUp();
150 
151         /** Returns true if the action is neither the initial nor the final release of a mouse
152          * or touch. */
isActionMove()153         boolean isActionMove();
154 
155         /** Returns true if the action is cancel. */
isActionCancel()156         boolean isActionCancel();
157 
158         // Eliminate the checked Exception from Autoclosable.
159         @Override
close()160         public void close();
161 
getOrigin()162         Point getOrigin();
getX()163         float getX();
getY()164         float getY();
getRawX()165         float getRawX();
getRawY()166         float getRawY();
getPointerCount()167         int getPointerCount();
168 
169         /** Returns true if there is an item under the finger/cursor. */
isOverItem()170         boolean isOverItem();
171 
172         /**
173          * Returns true if there is a model backed item under the finger/cursor.
174          * Resulting calls on the event instance should never return a null
175          * DocumentDetails and DocumentDetails#hasModelId should always return true
176          */
isOverModelItem()177         boolean isOverModelItem();
178 
179         /**
180          * Returns true if the event is over an area that can be dragged via touch.
181          * List items have a white area that is not draggable.
182          */
isOverDragHotspot()183         boolean isOverDragHotspot();
184 
185         /**
186          * Returns true if the event is a two/three-finger scroll on touchpad.
187          */
isTouchpadScroll()188         boolean isTouchpadScroll();
189 
190         /** Returns the adapter position of the item under the finger/cursor. */
getItemPosition()191         int getItemPosition();
192 
193         /** Returns the DocumentDetails for the item under the event, or null. */
getDocumentDetails()194         @Nullable DocumentDetails getDocumentDetails();
195     }
196 
197     public static final class MotionInputEvent implements InputEvent {
198         private static final String TAG = "MotionInputEvent";
199 
200         private static final int UNSET_POSITION = RecyclerView.NO_POSITION - 1;
201 
202         private static final Pools.SimplePool<MotionInputEvent> sPool = new Pools.SimplePool<>(1);
203 
204         private MotionEvent mEvent;
205         private @Nullable RecyclerView mRecView;
206 
207         private int mPosition = UNSET_POSITION;
208         private @Nullable DocumentDetails mDocDetails;
209 
MotionInputEvent()210         private MotionInputEvent() {
211             if (DEBUG) Log.i(TAG, "Created a new instance.");
212         }
213 
obtain(MotionEvent event, RecyclerView view)214         public static MotionInputEvent obtain(MotionEvent event, RecyclerView view) {
215             Shared.checkMainLoop();
216 
217             MotionInputEvent instance = sPool.acquire();
218             instance = (instance != null ? instance : new MotionInputEvent());
219 
220             instance.mEvent = event;
221             instance.mRecView = view;
222 
223             return instance;
224         }
225 
recycle()226         public void recycle() {
227             Shared.checkMainLoop();
228 
229             mEvent = null;
230             mRecView = null;
231             mPosition = UNSET_POSITION;
232             mDocDetails = null;
233 
234             boolean released = sPool.release(this);
235             // This assert is used to guarantee we won't generate too many instances that can't be
236             // held in the pool, which indicates our pool size is too small.
237             //
238             // Right now one instance is enough because we expect all instances are only used in
239             // main thread.
240             assert(released);
241         }
242 
243         @Override
close()244         public void close() {
245             recycle();
246         }
247 
248         @Override
isMouseEvent()249         public boolean isMouseEvent() {
250             return Events.isMouseEvent(mEvent);
251         }
252 
253         @Override
isPrimaryButtonPressed()254         public boolean isPrimaryButtonPressed() {
255             return mEvent.isButtonPressed(MotionEvent.BUTTON_PRIMARY);
256         }
257 
258         @Override
isSecondaryButtonPressed()259         public boolean isSecondaryButtonPressed() {
260             return mEvent.isButtonPressed(MotionEvent.BUTTON_SECONDARY);
261         }
262 
263         @Override
isTertiaryButtonPressed()264         public boolean isTertiaryButtonPressed() {
265             return mEvent.isButtonPressed(MotionEvent.BUTTON_TERTIARY);
266         }
267 
268         @Override
isAltKeyDown()269         public boolean isAltKeyDown() {
270             return Events.hasAltBit(mEvent.getMetaState());
271         }
272 
273         @Override
isShiftKeyDown()274         public boolean isShiftKeyDown() {
275             return Events.hasShiftBit(mEvent.getMetaState());
276         }
277 
278         @Override
isCtrlKeyDown()279         public boolean isCtrlKeyDown() {
280             return Events.hasCtrlBit(mEvent.getMetaState());
281         }
282 
283         @Override
isActionDown()284         public boolean isActionDown() {
285             return mEvent.getActionMasked() == MotionEvent.ACTION_DOWN;
286         }
287 
288         @Override
isActionUp()289         public boolean isActionUp() {
290             return mEvent.getActionMasked() == MotionEvent.ACTION_UP;
291         }
292 
293         @Override
isMultiPointerActionDown()294         public boolean isMultiPointerActionDown() {
295             return mEvent.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN;
296         }
297 
298         @Override
isMultiPointerActionUp()299         public boolean isMultiPointerActionUp() {
300             return mEvent.getActionMasked() == MotionEvent.ACTION_POINTER_UP;
301         }
302 
303 
304         @Override
isActionMove()305         public boolean isActionMove() {
306             return mEvent.getActionMasked() == MotionEvent.ACTION_MOVE;
307         }
308 
309         @Override
isActionCancel()310         public boolean isActionCancel() {
311             return mEvent.getActionMasked() == MotionEvent.ACTION_CANCEL;
312         }
313 
314         @Override
getOrigin()315         public Point getOrigin() {
316             return new Point((int) mEvent.getX(), (int) mEvent.getY());
317         }
318 
319         @Override
getX()320         public float getX() {
321             return mEvent.getX();
322         }
323 
324         @Override
getY()325         public float getY() {
326             return mEvent.getY();
327         }
328 
329         @Override
getRawX()330         public float getRawX() {
331             return mEvent.getRawX();
332         }
333 
334         @Override
getRawY()335         public float getRawY() {
336             return mEvent.getRawY();
337         }
338 
339         @Override
getPointerCount()340         public int getPointerCount() {
341             return mEvent.getPointerCount();
342         }
343 
344         @Override
isTouchpadScroll()345         public boolean isTouchpadScroll() {
346             // Touchpad inputs are treated as mouse inputs, and when scrolling, there are no buttons
347             // returned.
348             return isMouseEvent() && isActionMove() && mEvent.getButtonState() == 0;
349         }
350 
351         @Override
isOverDragHotspot()352         public boolean isOverDragHotspot() {
353             return isOverItem() ? getDocumentDetails().isInDragHotspot(this) : false;
354         }
355 
356         @Override
isOverItem()357         public boolean isOverItem() {
358             return getItemPosition() != RecyclerView.NO_POSITION;
359         }
360 
361         @Override
isOverModelItem()362         public boolean isOverModelItem() {
363             return isOverItem() && getDocumentDetails().hasModelId();
364         }
365 
366         @Override
getItemPosition()367         public int getItemPosition() {
368             if (mPosition == UNSET_POSITION) {
369                 View child = mRecView.findChildViewUnder(mEvent.getX(), mEvent.getY());
370                 mPosition = (child != null)
371                         ? mRecView.getChildAdapterPosition(child)
372                         : RecyclerView.NO_POSITION;
373             }
374             return mPosition;
375         }
376 
377         @Override
getDocumentDetails()378         public @Nullable DocumentDetails getDocumentDetails() {
379             if (mDocDetails == null) {
380                 View childView = mRecView.findChildViewUnder(mEvent.getX(), mEvent.getY());
381                 mDocDetails = (childView != null)
382                 ? (DocumentHolder) mRecView.getChildViewHolder(childView)
383                         : null;
384             }
385             if (isOverItem()) {
386                 assert(mDocDetails != null);
387             }
388             return mDocDetails;
389         }
390 
391         @Override
toString()392         public String toString() {
393             return new StringBuilder()
394                     .append("MotionInputEvent {")
395                     .append("isMouseEvent=").append(isMouseEvent())
396                     .append(" isPrimaryButtonPressed=").append(isPrimaryButtonPressed())
397                     .append(" isSecondaryButtonPressed=").append(isSecondaryButtonPressed())
398                     .append(" isShiftKeyDown=").append(isShiftKeyDown())
399                     .append(" isAltKeyDown=").append(isAltKeyDown())
400                     .append(" action(decoded)=").append(
401                             MotionEvent.actionToString(mEvent.getActionMasked()))
402                     .append(" getOrigin=").append(getOrigin())
403                     .append(" isOverItem=").append(isOverItem())
404                     .append(" getItemPosition=").append(getItemPosition())
405                     .append(" getDocumentDetails=").append(getDocumentDetails())
406                     .append(" getPointerCount=").append(getPointerCount())
407                     .append("}")
408                     .toString();
409         }
410     }
411 }
412