/*
* Copyright (C) 2012 The Android Open Source Project
*
* 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 com.android.uiautomator.core;
import android.graphics.Rect;
import android.util.Log;
import android.view.accessibility.AccessibilityNodeInfo;
/**
* UiScrollable is a {@link UiCollection} and provides support for searching
* for items in scrollable layout elements. This class can be used with
* horizontally or vertically scrollable controls.
* @since API Level 16
*/
public class UiScrollable extends UiCollection {
private static final String LOG_TAG = UiScrollable.class.getSimpleName();
// More steps slows the swipe and prevents contents from being flung too far
private static final int SCROLL_STEPS = 55;
private static final int FLING_STEPS = 5;
// Restrict a swipe's starting and ending points inside a 10% margin of the target
private static final double DEFAULT_SWIPE_DEADZONE_PCT = 0.1;
// Limits the number of swipes/scrolls performed during a search
private static int mMaxSearchSwipes = 30;
// Used in ScrollForward() and ScrollBackward() to determine swipe direction
private boolean mIsVerticalList = true;
private double mSwipeDeadZonePercentage = DEFAULT_SWIPE_DEADZONE_PCT;
/**
* Constructor.
*
* @param container a {@link UiSelector} selector to identify the scrollable
* layout element.
* @since API Level 16
*/
public UiScrollable(UiSelector container) {
// wrap the container selector with container so that QueryController can handle
// this type of enumeration search accordingly
super(container);
}
/**
* Set the direction of swipes to be vertical when performing scroll actions.
* @return reference to itself
* @since API Level 16
*/
public UiScrollable setAsVerticalList() {
Tracer.trace();
mIsVerticalList = true;
return this;
}
/**
* Set the direction of swipes to be horizontal when performing scroll actions.
* @return reference to itself
* @since API Level 16
*/
public UiScrollable setAsHorizontalList() {
Tracer.trace();
mIsVerticalList = false;
return this;
}
/**
* Used privately when performing swipe searches to decide if an element has become
* visible or not.
*
* @param selector
* @return true if found else false
* @since API Level 16
*/
protected boolean exists(UiSelector selector) {
if(getQueryController().findAccessibilityNodeInfo(selector) != null) {
return true;
}
return false;
}
/**
* Searches for a child element in the present scrollable container.
* The search first looks for a child element that matches the selector
* you provided, then looks for the content-description in its children elements.
* If both search conditions are fulfilled, the method returns a {@ link UiObject}
* representing the element matching the selector (not the child element in its
* subhierarchy containing the content-description). By default, this method performs a
* scroll search.
* See {@link #getChildByDescription(UiSelector, String, boolean)}
*
* @param childPattern {@link UiSelector} for a child in a scollable layout element
* @param text Content-description to find in the children of
* the <code>childPattern</code> match
* @return {@link UiObject} representing the child element that matches the search conditions
* @throws UiObjectNotFoundException
* @since API Level 16
*/
@Override
public UiObject getChildByDescription(UiSelector childPattern, String text)
throws UiObjectNotFoundException {
Tracer.trace(childPattern, text);
return getChildByDescription(childPattern, text, true);
}
/**
* Searches for a child element in the present scrollable container.
* The search first looks for a child element that matches the selector
* you provided, then looks for the content-description in its children elements.
* If both search conditions are fulfilled, the method returns a {@ link UiObject}
* representing the element matching the selector (not the child element in its
* subhierarchy containing the content-description).
*
* @param childPattern {@link UiSelector} for a child in a scollable layout element
* @param text Content-description to find in the children of
* the <code>childPattern</code> match (may be a partial match)
* @param allowScrollSearch set to true if scrolling is allowed
* @return {@link UiObject} representing the child element that matches the search conditions
* @throws UiObjectNotFoundException
* @since API Level 16
*/
public UiObject getChildByDescription(UiSelector childPattern, String text,
boolean allowScrollSearch) throws UiObjectNotFoundException {
Tracer.trace(childPattern, text, allowScrollSearch);
if (text != null) {
if (allowScrollSearch) {
scrollIntoView(new UiSelector().descriptionContains(text));
}
return super.getChildByDescription(childPattern, text);
}
throw new UiObjectNotFoundException("for description= \"" + text + "\"");
}
/**
* Searches for a child element in the present scrollable container that
* matches the selector you provided. The search is performed without
* scrolling and only on visible elements.
*
* @param childPattern {@link UiSelector} for a child in a scollable layout element
* @param instance int number representing the occurance of
* a <code>childPattern</code> match
* @return {@link UiObject} representing the child element that matches the search conditions
* @since API Level 16
*/
@Override
public UiObject getChildByInstance(UiSelector childPattern, int instance)
throws UiObjectNotFoundException {
Tracer.trace(childPattern, instance);
UiSelector patternSelector = UiSelector.patternBuilder(getSelector(),
UiSelector.patternBuilder(childPattern).instance(instance));
return new UiObject(patternSelector);
}
/**
* Searches for a child element in the present scrollable
* container. The search first looks for a child element that matches the
* selector you provided, then looks for the text in its children elements.
* If both search conditions are fulfilled, the method returns a {@ link UiObject}
* representing the element matching the selector (not the child element in its
* subhierarchy containing the text). By default, this method performs a
* scroll search.
* See {@link #getChildByText(UiSelector, String, boolean)}
*
* @param childPattern {@link UiSelector} selector for a child in a scrollable layout element
* @param text String to find in the children of the <code>childPattern</code> match
* @return {@link UiObject} representing the child element that matches the search conditions
* @throws UiObjectNotFoundException
* @since API Level 16
*/
@Override
public UiObject getChildByText(UiSelector childPattern, String text)
throws UiObjectNotFoundException {
Tracer.trace(childPattern, text);
return getChildByText(childPattern, text, true);
}
/**
* Searches for a child element in the present scrollable container. The
* search first looks for a child element that matches the
* selector you provided, then looks for the text in its children elements.
* If both search conditions are fulfilled, the method returns a {@ link UiObject}
* representing the element matching the selector (not the child element in its
* subhierarchy containing the text).
*
* @param childPattern {@link UiSelector} selector for a child in a scrollable layout element
* @param text String to find in the children of the <code>childPattern</code> match
* @param allowScrollSearch set to true if scrolling is allowed
* @return {@link UiObject} representing the child element that matches the search conditions
* @throws UiObjectNotFoundException
* @since API Level 16
*/
public UiObject getChildByText(UiSelector childPattern, String text, boolean allowScrollSearch)
throws UiObjectNotFoundException {
Tracer.trace(childPattern, text, allowScrollSearch);
if (text != null) {
if (allowScrollSearch) {
scrollIntoView(new UiSelector().text(text));
}
return super.getChildByText(childPattern, text);
}
throw new UiObjectNotFoundException("for text= \"" + text + "\"");
}
/**
* Performs a forward scroll action on the scrollable layout element until
* the content-description is found, or until swipe attempts have been exhausted.
* See {@link #setMaxSearchSwipes(int)}
*
* @param text content-description to find within the contents of this scrollable layout element.
* @return true if item is found; else, false
* @since API Level 16
*/
public boolean scrollDescriptionIntoView(String text) throws UiObjectNotFoundException {
Tracer.trace(text);
return scrollIntoView(new UiSelector().description(text));
}
/**
* Perform a forward scroll action to move through the scrollable layout element until
* a visible item that matches the {@link UiObject} is found.
*
* @param obj {@link UiObject}
* @return true if the item was found and now is in view else false
* @since API Level 16
*/
public boolean scrollIntoView(UiObject obj) throws UiObjectNotFoundException {
Tracer.trace(obj.getSelector());
return scrollIntoView(obj.getSelector());
}
/**
* Perform a scroll forward action to move through the scrollable layout
* element until a visible item that matches the selector is found.
*
* See {@link #scrollDescriptionIntoView(String)} and {@link #scrollTextIntoView(String)}.
*
* @param selector {@link UiSelector} selector
* @return true if the item was found and now is in view; else, false
* @since API Level 16
*/
public boolean scrollIntoView(UiSelector selector) throws UiObjectNotFoundException {
Tracer.trace(selector);
// if we happen to be on top of the text we want then return here
UiSelector childSelector = getSelector().childSelector(selector);
if (exists(childSelector)) {
return (true);
} else {
// we will need to reset the search from the beginning to start search
scrollToBeginning(mMaxSearchSwipes);
if (exists(childSelector)) {
return (true);
}
for (int x = 0; x < mMaxSearchSwipes; x++) {
boolean scrolled = scrollForward();
if(exists(childSelector)) {
return true;
}
if (!scrolled) {
return false;
}
}
}
return false;
}
/**
* Scrolls forward until the UiObject is fully visible in the scrollable container.
* Use this method to make sure that the child item's edges are not offscreen.
*
* @param childObject {@link UiObject} representing the child element
* @return true if the child element is already fully visible, or
* if the method scrolled successfully until the child became fully visible;
* otherwise, false if the attempt to scroll failed.
* @throws UiObjectNotFoundException
* @hide
*/
public boolean ensureFullyVisible(UiObject childObject) throws UiObjectNotFoundException {
Rect actual = childObject.getBounds();
Rect visible = childObject.getVisibleBounds();
if (visible.width() * visible.height() == actual.width() * actual.height()) {
// area match, item fully visible
return true;
}
boolean shouldSwipeForward = false;
if (mIsVerticalList) {
// if list is vertical, matching top edge implies obscured bottom edge
// so we need to scroll list forward
shouldSwipeForward = actual.top == visible.top;
} else {
// if list is horizontal, matching left edge implies obscured right edge,
// so we need to scroll list forward
shouldSwipeForward = actual.left == visible.left;
}
if (mIsVerticalList) {
if (shouldSwipeForward) {
return swipeUp(10);
} else {
return swipeDown(10);
}
} else {
if (shouldSwipeForward) {
return swipeLeft(10);
} else {
return swipeRight(10);
}
}
}
/**
* Performs a forward scroll action on the scrollable layout element until
* the text you provided is visible, or until swipe attempts have been exhausted.
* See {@link #setMaxSearchSwipes(int)}
*
* @param text test to look for
* @return true if item is found; else, false
* @since API Level 16
*/
public boolean scrollTextIntoView(String text) throws UiObjectNotFoundException {
Tracer.trace(text);
return scrollIntoView(new UiSelector().text(text));
}
/**
* Sets the maximum number of scrolls allowed when performing a
* scroll action in search of a child element.
* See {@link #getChildByDescription(UiSelector, String)} and
* {@link #getChildByText(UiSelector, String)}.
*
* @param swipes the number of search swipes to perform until giving up
* @return reference to itself
* @since API Level 16
*/
public UiScrollable setMaxSearchSwipes(int swipes) {
Tracer.trace(swipes);
mMaxSearchSwipes = swipes;
return this;
}
/**
* Gets the maximum number of scrolls allowed when performing a
* scroll action in search of a child element.
* See {@link #getChildByDescription(UiSelector, String)} and
* {@link #getChildByText(UiSelector, String)}.
*
* @return max the number of search swipes to perform until giving up
* @since API Level 16
*/
public int getMaxSearchSwipes() {
Tracer.trace();
return mMaxSearchSwipes;
}
/**
* Performs a forward fling with the default number of fling steps (5).
* If the swipe direction is set to vertical, then the swipes will be
* performed from bottom to top. If the swipe
* direction is set to horizontal, then the swipes will be performed from
* right to left. Make sure to take into account devices configured with
* right-to-left languages like Arabic and Hebrew.
*
* @return true if scrolled, false if can't scroll anymore
* @since API Level 16
*/
public boolean flingForward() throws UiObjectNotFoundException {
Tracer.trace();
return scrollForward(FLING_STEPS);
}
/**
* Performs a forward scroll with the default number of scroll steps (55).
* If the swipe direction is set to vertical,
* then the swipes will be performed from bottom to top. If the swipe
* direction is set to horizontal, then the swipes will be performed from
* right to left. Make sure to take into account devices configured with
* right-to-left languages like Arabic and Hebrew.
*
* @return true if scrolled, false if can't scroll anymore
* @since API Level 16
*/
public boolean scrollForward() throws UiObjectNotFoundException {
Tracer.trace();
return scrollForward(SCROLL_STEPS);
}
/**
* Performs a forward scroll. If the swipe direction is set to vertical,
* then the swipes will be performed from bottom to top. If the swipe
* direction is set to horizontal, then the swipes will be performed from
* right to left. Make sure to take into account devices configured with
* right-to-left languages like Arabic and Hebrew.
*
* @param steps number of steps. Use this to control the speed of the scroll action
* @return true if scrolled, false if can't scroll anymore
* @since API Level 16
*/
public boolean scrollForward(int steps) throws UiObjectNotFoundException {
Tracer.trace(steps);
Log.d(LOG_TAG, "scrollForward() on selector = " + getSelector());
AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
if(node == null) {
throw new UiObjectNotFoundException(getSelector().toString());
}
Rect rect = new Rect();
node.getBoundsInScreen(rect);
int downX = 0;
int downY = 0;
int upX = 0;
int upY = 0;
// scrolling is by default assumed vertically unless the object is explicitly
// set otherwise by setAsHorizontalContainer()
if(mIsVerticalList) {
int swipeAreaAdjust = (int)(rect.height() * getSwipeDeadZonePercentage());
// scroll vertically: swipe down -> up
downX = rect.centerX();
downY = rect.bottom - swipeAreaAdjust;
upX = rect.centerX();
upY = rect.top + swipeAreaAdjust;
} else {
int swipeAreaAdjust = (int)(rect.width() * getSwipeDeadZonePercentage());
// scroll horizontally: swipe right -> left
// TODO: Assuming device is not in right to left language
downX = rect.right - swipeAreaAdjust;
downY = rect.centerY();
upX = rect.left + swipeAreaAdjust;
upY = rect.centerY();
}
return getInteractionController().scrollSwipe(downX, downY, upX, upY, steps);
}
/**
* Performs a backwards fling action with the default number of fling
* steps (5). If the swipe direction is set to vertical,
* then the swipe will be performed from top to bottom. If the swipe
* direction is set to horizontal, then the swipes will be performed from
* left to right. Make sure to take into account devices configured with
* right-to-left languages like Arabic and Hebrew.
*
* @return true if scrolled, and false if can't scroll anymore
* @since API Level 16
*/
public boolean flingBackward() throws UiObjectNotFoundException {
Tracer.trace();
return scrollBackward(FLING_STEPS);
}
/**
* Performs a backward scroll with the default number of scroll steps (55).
* If the swipe direction is set to vertical,
* then the swipes will be performed from top to bottom. If the swipe
* direction is set to horizontal, then the swipes will be performed from
* left to right. Make sure to take into account devices configured with
* right-to-left languages like Arabic and Hebrew.
*
* @return true if scrolled, and false if can't scroll anymore
* @since API Level 16
*/
public boolean scrollBackward() throws UiObjectNotFoundException {
Tracer.trace();
return scrollBackward(SCROLL_STEPS);
}
/**
* Performs a backward scroll. If the swipe direction is set to vertical,
* then the swipes will be performed from top to bottom. If the swipe
* direction is set to horizontal, then the swipes will be performed from
* left to right. Make sure to take into account devices configured with
* right-to-left languages like Arabic and Hebrew.
*
* @param steps number of steps. Use this to control the speed of the scroll action.
* @return true if scrolled, false if can't scroll anymore
* @since API Level 16
*/
public boolean scrollBackward(int steps) throws UiObjectNotFoundException {
Tracer.trace(steps);
Log.d(LOG_TAG, "scrollBackward() on selector = " + getSelector());
AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
if (node == null) {
throw new UiObjectNotFoundException(getSelector().toString());
}
Rect rect = new Rect();
node.getBoundsInScreen(rect);
int downX = 0;
int downY = 0;
int upX = 0;
int upY = 0;
// scrolling is by default assumed vertically unless the object is explicitly
// set otherwise by setAsHorizontalContainer()
if(mIsVerticalList) {
int swipeAreaAdjust = (int)(rect.height() * getSwipeDeadZonePercentage());
Log.d(LOG_TAG, "scrollToBegining() using vertical scroll");
// scroll vertically: swipe up -> down
downX = rect.centerX();
downY = rect.top + swipeAreaAdjust;
upX = rect.centerX();
upY = rect.bottom - swipeAreaAdjust;
} else {
int swipeAreaAdjust = (int)(rect.width() * getSwipeDeadZonePercentage());
Log.d(LOG_TAG, "scrollToBegining() using hotizontal scroll");
// scroll horizontally: swipe left -> right
// TODO: Assuming device is not in right to left language
downX = rect.left + swipeAreaAdjust;
downY = rect.centerY();
upX = rect.right - swipeAreaAdjust;
upY = rect.centerY();
}
return getInteractionController().scrollSwipe(downX, downY, upX, upY, steps);
}
/**
* Scrolls to the beginning of a scrollable layout element. The beginning
* can be at the top-most edge in the case of vertical controls, or the
* left-most edge for horizontal controls. Make sure to take into account
* devices configured with right-to-left languages like Arabic and Hebrew.
*
* @param steps use steps to control the speed, so that it may be a scroll, or fling
* @return true on scrolled else false
* @since API Level 16
*/
public boolean scrollToBeginning(int maxSwipes, int steps) throws UiObjectNotFoundException {
Tracer.trace(maxSwipes, steps);
Log.d(LOG_TAG, "scrollToBeginning() on selector = " + getSelector());
// protect against potential hanging and return after preset attempts
for(int x = 0; x < maxSwipes; x++) {
if(!scrollBackward(steps)) {
break;
}
}
return true;
}
/**
* Scrolls to the beginning of a scrollable layout element. The beginning
* can be at the top-most edge in the case of vertical controls, or the
* left-most edge for horizontal controls. Make sure to take into account
* devices configured with right-to-left languages like Arabic and Hebrew.
*
* @param maxSwipes
* @return true on scrolled else false
* @since API Level 16
*/
public boolean scrollToBeginning(int maxSwipes) throws UiObjectNotFoundException {
Tracer.trace(maxSwipes);
return scrollToBeginning(maxSwipes, SCROLL_STEPS);
}
/**
* Performs a fling gesture to reach the beginning of a scrollable layout element.
* The beginning can be at the top-most edge in the case of vertical controls, or
* the left-most edge for horizontal controls. Make sure to take into
* account devices configured with right-to-left languages like Arabic and Hebrew.
*
* @param maxSwipes
* @return true on scrolled else false
* @since API Level 16
*/
public boolean flingToBeginning(int maxSwipes) throws UiObjectNotFoundException {
Tracer.trace(maxSwipes);
return scrollToBeginning(maxSwipes, FLING_STEPS);
}
/**
* Scrolls to the end of a scrollable layout element. The end can be at the
* bottom-most edge in the case of vertical controls, or the right-most edge for
* horizontal controls. Make sure to take into account devices configured with
* right-to-left languages like Arabic and Hebrew.
*
* @param steps use steps to control the speed, so that it may be a scroll, or fling
* @return true on scrolled else false
* @since API Level 16
*/
public boolean scrollToEnd(int maxSwipes, int steps) throws UiObjectNotFoundException {
Tracer.trace(maxSwipes, steps);
// protect against potential hanging and return after preset attempts
for(int x = 0; x < maxSwipes; x++) {
if(!scrollForward(steps)) {
break;
}
}
return true;
}
/**
* Scrolls to the end of a scrollable layout element. The end can be at the
* bottom-most edge in the case of vertical controls, or the right-most edge for
* horizontal controls. Make sure to take into account devices configured with
* right-to-left languages like Arabic and Hebrew.
*
* @param maxSwipes
* @return true on scrolled, else false
* @since API Level 16
*/
public boolean scrollToEnd(int maxSwipes) throws UiObjectNotFoundException {
Tracer.trace(maxSwipes);
return scrollToEnd(maxSwipes, SCROLL_STEPS);
}
/**
* Performs a fling gesture to reach the end of a scrollable layout element.
* The end can be at the bottom-most edge in the case of vertical controls, or
* the right-most edge for horizontal controls. Make sure to take into
* account devices configured with right-to-left languages like Arabic and Hebrew.
*
* @param maxSwipes
* @return true on scrolled, else false
* @since API Level 16
*/
public boolean flingToEnd(int maxSwipes) throws UiObjectNotFoundException {
Tracer.trace(maxSwipes);
return scrollToEnd(maxSwipes, FLING_STEPS);
}
/**
* Returns the percentage of a widget's size that's considered as a no-touch
* zone when swiping. The no-touch zone is set as a percentage of a widget's total
* width or height, denoting a margin around the swipable area of the widget.
* Swipes must start and end inside this margin. This is important when the
* widget being swiped may not respond to the swipe if started at a point
* too near to the edge. The default is 10% from either edge.
*
* @return a value between 0 and 1
* @since API Level 16
*/
public double getSwipeDeadZonePercentage() {
Tracer.trace();
return mSwipeDeadZonePercentage;
}
/**
* Sets the percentage of a widget's size that's considered as no-touch
* zone when swiping.
* The no-touch zone is set as percentage of a widget's total width or height,
* denoting a margin around the swipable area of the widget. Swipes must
* always start and end inside this margin. This is important when the
* widget being swiped may not respond to the swipe if started at a point
* too near to the edge. The default is 10% from either edge.
*
* @param swipeDeadZonePercentage is a value between 0 and 1
* @return reference to itself
* @since API Level 16
*/
public UiScrollable setSwipeDeadZonePercentage(double swipeDeadZonePercentage) {
Tracer.trace(swipeDeadZonePercentage);
mSwipeDeadZonePercentage = swipeDeadZonePercentage;
return this;
}
}