/* * Copyright 2011 Selenium committers * * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package io.selendroid.server.android; import android.app.Instrumentation; import android.content.Context; import android.os.PowerManager; import android.os.SystemClock; import android.view.InputDevice; import android.view.MotionEvent; import android.view.MotionEvent.PointerProperties; import android.view.View; import android.view.ViewConfiguration; import android.view.Window; import android.view.WindowManager; import android.view.MotionEvent.PointerCoords; import android.webkit.WebView; import android.widget.AbsListView; import android.widget.ScrollView; import io.selendroid.server.ServerInstrumentation; import io.selendroid.server.model.TouchScreen; import io.selendroid.server.model.interactions.Coordinates; import io.selendroid.server.util.SelendroidLogger; import io.selendroid.server.android.internal.Point; import io.selendroid.server.common.action.touch.FlickDirection; import io.selendroid.server.common.exceptions.SelendroidException; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.List; /** * Implements touch capabilities of a device. * */ public class AndroidTouchScreen implements TouchScreen { private static final int MOTION_EVENT_INJECTION_DELAY_MILLIS = 5; private static final int MOTION_EVENT_META_STATE = 0; private static final float MOTION_EVENT_X_PRECISION = 1.0f; private static final float MOTION_EVENT_Y_PRECISION = 1.0f; private static final int MOTION_EVENT_DEVICE_ID = 0; private static final int MOTION_EVENT_EDGE_FLAGS = 0; private static final int MOTION_EVENT_SOURCE = 0; private static final int MOTION_EVENT_FLAGS = 0; private final ServerInstrumentation instrumentation; private final MotionSender motions; private ArrayDeque<Pointer> pointers = new ArrayDeque<Pointer>(); public AndroidTouchScreen(ServerInstrumentation instrumentation, MotionSender motions) { this.instrumentation = instrumentation; this.motions = motions; } public void singleTap(Coordinates where) { Point toTap = where.getLocationOnScreen(); List<MotionEvent> motionEvents = new ArrayList<MotionEvent>(); long downTime = SystemClock.uptimeMillis(); motionEvents.add(getMotionEvent(downTime, downTime, MotionEvent.ACTION_DOWN, toTap)); motionEvents.add(getMotionEvent(downTime, downTime, MotionEvent.ACTION_UP, toTap)); motions.send(motionEvents); } public void down(int x, int y) { List<MotionEvent> event = new ArrayList<MotionEvent>(); long downTime = SystemClock.uptimeMillis(); Point coords = new Point(x, y); event.add(getMotionEvent(downTime, downTime, MotionEvent.ACTION_DOWN, coords)); motions.send(event); } public void down(int x, int y, int id) { List<MotionEvent> event = new ArrayList<MotionEvent>(); long downTime = SystemClock.uptimeMillis(); int action; Pointer p = new Pointer(id, x, y); if (pointers.isEmpty()) { action = MotionEvent.ACTION_DOWN; pointers.add(p); } else { action = MotionEvent.ACTION_POINTER_DOWN; pointers.addFirst(p); } event.add(getMotionEvent(downTime, downTime, action)); motions.send(event); } public void up(int x, int y) { List<MotionEvent> event = new ArrayList<MotionEvent>(); long downTime = SystemClock.uptimeMillis(); Point coords = new Point(x, y); event.add(getMotionEvent(downTime, downTime, MotionEvent.ACTION_UP, coords)); motions.send(event); } public void up(int x, int y, int id) { List<MotionEvent> event = new ArrayList<MotionEvent>(); long downTime = SystemClock.uptimeMillis(); int action; if (pointers.size() == 1) { action = MotionEvent.ACTION_UP; } else { action = MotionEvent.ACTION_POINTER_UP; movePointerToFront(id); } event.add(getMotionEvent(downTime, downTime, action)); motions.send(event); pointers.removeFirst(); } private void movePointerToFront(int id) { for(Pointer p : pointers) { if(p.getId() == id) { pointers.remove(p); pointers.addFirst(p); break; } } } public void move(int x, int y) { List<MotionEvent> event = new ArrayList<MotionEvent>(); long downTime = SystemClock.uptimeMillis(); Point coords = new Point(x, y); event.add(getMotionEvent(downTime, downTime, MotionEvent.ACTION_MOVE, coords)); motions.send(event); } public void move(int x, int y, int id) { List<MotionEvent> event = new ArrayList<MotionEvent>(); long downTime = SystemClock.uptimeMillis(); for(Pointer p : pointers) { if (p.getId() == id) { p.setCoords(x, y); } } event.add(getMotionEvent(downTime, downTime, MotionEvent.ACTION_MOVE)); motions.send(event); } public void cancel() { for(Pointer p : pointers) { up((int)p.getCoords().x, (int)p.getCoords().y, p.getId()); } } public void scroll(Coordinates where, int xOffset, int yOffset) { long downTime = SystemClock.uptimeMillis(); List<MotionEvent> motionEvents = new ArrayList<MotionEvent>(); Point origin = where.getLocationOnScreen(); Point destination = new Point(origin.x + xOffset, origin.y + yOffset); motionEvents.add(getMotionEvent(downTime, downTime, MotionEvent.ACTION_DOWN, origin)); Scroll scroll = new Scroll(origin, destination, downTime); // Initial acceleration from origin to reference point motionEvents.add(getBatchedMotionEvent(downTime, downTime, origin, scroll.getDecelerationPoint(), Scroll.INITIAL_STEPS, Scroll.TIME_BETWEEN_EVENTS)); // Deceleration phase from reference point to destination motionEvents.add(getBatchedMotionEvent(downTime, scroll.getEventTimeForReferencePoint(), scroll.getDecelerationPoint(), destination, Scroll.DECELERATION_STEPS, Scroll.TIME_BETWEEN_EVENTS)); motionEvents.add(getMotionEvent(downTime, (downTime + scroll.getEventTimeForDestinationPoint()), MotionEvent.ACTION_UP, destination)); motions.send(motionEvents); } public void doubleTap(Coordinates where) { Point toDoubleTap = where.getLocationOnScreen(); List<MotionEvent> motionEvents = new ArrayList<MotionEvent>(); long downTime = SystemClock.uptimeMillis(); motionEvents.add(getMotionEvent(downTime, downTime, MotionEvent.ACTION_DOWN, toDoubleTap)); motionEvents.add(getMotionEvent(downTime, downTime, MotionEvent.ACTION_UP, toDoubleTap)); motionEvents.add(getMotionEvent(downTime, downTime, MotionEvent.ACTION_DOWN, toDoubleTap)); motionEvents.add(getMotionEvent(downTime, downTime, MotionEvent.ACTION_UP, toDoubleTap)); motions.send(motionEvents); } public void longPress(Coordinates where) { long downTime = SystemClock.uptimeMillis(); long eventTime = SystemClock.uptimeMillis(); Point point = where.getLocationOnScreen(); Instrumentation inst = instrumentation.getInstrumentation(); MotionEvent event = null; boolean isSuccess = false; int retry = 0; while (!isSuccess && retry < 10) { try { if (event == null) { event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_DOWN, point.x, point.y, 0); } SelendroidLogger.debug("trying to send pointer"); inst.sendPointerSync(event); isSuccess = true; } catch (SecurityException e) { SelendroidLogger.error("failed: " + retry); retry++; } } if (!isSuccess) { throw new SelendroidException("Click can not be completed!"); } inst.sendPointerSync(event); inst.waitForIdleSync(); eventTime = SystemClock.uptimeMillis(); final int touchSlop = ViewConfiguration.get(inst.getTargetContext()).getScaledTouchSlop(); event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_MOVE, point.x + touchSlop / 2, point.y + touchSlop / 2, 0); inst.sendPointerSync(event); inst.waitForIdleSync(); try { Thread.sleep((long) (ViewConfiguration.getLongPressTimeout() * 1.5f)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } eventTime = SystemClock.uptimeMillis(); event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_UP, point.x, point.y, 0); inst.sendPointerSync(event); inst.waitForIdleSync(); } public void scroll(final int xOffset, final int yOffset) { List<View> scrollableContainer = ViewHierarchyAnalyzer.getDefaultInstance().findScrollableContainer(); if (scrollableContainer == null) { // nothing to do return; } for (View view : scrollableContainer) { if (view instanceof AbsListView) { final AbsListView absListView = (AbsListView) view; instrumentation.getCurrentActivity().runOnUiThread(new Runnable() { public void run() { absListView.scrollBy(xOffset, yOffset); } }); } else if (view instanceof ScrollView) { final ScrollView scrollView = (ScrollView) view; instrumentation.getCurrentActivity().runOnUiThread(new Runnable() { public void run() { scrollView.scrollBy(xOffset, yOffset); } }); } else if (view instanceof WebView) { final WebView webView = (WebView) view; instrumentation.getCurrentActivity().runOnUiThread(new Runnable() { public void run() { webView.scrollBy(xOffset, yOffset); } }); } } } public void flick(final int speedX, final int speedY) { List<View> scrollableContainer = ViewHierarchyAnalyzer.getDefaultInstance().findScrollableContainer(); if (scrollableContainer == null) { // nothing to do return; } for (View view : scrollableContainer) { if (view instanceof AbsListView) { // ignore } else if (view instanceof ScrollView) { final ScrollView scrollView = (ScrollView) view; instrumentation.getCurrentActivity().runOnUiThread(new Runnable() { public void run() { scrollView.fling(speedY); } }); } else if (view instanceof WebView) { final WebView webView = (WebView) view; instrumentation.getCurrentActivity().runOnUiThread(new Runnable() { public void run() { webView.flingScroll(speedX, speedY); } }); } } } public void flick(Coordinates where, int xOffset, int yOffset, int speed) { long downTime = SystemClock.uptimeMillis(); List<MotionEvent> motionEvents = new ArrayList<MotionEvent>(); Point origin = where.getLocationOnScreen(); Point destination = new Point(origin.x + xOffset, origin.y + yOffset); DynamicIntervalFlick flick = new DynamicIntervalFlick(speed); motionEvents.add(getMotionEvent(downTime, downTime, MotionEvent.ACTION_DOWN, origin)); motionEvents.add(getBatchedMotionEvent(downTime, downTime, origin, destination, flick.getNumberOfSteps(), flick.getTimeBetweenEvents())); motionEvents.add(getMotionEvent(downTime, flick.getTimeForDestinationPoint(downTime), MotionEvent.ACTION_UP, destination)); motions.send(motionEvents); } public void flick(Point origin, FlickDirection direction, int distance, int duration) { int xOffset = distance * direction.getxMultiplier(); int yOffset = distance * direction.getyMultiplier(); Point destination = new Point(origin.x + xOffset, origin.y + yOffset); generateFlickMotions(origin, destination, new FixedIntervalFlick(duration)); } private void generateFlickMotions(Point origin, Point destination, Flick flick) { long downTime = SystemClock.uptimeMillis(); List<MotionEvent> motionEvents = new ArrayList<MotionEvent>(); motionEvents.add(getMotionEvent(downTime, downTime, MotionEvent.ACTION_DOWN, origin)); motionEvents.add(getBatchedMotionEvent( downTime, downTime, origin, destination, flick.getNumberOfSteps(), flick.getTimeBetweenEvents())); motionEvents.add(getMotionEvent(downTime, flick.getTimeForDestinationPoint(downTime), MotionEvent.ACTION_UP, destination)); motions.send(motionEvents); } private MotionEvent getMotionEvent(long start, long eventTime, int action, Point coords) { return MotionEvent.obtain(start, eventTime, action, coords.x, coords.y, 0); } private MotionEvent getMotionEvent(long start, long eventTime, int action) { return MotionEvent.obtain(start, eventTime, action, pointers.size(), getPointerIds(), getPointerCoords(), MOTION_EVENT_META_STATE, MOTION_EVENT_X_PRECISION, MOTION_EVENT_Y_PRECISION, MOTION_EVENT_DEVICE_ID, MOTION_EVENT_EDGE_FLAGS, MOTION_EVENT_SOURCE, MOTION_EVENT_FLAGS); } private int[] getPointerIds () { int[] pointerIds = new int[pointers.size()]; int i = 0; for(Pointer p : pointers) { pointerIds[i++] = p.getId(); } return pointerIds; } private PointerCoords[] getPointerCoords () { PointerCoords[] pointerCoords = new PointerCoords[pointers.size()]; int i = 0; for(Pointer p : pointers) { pointerCoords[i++] = p.getCoords(); } return pointerCoords; } private MotionEvent getBatchedMotionEvent(long downTime, long startingEventTime, Point origin, Point destination, int steps, long timeBetweenEvents) { float xStep = (destination.x - origin.x) / steps; float yStep = (destination.y - origin.y) / steps; float x = origin.x; float y = origin.y; long eventTime = startingEventTime; x += xStep; y += yStep; MotionEvent event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_MOVE, x, y, 0); for (int i = 0; i < steps - 1; i++) { x += xStep; y += yStep; eventTime += timeBetweenEvents; event.addBatch(eventTime, x, y, 1.0f, 1.0f, 0); } eventTime += timeBetweenEvents; event.addBatch(eventTime, destination.getX(), destination.getY(), 1.0f, 1.0f, 0); return event; } @Override public float getBrightness() { PowerManager powerManager = (PowerManager) instrumentation.getInstrumentation().getContext().getSystemService(Context.POWER_SERVICE); if (!powerManager.isScreenOn()) { return 0f; } else { WindowManager.LayoutParams attributes = instrumentation.getCurrentActivity().getWindow().getAttributes(); return attributes.screenBrightness; } } @Override public void setBrightness(float brightness) { if (brightness < 0) { brightness = 0; } if (brightness > 1) { brightness = 1; } PowerManager powerManager = (PowerManager) instrumentation.getInstrumentation().getContext().getSystemService(Context.POWER_SERVICE); final Window window = instrumentation.getCurrentActivity().getWindow(); final WindowManager.LayoutParams attributes = window.getAttributes(); PowerManager.WakeLock wakeLock = null; if (brightness != 0) { // Turn on display if (!powerManager.isScreenOn()) { wakeLock = powerManager.newWakeLock( PowerManager.ACQUIRE_CAUSES_WAKEUP | PowerManager.ON_AFTER_RELEASE, "Selendroid screen wake"); } // Now set the brightness attributes.screenBrightness = brightness; } else { // Turn off the display. Oh boy. This is derived from a reading of the PowerManager SDK docs. // http://developer.android.com/reference/android/os/PowerManager.html attributes.screenBrightness = 0; wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Selendroid screen sleep"); } instrumentation.getCurrentActivity().runOnUiThread( new Runnable() { @Override public void run() { window.setAttributes(attributes); } } ); instrumentation.getInstrumentation().waitForIdleSync(); if (wakeLock != null) { try { wakeLock.acquire(); wakeLock.release(); } catch (SecurityException ignored) { // We can only turn off the screen if the AUT has the android.permission.WAKE_LOCK permission. } } } final class Scroll { private Point origin; private Point destination; private long downTime; // A regular scroll usually has 15 gestures, where the last 5 are used for deceleration final static int INITIAL_STEPS = 10; final static int DECELERATION_STEPS = 5; final int TOTAL_STEPS = INITIAL_STEPS + DECELERATION_STEPS; // Time in milliseconds to provide a speed similar to scroll final static long TIME_BETWEEN_EVENTS = 50; public Scroll(Point origin, Point destination, long downTime) { this.origin = origin; this.destination = destination; this.downTime = downTime; } // This method is used to calculate the point where the deceleration will start at 20% of the // distance to the destination point private Point getDecelerationPoint() { int deltaX = (destination.x - origin.x); int deltaY = (destination.y - origin.y); // Coordinates of reference point where deceleration should start for scroll gesture, on the // last 20% of the total distance to scroll int xRef = (int) (deltaX * 0.8); int yRef = (int) (deltaY * 0.8); return new Point(origin.x + xRef, origin.y + yRef); } private long getEventTimeForReferencePoint() { return (downTime + INITIAL_STEPS * TIME_BETWEEN_EVENTS); } private long getEventTimeForDestinationPoint() { return (downTime + TOTAL_STEPS * TIME_BETWEEN_EVENTS); } } interface Flick { public int getNumberOfSteps(); public long getTimeBetweenEvents(); public long getTimeForDestinationPoint(long downTime); } final class DynamicIntervalFlick implements Flick { private final int SPEED_NORMAL = 0; private final int SPEED_FAST = 1; private final int SPEED_SLOW = 2; private int speed; public DynamicIntervalFlick(int speed) { this.speed = speed; } public int getNumberOfSteps() { return speed == SPEED_SLOW ? 8 : 4; } public long getTimeBetweenEvents() { switch (speed) { case SPEED_SLOW: return 50; case SPEED_NORMAL: return 25; // Time in milliseconds to provide a speed similar to normal flick case SPEED_FAST: return 9; default: return 0; } } public long getTimeForDestinationPoint(long downTime) { return (downTime + getNumberOfSteps() * getTimeBetweenEvents()); } } final class FixedIntervalFlick implements Flick { private int EVENT_INTERVAL_MS = 15; private int time; // Total time in ms of flick gesture public FixedIntervalFlick(int time) { this.time = time; } public int getNumberOfSteps() { return time / EVENT_INTERVAL_MS; } public long getTimeBetweenEvents() { return (time == 0) ? 0 : EVENT_INTERVAL_MS; } public long getTimeForDestinationPoint(long downTime) { return downTime + time; } } public class Pointer { private final int id; private PointerCoords coords; public Pointer(int id, int x, int y) { this.id = id; coords = new PointerCoords(); setCoords(x, y); } public int getId() { return id; } public PointerCoords getCoords() { return coords; } public void setCoords(int x, int y) { coords.x = x; coords.y = y; } } }