1 /*
2  * Copyright (C) 2013 DroidDriver committers
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 io.appium.droiddriver.actions;
18 
19 import android.graphics.Rect;
20 import android.os.SystemClock;
21 import android.view.ViewConfiguration;
22 
23 import io.appium.droiddriver.UiElement;
24 import io.appium.droiddriver.exceptions.ActionException;
25 import io.appium.droiddriver.scroll.Direction.PhysicalDirection;
26 import io.appium.droiddriver.util.Events;
27 import io.appium.droiddriver.util.Strings;
28 import io.appium.droiddriver.util.Strings.ToStringHelper;
29 
30 /**
31  * An action that swipes the touch screen.
32  */
33 public class SwipeAction extends EventAction implements ScrollAction {
34   // Milliseconds between synthesized ACTION_MOVE events.
35   // Note: ACTION_MOVE_INTERVAL is the minimum interval between injected events;
36   // the actual interval typically is longer.
37   private static final int ACTION_MOVE_INTERVAL = 5;
38   /**
39    * The magic number from UiAutomator. This value is empirical. If it actually
40    * results in a fling, you can change it with {@link #setScrollSteps}.
41    */
42   private static int scrollSteps = 55;
43   private static int flingSteps = 3;
44 
45   /** Returns the {@link #scrollSteps} used in {@link #toScroll}. */
getScrollSteps()46   public static int getScrollSteps() {
47     return scrollSteps;
48   }
49 
50   /** Sets the {@link #scrollSteps} used in {@link #toScroll}. */
setScrollSteps(int scrollSteps)51   public static void setScrollSteps(int scrollSteps) {
52     SwipeAction.scrollSteps = scrollSteps;
53   }
54 
55   /** Returns the {@link #flingSteps} used in {@link #toFling}. */
getFlingSteps()56   public static int getFlingSteps() {
57     return flingSteps;
58   }
59 
60   /** Sets the {@link #flingSteps} used in {@link #toFling}. */
setFlingSteps(int flingSteps)61   public static void setFlingSteps(int flingSteps) {
62     SwipeAction.flingSteps = flingSteps;
63   }
64 
65   /**
66    * Gets {@link SwipeAction} instances for scrolling.
67    * <p>
68    * Note: This may result in flinging instead of scrolling, depending on the
69    * size of the target UiElement and the SDK version of the device. If it does
70    * not behave as expected, you can change steps with {@link #setScrollSteps}.
71    * </p>
72    *
73    * @param direction specifies where the view port will move, instead of the
74    *        finger.
75    * @see ViewConfiguration#getScaledMinimumFlingVelocity
76    */
toScroll(PhysicalDirection direction)77   public static SwipeAction toScroll(PhysicalDirection direction) {
78     return new SwipeAction(direction, scrollSteps);
79   }
80 
81   /**
82    * Gets {@link SwipeAction} instances for flinging.
83    * <p>
84    * Note: This may not actually fling, depending on the size of the target
85    * UiElement and the SDK version of the device. If it does not behave as
86    * expected, you can change steps with {@link #setFlingSteps}.
87    * </p>
88    *
89    * @param direction specifies where the view port will move, instead of the
90    *        finger.
91    * @see ViewConfiguration#getScaledMinimumFlingVelocity
92    */
toFling(PhysicalDirection direction)93   public static SwipeAction toFling(PhysicalDirection direction) {
94     return new SwipeAction(direction, flingSteps);
95   }
96 
97   private final PhysicalDirection direction;
98   private final boolean drag;
99   private final int steps;
100   private final float topMarginRatio;
101   private final float leftMarginRatio;
102   private final float bottomMarginRatio;
103   private final float rightMarginRatio;
104 
105   /**
106    * Defaults timeoutMillis to 1000 and no drag.
107    */
SwipeAction(PhysicalDirection direction, int steps)108   public SwipeAction(PhysicalDirection direction, int steps) {
109     this(direction, steps, false, 1000L);
110   }
111 
112   /**
113    * Defaults all margin ratios to 0.1F.
114    */
SwipeAction(PhysicalDirection direction, int steps, boolean drag, long timeoutMillis)115   public SwipeAction(PhysicalDirection direction, int steps, boolean drag, long timeoutMillis) {
116     this(direction, steps, drag, timeoutMillis, 0.1F, 0.1F, 0.1F, 0.1F);
117   }
118 
119   /**
120    * @param direction the scroll direction specifying where the view port will
121    *        move, instead of the finger.
122    * @param steps minimum 2; (steps-1) is the number of {@code ACTION_MOVE} that
123    *        will be injected between {@code ACTION_DOWN} and {@code ACTION_UP}.
124    * @param drag whether this is a drag
125    * @param timeoutMillis the value returned by {@link #getTimeoutMillis}
126    * @param topMarginRatio margin ratio from top
127    * @param leftMarginRatio margin ratio from left
128    * @param bottomMarginRatio margin ratio from bottom
129    * @param rightMarginRatio margin ratio from right
130    */
SwipeAction(PhysicalDirection direction, int steps, boolean drag, long timeoutMillis, float topMarginRatio, float leftMarginRatio, float bottomMarginRatio, float rightMarginRatio)131   public SwipeAction(PhysicalDirection direction, int steps, boolean drag, long timeoutMillis,
132       float topMarginRatio, float leftMarginRatio, float bottomMarginRatio, float rightMarginRatio) {
133     super(timeoutMillis);
134     this.direction = direction;
135     this.steps = Math.max(2, steps);
136     this.drag = drag;
137     this.topMarginRatio = topMarginRatio;
138     this.bottomMarginRatio = bottomMarginRatio;
139     this.leftMarginRatio = leftMarginRatio;
140     this.rightMarginRatio = rightMarginRatio;
141   }
142 
143   @Override
perform(InputInjector injector, UiElement element)144   public boolean perform(InputInjector injector, UiElement element) {
145     Rect elementRect = element.getVisibleBounds();
146 
147     int topMargin = (int) (elementRect.height() * topMarginRatio);
148     int bottomMargin = (int) (elementRect.height() * bottomMarginRatio);
149     int leftMargin = (int) (elementRect.width() * leftMarginRatio);
150     int rightMargin = (int) (elementRect.width() * rightMarginRatio);
151     int adjustedbottom = elementRect.bottom - bottomMargin;
152     int adjustedTop = elementRect.top + topMargin;
153     int adjustedLeft = elementRect.left + leftMargin;
154     int adjustedRight = elementRect.right - rightMargin;
155     int startX;
156     int startY;
157     int endX;
158     int endY;
159 
160     switch (direction) {
161       case DOWN:
162         startX = elementRect.centerX();
163         startY = adjustedbottom;
164         endX = elementRect.centerX();
165         endY = adjustedTop;
166         break;
167       case UP:
168         startX = elementRect.centerX();
169         startY = adjustedTop;
170         endX = elementRect.centerX();
171         endY = adjustedbottom;
172         break;
173       case LEFT:
174         startX = adjustedLeft;
175         startY = elementRect.centerY();
176         endX = adjustedRight;
177         endY = elementRect.centerY();
178         break;
179       case RIGHT:
180         startX = adjustedRight;
181         startY = elementRect.centerY();
182         endX = adjustedLeft;
183         endY = elementRect.centerY();
184         break;
185       default:
186         throw new ActionException("Unknown scroll direction: " + direction);
187     }
188 
189     double xStep = ((double) (endX - startX)) / steps;
190     double yStep = ((double) (endY - startY)) / steps;
191 
192     // First touch starts exactly at the point requested
193     long downTime = Events.touchDown(injector, startX, startY);
194     SystemClock.sleep(ACTION_MOVE_INTERVAL);
195     if (drag) {
196       SystemClock.sleep((long) (ViewConfiguration.getLongPressTimeout() * 1.5f));
197     }
198     for (int i = 1; i < steps; i++) {
199       Events.touchMove(injector, downTime, startX + (int) (xStep * i), startY + (int) (yStep * i));
200       SystemClock.sleep(ACTION_MOVE_INTERVAL);
201     }
202     if (drag) {
203       // Hold final position for a little bit to simulate drag.
204       SystemClock.sleep(100);
205     }
206     Events.touchUp(injector, downTime, endX, endY);
207     return true;
208   }
209 
210   @Override
toString()211   public String toString() {
212     ToStringHelper toStringHelper = Strings.toStringHelper(this);
213     toStringHelper.addValue(direction);
214     toStringHelper.add("steps", steps);
215     if (drag) {
216       toStringHelper.addValue("drag");
217     }
218     return toStringHelper.toString();
219   }
220 }
221