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.bots;
18 
19 import android.app.UiAutomation;
20 import android.content.Context;
21 import android.graphics.Point;
22 import android.graphics.Rect;
23 import android.os.SystemClock;
24 import android.view.InputDevice;
25 import android.view.MotionEvent;
26 import android.view.MotionEvent.PointerCoords;
27 import android.view.MotionEvent.PointerProperties;
28 
29 import androidx.test.uiautomator.Configurator;
30 import androidx.test.uiautomator.UiDevice;
31 import androidx.test.uiautomator.UiObject;
32 import androidx.test.uiautomator.UiObjectNotFoundException;
33 import androidx.test.uiautomator.UiSelector;
34 
35 /**
36  * A test helper class that provides support for controlling directory list
37  * and making assertions against the state of it.
38  */
39 public class GestureBot extends Bots.BaseBot {
40     private static final int LONGPRESS_STEPS = 60;
41     private static final int TRAVELING_STEPS = 20;
42     private static final int BAND_SELECTION_DEFAULT_STEPS = 100;
43     private static final int STEPS_INBETWEEN_POINTS = 2;
44     // Inserted after each motion event injection.
45     private static final int MOTION_EVENT_INJECTION_DELAY_MILLIS = 5;
46     private static final int LONG_PRESS_EVENT_INJECTION_DELAY_MILIS = 1000;
47     private final String mDirContainerId;
48     private final String mDirListId;
49     private final UiAutomation mAutomation;
50     private long mDownTime = 0;
51 
GestureBot(UiDevice device, UiAutomation automation, Context context, int timeout)52     public GestureBot(UiDevice device, UiAutomation automation, Context context, int timeout) {
53         super(device, context, timeout);
54         mDirContainerId = mTargetPackage + ":id/container_directory";
55         mDirListId = mTargetPackage + ":id/dir_list";
56         mAutomation = automation;
57     }
58 
gestureSelectFiles(String startLabel, String endLabel)59     public void gestureSelectFiles(String startLabel, String endLabel) throws Exception {
60         int toolType = Configurator.getInstance().getToolType();
61         Configurator.getInstance().setToolType(MotionEvent.TOOL_TYPE_FINGER);
62         Rect startCoord = findDocument(startLabel).getBounds();
63         Rect endCoord = findDocument(endLabel).getBounds();
64         double diffX = endCoord.centerX() - startCoord.centerX();
65         double diffY = endCoord.centerY() - startCoord.centerY();
66         Point[] points = new Point[LONGPRESS_STEPS + TRAVELING_STEPS];
67 
68         // First simulate long-press by having a bunch of MOVE events in the same coordinate
69         for (int i = 0; i < LONGPRESS_STEPS; i++) {
70             points[i] = new Point(startCoord.centerX(), startCoord.centerY());
71         }
72 
73         // Next put the actual drag/move events
74         for (int i = 0; i < TRAVELING_STEPS; i++) {
75             int newX = startCoord.centerX() + (int) (diffX / TRAVELING_STEPS * i);
76             int newY = startCoord.centerY() + (int) (diffY / TRAVELING_STEPS * i);
77             points[i + LONGPRESS_STEPS] = new Point(newX, newY);
78         }
79         mDevice.swipe(points, STEPS_INBETWEEN_POINTS);
80         Configurator.getInstance().setToolType(toolType);
81     }
82 
bandSelection(Point start, Point end)83     public void bandSelection(Point start, Point end) throws Exception {
84         bandSelection(start, end, BAND_SELECTION_DEFAULT_STEPS);
85     }
86 
fingerSelection(Point start, Point end)87     public void fingerSelection(Point start, Point end) throws Exception {
88         fingerSelection(start, end, BAND_SELECTION_DEFAULT_STEPS);
89     }
90 
bandSelection(Point start, Point end, int steps)91     public void bandSelection(Point start, Point end, int steps) throws Exception {
92         int toolType = Configurator.getInstance().getToolType();
93         Configurator.getInstance().setToolType(MotionEvent.TOOL_TYPE_MOUSE);
94         swipe(start.x, start.y, end.x, end.y, steps, MotionEvent.BUTTON_PRIMARY, false);
95         Configurator.getInstance().setToolType(toolType);
96     }
97 
fingerSelection(Point start, Point end, int steps)98     private void fingerSelection(Point start, Point end, int steps) throws Exception {
99         int toolType = Configurator.getInstance().getToolType();
100         Configurator.getInstance().setToolType(MotionEvent.TOOL_TYPE_FINGER);
101         swipe(start.x, start.y, end.x, end.y, steps, MotionEvent.BUTTON_PRIMARY, true);
102         Configurator.getInstance().setToolType(toolType);
103     }
104 
findDocument(String label)105     public UiObject findDocument(String label) throws UiObjectNotFoundException {
106         final UiSelector docList = new UiSelector().resourceId(
107                 mDirContainerId).childSelector(
108                 new UiSelector().resourceId(mDirListId));
109 
110         // Wait for the first list item to appear
111         new UiObject(docList.childSelector(new UiSelector())).waitForExists(mTimeout);
112 
113         return mDevice.findObject(docList.childSelector(new UiSelector().text(label)));
114     }
115 
swipe(int downX, int downY, int upX, int upY, int steps, int button, boolean fingerSelection)116     private void swipe(int downX, int downY, int upX, int upY, int steps, int button,
117             boolean fingerSelection) {
118         int swipeSteps = steps;
119         double xStep = 0;
120         double yStep = 0;
121 
122         // avoid a divide by zero
123         if (swipeSteps == 0) {
124             swipeSteps = 1;
125         }
126 
127         xStep = ((double) (upX - downX)) / swipeSteps;
128         yStep = ((double) (upY - downY)) / swipeSteps;
129 
130         // first touch starts exactly at the point requested
131         touchDown(downX, downY, button);
132         if (fingerSelection) {
133             SystemClock.sleep(LONG_PRESS_EVENT_INJECTION_DELAY_MILIS);
134         }
135         for (int i = 1; i < swipeSteps; i++) {
136             touchMove(downX + (int) (xStep * i), downY + (int) (yStep * i), button);
137             // set some known constant delay between steps as without it this
138             // become completely dependent on the speed of the system and results
139             // may vary on different devices. This guarantees at minimum we have
140             // a preset delay.
141             SystemClock.sleep(MOTION_EVENT_INJECTION_DELAY_MILLIS);
142         }
143         touchUp(upX, upY);
144     }
145 
touchDown(int x, int y, int button)146     private boolean touchDown(int x, int y, int button) {
147         long mDownTime = SystemClock.uptimeMillis();
148         MotionEvent event = getMotionEvent(mDownTime, mDownTime, MotionEvent.ACTION_DOWN, button, x,
149                 y);
150         return mAutomation.injectInputEvent(event, true);
151     }
152 
touchUp(int x, int y)153     private boolean touchUp(int x, int y) {
154         final long eventTime = SystemClock.uptimeMillis();
155         MotionEvent event = getMotionEvent(mDownTime, eventTime, MotionEvent.ACTION_UP, 0, x, y);
156         mDownTime = 0;
157         return mAutomation.injectInputEvent(event, true);
158     }
159 
touchMove(int x, int y, int button)160     private boolean touchMove(int x, int y, int button) {
161         final long eventTime = SystemClock.uptimeMillis();
162         MotionEvent event = getMotionEvent(mDownTime, eventTime, MotionEvent.ACTION_MOVE, button, x,
163                 y);
164         return mAutomation.injectInputEvent(event, true);
165     }
166 
167     /** Helper function to obtain a MotionEvent. */
getMotionEvent(long downTime, long eventTime, int action, int button, float x, float y)168     private static MotionEvent getMotionEvent(long downTime, long eventTime, int action, int button,
169             float x, float y) {
170 
171         PointerProperties properties = new PointerProperties();
172         properties.id = 0;
173         properties.toolType = Configurator.getInstance().getToolType();
174 
175         PointerCoords coords = new PointerCoords();
176         coords.pressure = 1;
177         coords.size = 1;
178         coords.x = x;
179         coords.y = y;
180 
181         return MotionEvent.obtain(downTime, eventTime, action, 1,
182                 new PointerProperties[]{properties}, new PointerCoords[]{coords},
183                 0, button, 1.0f, 1.0f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
184     }
185 }
186