UiScrollable.java revision e54d649fb83a0a44516e5c25a9ac1992c8950e59
1/*
2 * Copyright (C) 2012 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 */
16package com.android.uiautomator.core;
17
18import android.graphics.Rect;
19import android.util.Log;
20import android.view.accessibility.AccessibilityNodeInfo;
21
22/**
23 * UiScrollable is a {@link UiCollection} however this class provides additional functionality
24 * where the tests need to deal with scrollable contents or desire to enumerate lists of
25 * items. This calls can perform automatic searches within a scrollable container. Whether
26 * the content scrolls vertically or horizontally can be set by calling
27 * {@link #setAsVerticalList()} which is the default, or {@link #setAsHorizontalList()}.
28 */
29public class UiScrollable extends UiCollection {
30    private static final String LOG_TAG = UiScrollable.class.getSimpleName();
31
32    // More steps slows the swipe and prevents contents from being flung too far
33    private static final int SCROLL_STEPS = 55;
34
35    private static final int FLING_STEPS = 5;
36
37    // Restrict a swipe's starting and ending points inside a 10% margin of the target
38    private static final double DEFAULT_SWIPE_DEADZONE_PCT = 0.1;
39
40    // Limits the number of swipes/scrolls performed during a search
41    private static int mMaxSearchSwipes = 30;
42
43    // Used in ScrollForward() and ScrollBackward() to determine swipe direction
44    protected boolean mIsVerticalList = true;
45
46    private double mSwipeDeadZonePercentage = DEFAULT_SWIPE_DEADZONE_PCT;
47
48    /**
49     * UiScrollable is a {@link UiCollection} and as such requires a {@link By} selector to identify
50     * the UI element it represents. In the case of UiScrollable, the selector specified is
51     * considered a container where further calls to enumerate or find children will be performed
52     * in.
53     * @param container a {@link By} selector
54     */
55    public UiScrollable(By container) {
56        // wrap the container selector with container so that QueryController can handle
57        // this type of enumeration search accordingly
58        super(container);
59    }
60
61    /**
62     * Set the direction of swipes when performing scroll search
63     */
64    public void setAsVerticalList() {
65        mIsVerticalList = true;
66    }
67
68    /**
69     * Set the direction of swipes when performing scroll search
70     */
71    public void setAsHorizontalList() {
72        mIsVerticalList = false;
73    }
74
75    /**
76     * Used privately when performing swipe searches to decide if an element has become
77     * visible or not.
78     * @param selector
79     * @return true if found else false
80     */
81    protected boolean exists(By selector) {
82        if(getQueryController().findAccessibilityNodeInfo(selector) != null) {
83            return true;
84        }
85        return false;
86    }
87
88    /**
89     * Searches for child UI element within the constraints of this UiScrollable {@link By}
90     * selector. It looks for any child matching the <code>childPattern</code> argument that has
91     * a child UI element anywhere within its sub hierarchy that has content-description text.
92     * The returned UiObject will point at the <code>childPattern</code> instance that matched the
93     * search and not at the identifying child element that matched the content description.</p>
94     * By default this operation will perform scroll search while attempting to find the
95     * UI element.
96     * See {@link #getChildByDescription(By, String, boolean)}
97     * @param childPattern {@link By} selector of the child pattern to match and return
98     * @param text String of the identifying child contents of of the <code>childPattern</code>
99     * @return {@link UiObject} pointing at and instance of <code>childPattern</code>
100     * @throws UiObjectNotFoundException
101     */
102    @Override
103    public UiObject getChildByDescription(By childPattern, String text)
104            throws UiObjectNotFoundException {
105        return getChildByDescription(childPattern, text, true);
106    }
107
108    /**
109     * Searches for child UI element within the constraints of this UiScrollable {@link By}
110     * selector. It looks for any child matching the <code>childPattern</code> argument that has
111     * a child UI element anywhere within its sub hierarchy that has content-description text.
112     * The returned UiObject will point at the <code>childPattern</code> instance that matched the
113     * search and not at the identifying child element that matched the content description.
114     * @param childPattern {@link By} selector of the child pattern to match and return
115     * @param text String may be a partial match for the content-description of a child element.
116     * @param allowScrollSearch set to true if scrolling is allowed
117     * @return {@link UiObject} pointing at and instance of <code>childPattern</code>
118     * @throws UiObjectNotFoundException
119     */
120    public UiObject getChildByDescription(By childPattern, String text, boolean allowScrollSearch)
121            throws UiObjectNotFoundException {
122        if (text != null) {
123            if (allowScrollSearch) {
124                scrollIntoView(By.selector().descriptionContains(text));
125            }
126            return super.getChildByDescription(childPattern, text);
127        }
128        throw new UiObjectNotFoundException("for description= \"" + text + "\"");
129    }
130
131    /**
132     * Searches for child UI element within the constraints of this UiScrollable {@link By}
133     * selector. It looks for any child matching the <code>childPattern</code> argument and
134     * return the <code>instance</code> specified. The operation is performed only on the visible
135     * items and no scrolling is performed in this case.
136     * @param childPattern {@link By} selector of the child pattern to match and return
137     * @param instance int the desired matched instance of this <code>childPattern</code>
138     * @return {@link UiObject} pointing at and instance of <code>childPattern</code>
139     */
140    @Override
141    public UiObject getChildByInstance(By childPattern, int instance)
142            throws UiObjectNotFoundException {
143        By patternSelector = By.patternBuilder(getSelector(),
144                By.patternBuilder(childPattern).instance(instance));
145        return new UiObject(patternSelector);
146    }
147
148    /**
149     * Searches for child UI element within the constraints of this UiScrollable {@link By}
150     * selector. It looks for any child matching the <code>childPattern</code> argument that has
151     * a child UI element anywhere within its sub hierarchy that has text attribute =
152     * <code>text</code>. The returned UiObject will point at the <code>childPattern</code>
153     * instance that matched the search and not at the identifying child element that matched the
154     * text attribute.</p>
155     * By default this operation will perform scroll search while attempting to find the UI
156     * element.
157     * See {@link #getChildByText(By, String, boolean)}
158     * @param childPattern {@link By} selector of the child pattern to match and return
159     * @param text String of the identifying child contents of of the <code>childPattern</code>
160     * @return {@link UiObject} pointing at and instance of <code>childPattern</code>
161     * @throws UiObjectNotFoundException
162     */
163    @Override
164    public UiObject getChildByText(By childPattern, String text)
165            throws UiObjectNotFoundException {
166        return getChildByText(childPattern, text, true);
167    }
168
169    /**
170     * Searches for child UI element within the constraints of this UiScrollable {@link By}
171     * selector. It looks for any child matching the <code>childPattern</code> argument that has
172     * a child UI element anywhere within its sub hierarchy that has the text attribute =
173     * <code>text</code>.
174     * The returned UiObject will point at the <code>childPattern</code> instance that matched the
175     * search and not at the identifying child element that matched the text attribute.
176     * @param childPattern {@link By} selector of the child pattern to match and return
177     * @param text String of the identifying child contents of of the <code>childPattern</code>
178     * @param allowScrollSearch set to true if scrolling is allowed
179     * @return {@link UiObject} pointing at and instance of <code>childPattern</code>
180     * @throws UiObjectNotFoundException
181     */
182    public UiObject getChildByText(By childPattern, String text, boolean allowScrollSearch)
183            throws UiObjectNotFoundException {
184
185        if (text != null) {
186            if (allowScrollSearch) {
187                scrollIntoView(By.selector().text(text));
188            }
189            return super.getChildByText(childPattern, text);
190        }
191        throw new UiObjectNotFoundException("for text= \"" + text + "\"");
192    }
193
194    /**
195     * Performs a swipe Up on the associated UI element until the requested content-description
196     * is found or until swipe attempts have been exhausted. See {@link #setMaxSearchSwipes(int)}
197     * @param text to look for anywhere within the contents of this scrollable.
198     * @return true if item us found else false
199     */
200    public boolean scrollDescriptionIntoView(String text) {
201        return scrollIntoView(By.selector().description(text));
202    }
203
204    /**
205     * Perform a scroll search for a UI element matching the {@link By} selector argument. Also
206     * see {@link #scrollDescriptionIntoView(String)} and {@link #scrollTextIntoView(String)}.
207     * @param selector {@link By} selector
208     * @return true if the item was found and now is in view else false
209     */
210    public boolean scrollIntoView(By selector) {
211        // if we happen to be on top of the text we want then return here
212        if (exists(getSelector().childSelector(selector))) {
213            return (true);
214        } else {
215            // we will need to reset the search from the beginning to start search
216            scrollToBeginning(mMaxSearchSwipes);
217            if (exists(getSelector().childSelector(selector))) {
218                return (true);
219            }
220            for (int x = 0; x < mMaxSearchSwipes; x++) {
221                if(!scrollForward()) {
222                    return false;
223                }
224
225                if(exists(getSelector().childSelector(selector))) {
226                    return true;
227                }
228            }
229        }
230        return false;
231    }
232
233    /**
234     * Performs a swipe up on the associated display element until the requested text
235     * appears or until swipe attempts have been exhausted. See {@link #setMaxSearchSwipes(int)}
236     * @param text to look for
237     * @return true if item us found else false
238     */
239    public boolean scrollTextIntoView(String text) {
240        return scrollIntoView(By.selector().text(text));
241    }
242
243    /**
244     * {@link #getChildByDescription(String, boolean)} and {@link #getChildByText(String, boolean)}
245     * use an arguments that specifies if scrolling is allowed while searching for the UI element.
246     * The number of scrolls allowed to perform a search can be modified by this method.
247     * The current value can be read by calling {@link #getMaxSearchSwipes()}
248     * @param swipes
249     */
250    public void setMaxSearchSwipes(int swipes) {
251        mMaxSearchSwipes = swipes;
252    }
253
254    /**
255     * {@link #getChildByDescription(String, boolean)} and {@link #getChildByText(String, boolean)}
256     * use an arguments that specifies if scrolling is allowed while searching for the UI element.
257     * The number of scrolls currently allowed to perform a search can be read by this method.
258     * See {@link #setMaxSearchSwipes(int)}
259     * @return max value of the number of swipes currently allowed during a scroll search
260     */
261    public int getMaxSearchSwipes() {
262        return mMaxSearchSwipes;
263    }
264
265    /**
266     * A convenience version of {@link UiScrollable#scrollForward(int)}, performs a fling
267     *
268     * @return true if scrolled and false if can't scroll anymore
269     */
270    public boolean flingForward() {
271        return scrollForward(FLING_STEPS);
272    }
273
274    /**
275     * A convenience version of {@link UiScrollable#scrollForward(int)}, performs a regular scroll
276     *
277     * @return true if scrolled and false if can't scroll anymore
278     */
279    public boolean scrollForward() {
280        return scrollForward(SCROLL_STEPS);
281    }
282
283    /**
284     * Perform a scroll forward. If this list is set to vertical (see {@link #setAsVerticalList()}
285     * default) then the swipes will be executed from the bottom to top. If this list is set
286     * to horizontal (see {@link #setAsHorizontalList()}) then the swipes will be executed from
287     * the right to left.
288     *
289     * @param steps use steps to control the speed, so that it may be a scroll, or fling
290     * @return true if scrolled and false if can't scroll anymore
291     */
292    public boolean scrollForward(int steps) {
293        Log.d(LOG_TAG, "scrollForward() on selector = " + getSelector());
294        AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
295        if(node == null) {
296            // Object Not Found
297            return false;
298        }
299        Rect rect = new Rect();;
300        node.getBoundsInScreen(rect);
301
302        int downX = 0;
303        int downY = 0;
304        int upX = 0;
305        int upY = 0;
306
307        // scrolling is by default assumed vertically unless the object is explicitly
308        // set otherwise by setAsHorizontalContainer()
309        if(mIsVerticalList) {
310            int swipeAreaAdjust = (int)(rect.height() * getSwipeDeadZonePercentage());
311            // scroll vertically: swipe down -> up
312            downX = rect.centerX();
313            downY = rect.bottom - swipeAreaAdjust;
314            upX = rect.centerX();
315            upY = rect.top + swipeAreaAdjust;
316        } else {
317            int swipeAreaAdjust = (int)(rect.width() * getSwipeDeadZonePercentage());
318            // scroll horizontally: swipe right -> left
319            // TODO: Assuming device is not in right to left language
320            downX = rect.right - swipeAreaAdjust;
321            downY = rect.centerY();
322            upX = rect.left + swipeAreaAdjust;
323            upY = rect.centerY();
324        }
325        return getInteractionController().scrollSwipe(downX, downY, upX, upY, steps);
326    }
327
328    /**
329     * A convenience version of {@link UiScrollable#scrollBackward(int)}, performs a fling
330     *
331     * @return true if scrolled and false if can't scroll anymore
332     */
333    public boolean flingBackward() {
334        return scrollBackward(FLING_STEPS);
335    }
336
337    /**
338     * A convenience version of {@link UiScrollable#scrollBackward(int)}, performs a regular scroll
339     *
340     * @return true if scrolled and false if can't scroll anymore
341     */
342    public boolean scrollBackward() {
343        return scrollBackward(SCROLL_STEPS);
344    }
345
346    /**
347     * Perform a scroll backward. If this list is set to vertical (see {@link #setAsVerticalList()}
348     * default) then the swipes will be executed from the top to bottom. If this list is set
349     * to horizontal (see {@link #setAsHorizontalList()}) then the swipes will be executed from
350     * the left to right.
351     *
352     * @param steps use steps to control the speed, so that it may be a scroll, or fling
353     * @return true if scrolled and false if can't scroll anymore
354     */
355    public boolean scrollBackward(int steps) {
356        Log.d(LOG_TAG, "scrollBackward() on selector = " + getSelector());
357        AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
358        if(node == null) {
359            // Object Not Found
360            return false;
361        }
362        Rect rect = new Rect();;
363        node.getBoundsInScreen(rect);
364
365        int downX = 0;
366        int downY = 0;
367        int upX = 0;
368        int upY = 0;
369
370        // scrolling is by default assumed vertically unless the object is explicitly
371        // set otherwise by setAsHorizontalContainer()
372        if(mIsVerticalList) {
373            int swipeAreaAdjust = (int)(rect.height() * getSwipeDeadZonePercentage());
374            Log.d(LOG_TAG, "scrollToBegining() using vertical scroll");
375            // scroll vertically: swipe up -> down
376            downX = rect.centerX();
377            downY = rect.top + swipeAreaAdjust;
378            upX = rect.centerX();
379            upY = rect.bottom - swipeAreaAdjust;
380        } else {
381            int swipeAreaAdjust = (int)(rect.width() * getSwipeDeadZonePercentage());
382            Log.d(LOG_TAG, "scrollToBegining() using hotizontal scroll");
383            // scroll horizontally: swipe left -> right
384            // TODO: Assuming device is not in right to left language
385            downX = rect.left + swipeAreaAdjust;
386            downY = rect.centerY();
387            upX = rect.right - swipeAreaAdjust;
388            upY = rect.centerY();
389        }
390        return getInteractionController().scrollSwipe(downX, downY, upX, upY, steps);
391    }
392
393    /**
394     * Scrolls to the beginning of a scrollable UI element. The beginning could be the top most
395     * in case of vertical lists or the left most in case of horizontal lists.
396     *
397     * @param steps use steps to control the speed, so that it may be a scroll, or fling
398     * @return true on scrolled else false
399     */
400    public boolean scrollToBeginning(int maxSwipes, int steps) {
401        Log.d(LOG_TAG, "scrollToBeginning() on selector = " + getSelector());
402        // protect against potential hanging and return after preset attempts
403        for(int x = 0; x < maxSwipes; x++) {
404            if(!scrollBackward(steps)) {
405                break;
406            }
407        }
408        return true;
409    }
410
411    /**
412     * A convenience version of {@link UiScrollable#scrollToBeginning(int, int)} with regular scroll
413     *
414     * @param maxSwipes
415     * @return true on scrolled else false
416     */
417    public boolean scrollToBeginning(int maxSwipes) {
418        return scrollToBeginning(maxSwipes, SCROLL_STEPS);
419    }
420
421    /**
422     * A convenience version of {@link UiScrollable#scrollToBeginning(int, int)} with fling
423     *
424     * @param maxSwipes
425     * @return true on scrolled else false
426     */
427    public boolean flingToBeginning(int maxSwipes) {
428        return scrollToBeginning(maxSwipes, FLING_STEPS);
429    }
430
431    /**
432     * Scrolls to the end of a scrollable UI element. The end could be the bottom most
433     * in case of vertical controls or the right most for horizontal controls
434     *
435     * @param steps use steps to control the speed, so that it may be a scroll, or fling
436     * @return true on scrolled else false
437     */
438    public boolean scrollToEnd(int maxSwipes, int steps) {
439        // protect against potential hanging and return after preset attempts
440        for(int x = 0; x < maxSwipes; x++) {
441            if(!scrollForward(steps)) {
442                break;
443            }
444        }
445        return true;
446    }
447
448    /**
449     * A convenience version of {@link UiScrollable#scrollToEnd(int, int)} with regular scroll
450     *
451     * @param maxSwipes
452     * @return true on scrolled else false
453     */
454    public boolean scrollToEnd(int maxSwipes) {
455        return scrollToEnd(maxSwipes, SCROLL_STEPS);
456    }
457
458    /**
459     * A convenience version of {@link UiScrollable#scrollToEnd(int, int)} with fling
460     *
461     * @param maxSwipes
462     * @return true on scrolled else false
463     */
464    public boolean flingToEnd(int maxSwipes) {
465        return scrollToEnd(maxSwipes, FLING_STEPS);
466    }
467
468    public double getSwipeDeadZonePercentage() {
469        return mSwipeDeadZonePercentage;
470    }
471
472    public void setSwipeDeadZonePercentage(double swipeDeadZonePercentage) {
473        mSwipeDeadZonePercentage = swipeDeadZonePercentage;
474    }
475}
476