/* * 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 childPattern 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 childPattern 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 childPattern 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 childPattern 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 childPattern 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; } }