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