1 /*
2  * Copyright (C) 2017 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.compatibility.common.util;
18 
19 import static org.mockito.Matchers.argThat;
20 import static org.mockito.Matchers.eq;
21 import static org.mockito.Mockito.inOrder;
22 import static org.mockito.Mockito.mock;
23 import static org.mockito.Mockito.times;
24 
25 import android.app.Instrumentation;
26 import android.os.SystemClock;
27 import android.view.InputDevice;
28 import android.view.MotionEvent;
29 import android.view.View;
30 
31 import androidx.test.InstrumentationRegistry;
32 
33 import org.mockito.ArgumentMatcher;
34 import org.mockito.InOrder;
35 
36 public final class CtsMouseUtil {
37 
38     // TODO(b/272376728): make it an instance object instead
39     private static final UserHelper sUserHelper = new UserHelper(
40             InstrumentationRegistry.getInstrumentation().getTargetContext());
41 
CtsMouseUtil()42     private CtsMouseUtil() {}
43 
installHoverListener(View view)44     public static View.OnHoverListener installHoverListener(View view) {
45         return installHoverListener(view, true);
46     }
47 
installHoverListener(View view, boolean result)48     public static View.OnHoverListener installHoverListener(View view, boolean result) {
49         final View.OnHoverListener mockListener = mock(View.OnHoverListener.class);
50         view.setOnHoverListener((v, event) -> {
51             // Clone the event to work around event instance reuse in the framework.
52             mockListener.onHover(v, MotionEvent.obtain(event));
53             return result;
54         });
55         return mockListener;
56     }
57 
clearHoverListener(View view)58     public static void clearHoverListener(View view) {
59         view.setOnHoverListener(null);
60     }
61 
obtainMouseEvent(int action, View anchor, int offsetX, int offsetY)62     public static MotionEvent obtainMouseEvent(int action, View anchor, int offsetX, int offsetY) {
63         final long eventTime = SystemClock.uptimeMillis();
64         final int[] screenPos = new int[2];
65         anchor.getLocationOnScreen(screenPos);
66         final int x = screenPos[0] + offsetX;
67         final int y = screenPos[1] + offsetY;
68         MotionEvent event = MotionEvent.obtain(eventTime, eventTime, action, x, y, 0);
69         sUserHelper.injectDisplayIdIfNeeded(event);
70         event.setSource(InputDevice.SOURCE_MOUSE);
71         return event;
72     }
73 
74     /**
75      * Emulates a hover move on a point relative to the top-left corner of the passed {@link View}.
76      * Offset parameters are used to compute the final screen coordinates of the tap point.
77      *
78      * @param instrumentation the instrumentation used to run the test
79      * @param anchor the anchor view to determine the tap location on the screen
80      * @param offsetX extra X offset for the move
81      * @param offsetY extra Y offset for the move
82      */
emulateHoverOnView(Instrumentation instrumentation, View anchor, int offsetX, int offsetY)83     public static void emulateHoverOnView(Instrumentation instrumentation, View anchor, int offsetX,
84             int offsetY) {
85         final long downTime = SystemClock.uptimeMillis();
86         final int[] screenPos = new int[2];
87         anchor.getLocationOnScreen(screenPos);
88         final int x = screenPos[0] + offsetX;
89         final int y = screenPos[1] + offsetY;
90         injectHoverEvent(instrumentation, downTime, x, y);
91     }
92 
injectHoverEvent(Instrumentation instrumentation, long downTime, int xOnScreen, int yOnScreen)93     private static void injectHoverEvent(Instrumentation instrumentation, long downTime,
94             int xOnScreen, int yOnScreen) {
95         MotionEvent event = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_HOVER_MOVE,
96                 xOnScreen, yOnScreen, 0);
97         sUserHelper.injectDisplayIdIfNeeded(event);
98         event.setSource(InputDevice.SOURCE_MOUSE);
99         instrumentation.sendPointerSync(event);
100         event.recycle();
101     }
102 
103     public static class ActionMatcher implements ArgumentMatcher<MotionEvent> {
104         private final int mAction;
105 
ActionMatcher(int action)106         public ActionMatcher(int action) {
107             mAction = action;
108         }
109 
110         @Override
matches(MotionEvent actual)111         public boolean matches(MotionEvent actual) {
112             return actual.getAction() == mAction;
113         }
114 
115         @Override
toString()116         public String toString() {
117             return "action=" + MotionEvent.actionToString(mAction);
118         }
119     }
120 
121     public static class PositionMatcher extends ActionMatcher {
122         private final int mX;
123         private final int mY;
124 
PositionMatcher(int action, int x, int y)125         public PositionMatcher(int action, int x, int y) {
126             super(action);
127             mX = x;
128             mY = y;
129         }
130 
131         @Override
matches(MotionEvent actual)132         public boolean matches(MotionEvent actual) {
133             return super.matches(actual)
134                     && Math.round(actual.getX()) == mX
135                     && Math.round(actual.getY()) == mY;
136         }
137 
138         @Override
toString()139         public String toString() {
140             return super.toString() + "@(" + mX + "," + mY + ")";
141         }
142     }
143 
verifyEnterMove(View.OnHoverListener listener, View view, int moveCount)144     public static void verifyEnterMove(View.OnHoverListener listener, View view, int moveCount) {
145         final InOrder inOrder = inOrder(listener);
146         verifyEnterMoveInternal(listener, view, moveCount, inOrder);
147         inOrder.verifyNoMoreInteractions();
148     }
149 
verifyEnterMoveExit( View.OnHoverListener listener, View view, int moveCount)150     public static void verifyEnterMoveExit(
151             View.OnHoverListener listener, View view, int moveCount) {
152         final InOrder inOrder = inOrder(listener);
153         verifyEnterMoveInternal(listener, view, moveCount, inOrder);
154         inOrder.verify(listener, times(1)).onHover(eq(view),
155                 argThat(new ActionMatcher(MotionEvent.ACTION_HOVER_EXIT)));
156         inOrder.verifyNoMoreInteractions();
157     }
158 
verifyEnterMoveInternal( View.OnHoverListener listener, View view, int moveCount, InOrder inOrder)159     private static void verifyEnterMoveInternal(
160             View.OnHoverListener listener, View view, int moveCount, InOrder inOrder) {
161         inOrder.verify(listener, times(1)).onHover(eq(view),
162                 argThat(new ActionMatcher(MotionEvent.ACTION_HOVER_ENTER)));
163         inOrder.verify(listener, times(moveCount)).onHover(eq(view),
164                 argThat(new ActionMatcher(MotionEvent.ACTION_HOVER_MOVE)));
165     }
166 }
167 
168