blob: 4022c5af5752d943ecb98379347f1b69d810bb9a [file] [log] [blame]
/*
* 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} however this class provides additional functionality
* where the tests need to deal with scrollable contents or desire to enumerate lists of
* items. This calls can perform automatic searches within a scrollable container. Whether
* the content scrolls vertically or horizontally can be set by calling
* {@link #setAsVerticalList()} which is the default, or {@link #setAsHorizontalList()}.
*/
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
protected boolean mIsVerticalList = true;
private double mSwipeDeadZonePercentage = DEFAULT_SWIPE_DEADZONE_PCT;
/**
* UiScrollable is a {@link UiCollection} and as such requires a {@link UiSelector} to identify
* the UI element it represents. In the case of UiScrollable, the selector specified is
* considered a container where further calls to enumerate or find children will be performed
* in.
* @param container a {@link UiSelector} selector
*/
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 when performing scroll search
*/
public void setAsVerticalList() {
mIsVerticalList = true;
}
/**
* Set the direction of swipes when performing scroll search
*/
public void setAsHorizontalList() {
mIsVerticalList = false;
}
/**
* Used privately when performing swipe searches to decide if an element has become
* visible or not.
* @param selector
* @return true if found else false
*/
protected boolean exists(UiSelector selector) {
if(getQueryController().findAccessibilityNodeInfo(selector) != null) {
return true;
}
return false;
}
/**
* Searches for child UI element within the constraints of this UiScrollable {@link UiSelector}
* selector. It looks for any child matching the <code>childPattern</code> argument that has
* a child UI element anywhere within its sub hierarchy that has content-description text.
* The returned UiObject will point at the <code>childPattern</code> instance that matched the
* search and not at the identifying child element that matched the content description.</p>
* By default this operation will perform scroll search while attempting to find the
* UI element.
* See {@link #getChildByDescription(UiSelector, String, boolean)}
* @param childPattern {@link UiSelector} selector of the child pattern to match and return
* @param text String of the identifying child contents of of the <code>childPattern</code>
* @return {@link UiObject} pointing at and instance of <code>childPattern</code>
* @throws UiObjectNotFoundException
*/
@Override
public UiObject getChildByDescription(UiSelector childPattern, String text)
throws UiObjectNotFoundException {
return getChildByDescription(childPattern, text, true);
}
/**
* Searches for child UI element within the constraints of this UiScrollable {@link UiSelector}
* selector. It looks for any child matching the <code>childPattern</code> argument that has
* a child UI element anywhere within its sub hierarchy that has content-description text.
* The returned UiObject will point at the <code>childPattern</code> instance that matched the
* search and not at the identifying child element that matched the content description.
* @param childPattern {@link UiSelector} selector of the child pattern to match and return
* @param text String may be a partial match for the content-description of a child element.
* @param allowScrollSearch set to true if scrolling is allowed
* @return {@link UiObject} pointing at and instance of <code>childPattern</code>
* @throws UiObjectNotFoundException
*/
public UiObject getChildByDescription(UiSelector childPattern, String text,
boolean allowScrollSearch) throws UiObjectNotFoundException {
if (text != null) {
if (allowScrollSearch) {
scrollIntoView(new UiSelector().descriptionContains(text));
}
return super.getChildByDescription(childPattern, text);
}
throw new UiObjectNotFoundException("for description= \"" + text + "\"");
}
/**
* Searches for child UI element within the constraints of this UiScrollable {@link UiSelector}
* selector. It looks for any child matching the <code>childPattern</code> argument and
* return the <code>instance</code> specified. The operation is performed only on the visible
* items and no scrolling is performed in this case.
* @param childPattern {@link UiSelector} selector of the child pattern to match and return
* @param instance int the desired matched instance of this <code>childPattern</code>
* @return {@link UiObject} pointing at and instance of <code>childPattern</code>
*/
@Override
public UiObject getChildByInstance(UiSelector childPattern, int instance)
throws UiObjectNotFoundException {
UiSelector patternSelector = UiSelector.patternBuilder(getSelector(),
UiSelector.patternBuilder(childPattern).instance(instance));
return new UiObject(patternSelector);
}
/**
* Searches for child UI element within the constraints of this UiScrollable {@link UiSelector}
* selector. It looks for any child matching the <code>childPattern</code> argument that has
* a child UI element anywhere within its sub hierarchy that has text attribute =
* <code>text</code>. The returned UiObject will point at the <code>childPattern</code>
* instance that matched the search and not at the identifying child element that matched the
* text attribute.</p>
* By default this operation will perform scroll search while attempting to find the UI
* element.
* See {@link #getChildByText(UiSelector, String, boolean)}
* @param childPattern {@link UiSelector} selector of the child pattern to match and return
* @param text String of the identifying child contents of of the <code>childPattern</code>
* @return {@link UiObject} pointing at and instance of <code>childPattern</code>
* @throws UiObjectNotFoundException
*/
@Override
public UiObject getChildByText(UiSelector childPattern, String text)
throws UiObjectNotFoundException {
return getChildByText(childPattern, text, true);
}
/**
* Searches for child UI element within the constraints of this UiScrollable {@link UiSelector}
* selector. It looks for any child matching the <code>childPattern</code> argument that has
* a child UI element anywhere within its sub hierarchy that has the text attribute =
* <code>text</code>.
* The returned UiObject will point at the <code>childPattern</code> instance that matched the
* search and not at the identifying child element that matched the text attribute.
* @param childPattern {@link UiSelector} selector of the child pattern to match and return
* @param text String of the identifying child contents of of the <code>childPattern</code>
* @param allowScrollSearch set to true if scrolling is allowed
* @return {@link UiObject} pointing at and instance of <code>childPattern</code>
* @throws UiObjectNotFoundException
*/
public UiObject getChildByText(UiSelector childPattern, String text, boolean allowScrollSearch)
throws UiObjectNotFoundException {
if (text != null) {
if (allowScrollSearch) {
scrollIntoView(new UiSelector().text(text));
}
return super.getChildByText(childPattern, text);
}
throw new UiObjectNotFoundException("for text= \"" + text + "\"");
}
/**
* Performs a swipe Up on the associated UI element until the requested content-description
* is found or until swipe attempts have been exhausted. See {@link #setMaxSearchSwipes(int)}
* @param text to look for anywhere within the contents of this scrollable.
* @return true if item us found else false
*/
public boolean scrollDescriptionIntoView(String text) {
return scrollIntoView(new UiSelector().description(text));
}
/**
* Perform a scroll search for a UI element matching the {@link UiSelector} selector argument.
* Also 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
*/
public boolean scrollIntoView(UiSelector selector) {
// if we happen to be on top of the text we want then return here
if (exists(getSelector().childSelector(selector))) {
return (true);
} else {
// we will need to reset the search from the beginning to start search
scrollToBeginning(mMaxSearchSwipes);
if (exists(getSelector().childSelector(selector))) {
return (true);
}
for (int x = 0; x < mMaxSearchSwipes; x++) {
if(!scrollForward()) {
return false;
}
if(exists(getSelector().childSelector(selector))) {
return true;
}
}
}
return false;
}
/**
* Performs a swipe up on the associated display element until the requested text
* appears or until swipe attempts have been exhausted. See {@link #setMaxSearchSwipes(int)}
* @param text to look for
* @return true if item us found else false
*/
public boolean scrollTextIntoView(String text) {
return scrollIntoView(new UiSelector().text(text));
}
/**
* {@link #getChildByDescription(String, boolean)} and {@link #getChildByText(String, boolean)}
* use an arguments that specifies if scrolling is allowed while searching for the UI element.
* The number of scrolls allowed to perform a search can be modified by this method.
* The current value can be read by calling {@link #getMaxSearchSwipes()}
* @param swipes
*/
public void setMaxSearchSwipes(int swipes) {
mMaxSearchSwipes = swipes;
}
/**
* {@link #getChildByDescription(String, boolean)} and {@link #getChildByText(String, boolean)}
* use an arguments that specifies if scrolling is allowed while searching for the UI element.
* The number of scrolls currently allowed to perform a search can be read by this method.
* See {@link #setMaxSearchSwipes(int)}
* @return max value of the number of swipes currently allowed during a scroll search
*/
public int getMaxSearchSwipes() {
return mMaxSearchSwipes;
}
/**
* A convenience version of {@link UiScrollable#scrollForward(int)}, performs a fling
*
* @return true if scrolled and false if can't scroll anymore
*/
public boolean flingForward() {
return scrollForward(FLING_STEPS);
}
/**
* A convenience version of {@link UiScrollable#scrollForward(int)}, performs a regular scroll
*
* @return true if scrolled and false if can't scroll anymore
*/
public boolean scrollForward() {
return scrollForward(SCROLL_STEPS);
}
/**
* Perform a scroll forward. If this list is set to vertical (see {@link #setAsVerticalList()}
* default) then the swipes will be executed from the bottom to top. If this list is set
* to horizontal (see {@link #setAsHorizontalList()}) then the swipes will be executed from
* the right to left.
*
* @param steps use steps to control the speed, so that it may be a scroll, or fling
* @return true if scrolled and false if can't scroll anymore
*/
public boolean scrollForward(int steps) {
Log.d(LOG_TAG, "scrollForward() on selector = " + getSelector());
AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
if(node == null) {
// Object Not Found
return false;
}
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);
}
/**
* A convenience version of {@link UiScrollable#scrollBackward(int)}, performs a fling
*
* @return true if scrolled and false if can't scroll anymore
*/
public boolean flingBackward() {
return scrollBackward(FLING_STEPS);
}
/**
* A convenience version of {@link UiScrollable#scrollBackward(int)}, performs a regular scroll
*
* @return true if scrolled and false if can't scroll anymore
*/
public boolean scrollBackward() {
return scrollBackward(SCROLL_STEPS);
}
/**
* Perform a scroll backward. If this list is set to vertical (see {@link #setAsVerticalList()}
* default) then the swipes will be executed from the top to bottom. If this list is set
* to horizontal (see {@link #setAsHorizontalList()}) then the swipes will be executed from
* the left to right.
*
* @param steps use steps to control the speed, so that it may be a scroll, or fling
* @return true if scrolled and false if can't scroll anymore
*/
public boolean scrollBackward(int steps) {
Log.d(LOG_TAG, "scrollBackward() on selector = " + getSelector());
AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
if(node == null) {
// Object Not Found
return false;
}
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 UI element. The beginning could be the top most
* in case of vertical lists or the left most in case of horizontal lists.
*
* @param steps use steps to control the speed, so that it may be a scroll, or fling
* @return true on scrolled else false
*/
public boolean scrollToBeginning(int maxSwipes, int 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;
}
/**
* A convenience version of {@link UiScrollable#scrollToBeginning(int, int)} with regular scroll
*
* @param maxSwipes
* @return true on scrolled else false
*/
public boolean scrollToBeginning(int maxSwipes) {
return scrollToBeginning(maxSwipes, SCROLL_STEPS);
}
/**
* A convenience version of {@link UiScrollable#scrollToBeginning(int, int)} with fling
*
* @param maxSwipes
* @return true on scrolled else false
*/
public boolean flingToBeginning(int maxSwipes) {
return scrollToBeginning(maxSwipes, FLING_STEPS);
}
/**
* Scrolls to the end of a scrollable UI element. The end could be the bottom most
* in case of vertical controls or the right most for horizontal controls
*
* @param steps use steps to control the speed, so that it may be a scroll, or fling
* @return true on scrolled else false
*/
public boolean scrollToEnd(int maxSwipes, int steps) {
// protect against potential hanging and return after preset attempts
for(int x = 0; x < maxSwipes; x++) {
if(!scrollForward(steps)) {
break;
}
}
return true;
}
/**
* A convenience version of {@link UiScrollable#scrollToEnd(int, int)} with regular scroll
*
* @param maxSwipes
* @return true on scrolled else false
*/
public boolean scrollToEnd(int maxSwipes) {
return scrollToEnd(maxSwipes, SCROLL_STEPS);
}
/**
* A convenience version of {@link UiScrollable#scrollToEnd(int, int)} with fling
*
* @param maxSwipes
* @return true on scrolled else false
*/
public boolean flingToEnd(int maxSwipes) {
return scrollToEnd(maxSwipes, FLING_STEPS);
}
public double getSwipeDeadZonePercentage() {
return mSwipeDeadZonePercentage;
}
public void setSwipeDeadZonePercentage(double swipeDeadZonePercentage) {
mSwipeDeadZonePercentage = swipeDeadZonePercentage;
}
}