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 * @since API Level 16
26 */
27public class UiScrollable extends UiCollection {
28    private static final String LOG_TAG = UiScrollable.class.getSimpleName();
29
30    // More steps slows the swipe and prevents contents from being flung too far
31    private static final int SCROLL_STEPS = 55;
32
33    private static final int FLING_STEPS = 5;
34
35    // Restrict a swipe's starting and ending points inside a 10% margin of the target
36    private static final double DEFAULT_SWIPE_DEADZONE_PCT = 0.1;
37
38    // Limits the number of swipes/scrolls performed during a search
39    private static int mMaxSearchSwipes = 30;
40
41    // Used in ScrollForward() and ScrollBackward() to determine swipe direction
42    private boolean mIsVerticalList = true;
43
44    private double mSwipeDeadZonePercentage = DEFAULT_SWIPE_DEADZONE_PCT;
45
46    /**
47     * UiScrollable is a {@link UiCollection} and as such requires a {@link UiSelector} to
48     * identify the container UI element of the scrollable collection. Further operations on
49     * the items in the container will require specifying UiSelector as an item selector.
50     *
51     * @param container a {@link UiSelector} selector
52     * @since API Level 16
53     */
54    public UiScrollable(UiSelector container) {
55        // wrap the container selector with container so that QueryController can handle
56        // this type of enumeration search accordingly
57        super(container);
58    }
59
60    /**
61     * Set the direction of swipes when performing scroll search
62     * @return reference to itself
63     * @since API Level 16
64     */
65    public UiScrollable setAsVerticalList() {
66        Tracer.trace();
67        mIsVerticalList = true;
68        return this;
69    }
70
71    /**
72     * Set the direction of swipes when performing scroll search
73     * @return reference to itself
74     * @since API Level 16
75     */
76    public UiScrollable setAsHorizontalList() {
77        Tracer.trace();
78        mIsVerticalList = false;
79        return this;
80    }
81
82    /**
83     * Used privately when performing swipe searches to decide if an element has become
84     * visible or not.
85     *
86     * @param selector
87     * @return true if found else false
88     * @since API Level 16
89     */
90    protected boolean exists(UiSelector selector) {
91        if(getQueryController().findAccessibilityNodeInfo(selector) != null) {
92            return true;
93        }
94        return false;
95    }
96
97    /**
98     * Searches for child UI element within the constraints of this UiScrollable {@link UiSelector}
99     * container. It looks for any child matching the <code>childPattern</code> argument within its
100     * hierarchy with a matching content-description text. The returned UiObject will represent the
101     * UI element matching the <code>childPattern</code> and not the sub element that matched the
102     * content description.</p>
103     * By default this operation will perform scroll search while attempting to find the UI element
104     * See {@link #getChildByDescription(UiSelector, String, boolean)}
105     *
106     * @param childPattern {@link UiSelector} selector of the child pattern to match and return
107     * @param text String of the identifying child contents of of the <code>childPattern</code>
108     * @return {@link UiObject} pointing at and instance of <code>childPattern</code>
109     * @throws UiObjectNotFoundException
110     * @since API Level 16
111     */
112    @Override
113    public UiObject getChildByDescription(UiSelector childPattern, String text)
114            throws UiObjectNotFoundException {
115        Tracer.trace(childPattern, text);
116        return getChildByDescription(childPattern, text, true);
117    }
118
119    /**
120     * See {@link #getChildByDescription(UiSelector, String)}
121     *
122     * @param childPattern {@link UiSelector} selector of the child pattern to match and return
123     * @param text String may be a partial match for the content-description of a child element.
124     * @param allowScrollSearch set to true if scrolling is allowed
125     * @return {@link UiObject} pointing at and instance of <code>childPattern</code>
126     * @throws UiObjectNotFoundException
127     * @since API Level 16
128     */
129    public UiObject getChildByDescription(UiSelector childPattern, String text,
130            boolean allowScrollSearch) throws UiObjectNotFoundException {
131        Tracer.trace(childPattern, text, allowScrollSearch);
132        if (text != null) {
133            if (allowScrollSearch) {
134                scrollIntoView(new UiSelector().descriptionContains(text));
135            }
136            return super.getChildByDescription(childPattern, text);
137        }
138        throw new UiObjectNotFoundException("for description= \"" + text + "\"");
139    }
140
141    /**
142     * Searches for child UI element within the constraints of this UiScrollable {@link UiSelector}
143     * selector. It looks for any child matching the <code>childPattern</code> argument and
144     * return the <code>instance</code> specified. The operation is performed only on the visible
145     * items and no scrolling is performed in this case.
146     *
147     * @param childPattern {@link UiSelector} selector of the child pattern to match and return
148     * @param instance int the desired matched instance of this <code>childPattern</code>
149     * @return {@link UiObject} pointing at and instance of <code>childPattern</code>
150     * @since API Level 16
151     */
152    @Override
153    public UiObject getChildByInstance(UiSelector childPattern, int instance)
154            throws UiObjectNotFoundException {
155        Tracer.trace(childPattern, instance);
156        UiSelector patternSelector = UiSelector.patternBuilder(getSelector(),
157                UiSelector.patternBuilder(childPattern).instance(instance));
158        return new UiObject(patternSelector);
159    }
160
161    /**
162     * Searches for child UI element within the constraints of this UiScrollable {@link UiSelector}
163     * container. It looks for any child matching the <code>childPattern</code> argument that has
164     * a sub UI element anywhere within its sub hierarchy that has text attribute
165     * <code>text</code>. The returned UiObject will point at the <code>childPattern</code>
166     * instance that matched the search and not at the text matched sub element</p>
167     * By default this operation will perform scroll search while attempting to find the UI
168     * element.
169     * See {@link #getChildByText(UiSelector, String, boolean)}
170     *
171     * @param childPattern {@link UiSelector} selector of the child pattern to match and return
172     * @param text String of the identifying child contents of of the <code>childPattern</code>
173     * @return {@link UiObject} pointing at and instance of <code>childPattern</code>
174     * @throws UiObjectNotFoundException
175     * @since API Level 16
176     */
177    @Override
178    public UiObject getChildByText(UiSelector childPattern, String text)
179            throws UiObjectNotFoundException {
180        Tracer.trace(childPattern, text);
181        return getChildByText(childPattern, text, true);
182    }
183
184    /**
185     * See {@link #getChildByText(UiSelector, String)}
186     *
187     * @param childPattern {@link UiSelector} selector of the child pattern to match and return
188     * @param text String of the identifying child contents of of the <code>childPattern</code>
189     * @param allowScrollSearch set to true if scrolling is allowed
190     * @return {@link UiObject} pointing at and instance of <code>childPattern</code>
191     * @throws UiObjectNotFoundException
192     * @since API Level 16
193     */
194    public UiObject getChildByText(UiSelector childPattern, String text, boolean allowScrollSearch)
195            throws UiObjectNotFoundException {
196        Tracer.trace(childPattern, text, allowScrollSearch);
197        if (text != null) {
198            if (allowScrollSearch) {
199                scrollIntoView(new UiSelector().text(text));
200            }
201            return super.getChildByText(childPattern, text);
202        }
203        throw new UiObjectNotFoundException("for text= \"" + text + "\"");
204    }
205
206    /**
207     * Performs a swipe Up on the UI element until the requested content-description
208     * is visible or until swipe attempts have been exhausted. See {@link #setMaxSearchSwipes(int)}
209     *
210     * @param text to look for anywhere within the contents of this scrollable.
211     * @return true if item us found else false
212     * @since API Level 16
213     */
214    public boolean scrollDescriptionIntoView(String text) throws UiObjectNotFoundException {
215        Tracer.trace(text);
216        return scrollIntoView(new UiSelector().description(text));
217    }
218
219    /**
220     * Perform a scroll search for a UI element matching the {@link UiSelector} selector argument.
221     * See {@link #scrollDescriptionIntoView(String)} and {@link #scrollTextIntoView(String)}.
222     *
223     * @param obj {@link UiObject}
224     * @return true if the item was found and now is in view else false
225     * @since API Level 16
226     */
227    public boolean scrollIntoView(UiObject obj) throws UiObjectNotFoundException {
228        Tracer.trace(obj.getSelector());
229        return scrollIntoView(obj.getSelector());
230    }
231
232    /**
233     * Perform a scroll search for a UI element matching the {@link UiSelector} selector argument.
234     * See {@link #scrollDescriptionIntoView(String)} and {@link #scrollTextIntoView(String)}.
235     *
236     * @param selector {@link UiSelector} selector
237     * @return true if the item was found and now is in view else false
238     * @since API Level 16
239     */
240    public boolean scrollIntoView(UiSelector selector) throws UiObjectNotFoundException {
241        Tracer.trace(selector);
242        // if we happen to be on top of the text we want then return here
243        if (exists(getSelector().childSelector(selector))) {
244            return (true);
245        } else {
246            // we will need to reset the search from the beginning to start search
247            scrollToBeginning(mMaxSearchSwipes);
248            if (exists(getSelector().childSelector(selector))) {
249                return (true);
250            }
251            for (int x = 0; x < mMaxSearchSwipes; x++) {
252                if(!scrollForward()) {
253                    return false;
254                }
255
256                if(exists(getSelector().childSelector(selector))) {
257                    return true;
258                }
259            }
260        }
261        return false;
262    }
263
264    /**
265     * Performs a swipe up on the UI element until the requested text is visible
266     * or until swipe attempts have been exhausted. See {@link #setMaxSearchSwipes(int)}
267     *
268     * @param text to look for
269     * @return true if item us found else false
270     * @since API Level 16
271     */
272    public boolean scrollTextIntoView(String text) throws UiObjectNotFoundException {
273        Tracer.trace(text);
274        return scrollIntoView(new UiSelector().text(text));
275    }
276
277    /**
278     * {@link #getChildByDescription(UiSelector, String)} and
279     * {@link #getChildByText(UiSelector, String)} use an arguments that specifies if scrolling is
280     * allowed while searching for the UI element.  The number of scrolls allowed to perform a
281     * search can be modified by this method.  The current value can be read by calling
282     * {@link #getMaxSearchSwipes()}
283     *
284     * @param swipes is the number of search swipes until abort
285     * @return reference to itself
286     * @since API Level 16
287     */
288    public UiScrollable setMaxSearchSwipes(int swipes) {
289        Tracer.trace(swipes);
290        mMaxSearchSwipes = swipes;
291        return this;
292    }
293
294    /**
295     * {@link #getChildByDescription(UiSelector, String)} and
296     * {@link #getChildByText(UiSelector, String)} use an arguments that specifies if scrolling is
297     * allowed while searching for the UI element.  The number of scrolls currently allowed to
298     * perform a search can be read by this method.
299     * See {@link #setMaxSearchSwipes(int)}
300     *
301     * @return max value of the number of swipes currently allowed during a scroll search
302     * @since API Level 16
303     */
304    public int getMaxSearchSwipes() {
305        Tracer.trace();
306        return mMaxSearchSwipes;
307    }
308
309    /**
310     * A convenience version of {@link UiScrollable#scrollForward(int)}, performs a fling
311     *
312     * @return true if scrolled and false if can't scroll anymore
313     * @since API Level 16
314     */
315    public boolean flingForward() throws UiObjectNotFoundException {
316        Tracer.trace();
317        return scrollForward(FLING_STEPS);
318    }
319
320    /**
321     * A convenience version of {@link UiScrollable#scrollForward(int)}, performs a regular scroll
322     *
323     * @return true if scrolled and false if can't scroll anymore
324     * @since API Level 16
325     */
326    public boolean scrollForward() throws UiObjectNotFoundException {
327        Tracer.trace();
328        return scrollForward(SCROLL_STEPS);
329    }
330
331    /**
332     * Perform a scroll forward. If this list is set to vertical (see {@link #setAsVerticalList()}
333     * default) then the swipes will be executed from the bottom to top. If this list is set
334     * to horizontal (see {@link #setAsHorizontalList()}) then the swipes will be executed from
335     * the right to left. Caution is required on devices configured with right to left languages
336     * like Arabic and Hebrew.
337     *
338     * @param steps use steps to control the speed, so that it may be a scroll, or fling
339     * @return true if scrolled and false if can't scroll anymore
340     * @since API Level 16
341     */
342    public boolean scrollForward(int steps) throws UiObjectNotFoundException {
343        Tracer.trace(steps);
344        Log.d(LOG_TAG, "scrollForward() on selector = " + getSelector());
345        AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
346        if(node == null) {
347            throw new UiObjectNotFoundException(getSelector().toString());
348        }
349        Rect rect = new Rect();
350        node.getBoundsInScreen(rect);
351
352        int downX = 0;
353        int downY = 0;
354        int upX = 0;
355        int upY = 0;
356
357        // scrolling is by default assumed vertically unless the object is explicitly
358        // set otherwise by setAsHorizontalContainer()
359        if(mIsVerticalList) {
360            int swipeAreaAdjust = (int)(rect.height() * getSwipeDeadZonePercentage());
361            // scroll vertically: swipe down -> up
362            downX = rect.centerX();
363            downY = rect.bottom - swipeAreaAdjust;
364            upX = rect.centerX();
365            upY = rect.top + swipeAreaAdjust;
366        } else {
367            int swipeAreaAdjust = (int)(rect.width() * getSwipeDeadZonePercentage());
368            // scroll horizontally: swipe right -> left
369            // TODO: Assuming device is not in right to left language
370            downX = rect.right - swipeAreaAdjust;
371            downY = rect.centerY();
372            upX = rect.left + swipeAreaAdjust;
373            upY = rect.centerY();
374        }
375        return getInteractionController().scrollSwipe(downX, downY, upX, upY, steps);
376    }
377
378    /**
379     * See {@link UiScrollable#scrollBackward(int)}
380     *
381     * @return true if scrolled and false if can't scroll anymore
382     * @since API Level 16
383     */
384    public boolean flingBackward() throws UiObjectNotFoundException {
385        Tracer.trace();
386        return scrollBackward(FLING_STEPS);
387    }
388
389    /**
390     * See {@link UiScrollable#scrollBackward(int)}
391     *
392     * @return true if scrolled and false if can't scroll anymore
393     * @since API Level 16
394     */
395    public boolean scrollBackward() throws UiObjectNotFoundException {
396        Tracer.trace();
397        return scrollBackward(SCROLL_STEPS);
398    }
399
400    /**
401     * Perform a scroll backward. If this list is set to vertical (see {@link #setAsVerticalList()}
402     * default) then the swipes will be executed from the top to bottom. If this list is set
403     * to horizontal (see {@link #setAsHorizontalList()}) then the swipes will be executed from
404     * the left to right. Caution is required on devices configured with right to left languages
405     * like Arabic and Hebrew.
406     *
407     * @param steps use steps to control the speed, so that it may be a scroll, or fling
408     * @return true if scrolled and false if can't scroll anymore
409     * @since API Level 16
410     */
411    public boolean scrollBackward(int steps) throws UiObjectNotFoundException {
412        Tracer.trace(steps);
413        Log.d(LOG_TAG, "scrollBackward() on selector = " + getSelector());
414        AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
415        if (node == null) {
416            throw new UiObjectNotFoundException(getSelector().toString());
417        }
418        Rect rect = new Rect();
419        node.getBoundsInScreen(rect);
420
421        int downX = 0;
422        int downY = 0;
423        int upX = 0;
424        int upY = 0;
425
426        // scrolling is by default assumed vertically unless the object is explicitly
427        // set otherwise by setAsHorizontalContainer()
428        if(mIsVerticalList) {
429            int swipeAreaAdjust = (int)(rect.height() * getSwipeDeadZonePercentage());
430            Log.d(LOG_TAG, "scrollToBegining() using vertical scroll");
431            // scroll vertically: swipe up -> down
432            downX = rect.centerX();
433            downY = rect.top + swipeAreaAdjust;
434            upX = rect.centerX();
435            upY = rect.bottom - swipeAreaAdjust;
436        } else {
437            int swipeAreaAdjust = (int)(rect.width() * getSwipeDeadZonePercentage());
438            Log.d(LOG_TAG, "scrollToBegining() using hotizontal scroll");
439            // scroll horizontally: swipe left -> right
440            // TODO: Assuming device is not in right to left language
441            downX = rect.left + swipeAreaAdjust;
442            downY = rect.centerY();
443            upX = rect.right - swipeAreaAdjust;
444            upY = rect.centerY();
445        }
446        return getInteractionController().scrollSwipe(downX, downY, upX, upY, steps);
447    }
448
449    /**
450     * Scrolls to the beginning of a scrollable UI element. The beginning could be the top most
451     * in case of vertical lists or the left most in case of horizontal lists. Caution is required
452     * on devices configured with right to left languages like Arabic and Hebrew.
453     *
454     * @param steps use steps to control the speed, so that it may be a scroll, or fling
455     * @return true on scrolled else false
456     * @since API Level 16
457     */
458    public boolean scrollToBeginning(int maxSwipes, int steps) throws UiObjectNotFoundException {
459        Tracer.trace(maxSwipes, steps);
460        Log.d(LOG_TAG, "scrollToBeginning() on selector = " + getSelector());
461        // protect against potential hanging and return after preset attempts
462        for(int x = 0; x < maxSwipes; x++) {
463            if(!scrollBackward(steps)) {
464                break;
465            }
466        }
467        return true;
468    }
469
470    /**
471     * See {@link UiScrollable#scrollToBeginning(int, int)}
472     *
473     * @param maxSwipes
474     * @return true on scrolled else false
475     * @since API Level 16
476     */
477    public boolean scrollToBeginning(int maxSwipes) throws UiObjectNotFoundException {
478        Tracer.trace(maxSwipes);
479        return scrollToBeginning(maxSwipes, SCROLL_STEPS);
480    }
481
482    /**
483     * See {@link UiScrollable#scrollToBeginning(int, int)}
484     *
485     * @param maxSwipes
486     * @return true on scrolled else false
487     * @since API Level 16
488     */
489    public boolean flingToBeginning(int maxSwipes) throws UiObjectNotFoundException {
490        Tracer.trace(maxSwipes);
491        return scrollToBeginning(maxSwipes, FLING_STEPS);
492    }
493
494    /**
495     * Scrolls to the end of a scrollable UI element. The end could be the bottom most
496     * in case of vertical controls or the right most for horizontal controls. Caution
497     * is required on devices configured with right to left languages like Arabic and Hebrew.
498     *
499     * @param steps use steps to control the speed, so that it may be a scroll, or fling
500     * @return true on scrolled else false
501     * @since API Level 16
502     */
503    public boolean scrollToEnd(int maxSwipes, int steps) throws UiObjectNotFoundException {
504        Tracer.trace(maxSwipes, steps);
505        // protect against potential hanging and return after preset attempts
506        for(int x = 0; x < maxSwipes; x++) {
507            if(!scrollForward(steps)) {
508                break;
509            }
510        }
511        return true;
512    }
513
514    /**
515     * See {@link UiScrollable#scrollToEnd(int, int)}
516     *
517     * @param maxSwipes
518     * @return true on scrolled else false
519     * @since API Level 16
520     */
521    public boolean scrollToEnd(int maxSwipes) throws UiObjectNotFoundException {
522        Tracer.trace(maxSwipes);
523        return scrollToEnd(maxSwipes, SCROLL_STEPS);
524    }
525
526    /**
527     * See {@link UiScrollable#scrollToEnd(int, int)}
528     *
529     * @param maxSwipes
530     * @return true on scrolled else false
531     * @since API Level 16
532     */
533    public boolean flingToEnd(int maxSwipes) throws UiObjectNotFoundException {
534        Tracer.trace(maxSwipes);
535        return scrollToEnd(maxSwipes, FLING_STEPS);
536    }
537
538    /**
539     * Returns the percentage of a widget's size that's considered as no touch zone when swiping
540     *
541     * Dead zones are set as percentage of a widget's total width or height where
542     * swipe start point cannot start or swipe end point cannot end. It is like a margin
543     * around the actual dimensions of the widget. Swipes will always be start and
544     * end inside this margin.
545     *
546     * This is important when the widget being swiped may not respond to the swipe if
547     * started at a point too near to the edge. The default is 10% from either edge.
548     *
549     * @return a value between 0 and 1
550     * @since API Level 16
551     */
552    public double getSwipeDeadZonePercentage() {
553        Tracer.trace();
554        return mSwipeDeadZonePercentage;
555    }
556
557    /**
558     * Sets the percentage of a widget's size that's considered as no touch zone when swiping
559     *
560     * Dead zones are set as percentage of a widget's total width or height where
561     * swipe start point cannot start or swipe end point cannot end. It is like a margin
562     * around the actual dimensions of the widget. Swipes will always start and
563     * end inside this margin.
564     *
565     * This is important when the widget being swiped may not respond to the swipe if
566     * started at a point too near to the edge. The default is 10% from either edge
567     *
568     * @param swipeDeadZonePercentage is a value between 0 and 1
569     * @return reference to itself
570     * @since API Level 16
571     */
572    public UiScrollable setSwipeDeadZonePercentage(double swipeDeadZonePercentage) {
573        Tracer.trace(swipeDeadZonePercentage);
574        mSwipeDeadZonePercentage = swipeDeadZonePercentage;
575        return this;
576    }
577}
578