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