summaryrefslogtreecommitdiff
path: root/library/core-src/com/android/uiautomator/core/UiScrollable.java
diff options
context:
space:
mode:
Diffstat (limited to 'library/core-src/com/android/uiautomator/core/UiScrollable.java')
-rw-r--r--library/core-src/com/android/uiautomator/core/UiScrollable.java577
1 files changed, 577 insertions, 0 deletions
diff --git a/library/core-src/com/android/uiautomator/core/UiScrollable.java b/library/core-src/com/android/uiautomator/core/UiScrollable.java
new file mode 100644
index 0000000..c128ac2
--- /dev/null
+++ b/library/core-src/com/android/uiautomator/core/UiScrollable.java
@@ -0,0 +1,577 @@
+/*
+ * 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 a
+ * scrollable UI elements. Used with horizontally or vertically scrollable UI.
+ * @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;
+
+ /**
+ * UiScrollable is a {@link UiCollection} and as such requires a {@link UiSelector} to
+ * identify the container UI element of the scrollable collection. Further operations on
+ * the items in the container will require specifying UiSelector as an item selector.
+ *
+ * @param container a {@link UiSelector} selector
+ * @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 when performing scroll search
+ * @return reference to itself
+ * @since API Level 16
+ */
+ public UiScrollable setAsVerticalList() {
+ Tracer.trace();
+ mIsVerticalList = true;
+ return this;
+ }
+
+ /**
+ * Set the direction of swipes when performing scroll search
+ * @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 child UI element within the constraints of this UiScrollable {@link UiSelector}
+ * container. It looks for any child matching the <code>childPattern</code> argument within its
+ * hierarchy with a matching content-description text. The returned UiObject will represent the
+ * UI element matching the <code>childPattern</code> and not the sub 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
+ * @since API Level 16
+ */
+ @Override
+ public UiObject getChildByDescription(UiSelector childPattern, String text)
+ throws UiObjectNotFoundException {
+ Tracer.trace(childPattern, text);
+ return getChildByDescription(childPattern, text, true);
+ }
+
+ /**
+ * See {@link #getChildByDescription(UiSelector, String)}
+ *
+ * @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
+ * @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 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>
+ * @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 child UI element within the constraints of this UiScrollable {@link UiSelector}
+ * container. It looks for any child matching the <code>childPattern</code> argument that has
+ * a sub 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 text matched sub element</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
+ * @since API Level 16
+ */
+ @Override
+ public UiObject getChildByText(UiSelector childPattern, String text)
+ throws UiObjectNotFoundException {
+ Tracer.trace(childPattern, text);
+ return getChildByText(childPattern, text, true);
+ }
+
+ /**
+ * See {@link #getChildByText(UiSelector, String)}
+ *
+ * @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
+ * @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 swipe Up on the UI element until the requested content-description
+ * is visible 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
+ * @since API Level 16
+ */
+ public boolean scrollDescriptionIntoView(String text) throws UiObjectNotFoundException {
+ Tracer.trace(text);
+ return scrollIntoView(new UiSelector().description(text));
+ }
+
+ /**
+ * Perform a scroll search for a UI element matching the {@link UiSelector} selector argument.
+ * See {@link #scrollDescriptionIntoView(String)} and {@link #scrollTextIntoView(String)}.
+ *
+ * @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 search for a UI element matching the {@link UiSelector} selector argument.
+ * 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
+ 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 UI element until the requested text is visible
+ * or until swipe attempts have been exhausted. See {@link #setMaxSearchSwipes(int)}
+ *
+ * @param text to look for
+ * @return true if item us found else false
+ * @since API Level 16
+ */
+ public boolean scrollTextIntoView(String text) throws UiObjectNotFoundException {
+ Tracer.trace(text);
+ return scrollIntoView(new UiSelector().text(text));
+ }
+
+ /**
+ * {@link #getChildByDescription(UiSelector, String)} and
+ * {@link #getChildByText(UiSelector, String)} 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 is the number of search swipes until abort
+ * @return reference to itself
+ * @since API Level 16
+ */
+ public UiScrollable setMaxSearchSwipes(int swipes) {
+ Tracer.trace(swipes);
+ mMaxSearchSwipes = swipes;
+ return this;
+ }
+
+ /**
+ * {@link #getChildByDescription(UiSelector, String)} and
+ * {@link #getChildByText(UiSelector, String)} 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
+ * @since API Level 16
+ */
+ public int getMaxSearchSwipes() {
+ Tracer.trace();
+ return mMaxSearchSwipes;
+ }
+
+ /**
+ * A convenience version of {@link UiScrollable#scrollForward(int)}, performs a fling
+ *
+ * @return true if scrolled and false if can't scroll anymore
+ * @since API Level 16
+ */
+ public boolean flingForward() throws UiObjectNotFoundException {
+ Tracer.trace();
+ 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
+ * @since API Level 16
+ */
+ public boolean scrollForward() throws UiObjectNotFoundException {
+ Tracer.trace();
+ 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. Caution is required on 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 if scrolled and 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);
+ }
+
+ /**
+ * See {@link UiScrollable#scrollBackward(int)}
+ *
+ * @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);
+ }
+
+ /**
+ * See {@link UiScrollable#scrollBackward(int)}
+ *
+ * @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);
+ }
+
+ /**
+ * 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. Caution is required on 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 if scrolled and 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 UI element. The beginning could be the top most
+ * in case of vertical lists or the left most in case of horizontal lists. Caution is required
+ * on 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;
+ }
+
+ /**
+ * See {@link UiScrollable#scrollToBeginning(int, int)}
+ *
+ * @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);
+ }
+
+ /**
+ * See {@link UiScrollable#scrollToBeginning(int, int)}
+ *
+ * @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 UI element. The end could be the bottom most
+ * in case of vertical controls or the right most for horizontal controls. Caution
+ * is required on 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;
+ }
+
+ /**
+ * See {@link UiScrollable#scrollToEnd(int, int)}
+ *
+ * @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);
+ }
+
+ /**
+ * See {@link UiScrollable#scrollToEnd(int, int)}
+ *
+ * @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 no touch zone when swiping
+ *
+ * Dead zones are set as percentage of a widget's total width or height where
+ * swipe start point cannot start or swipe end point cannot end. It is like a margin
+ * around the actual dimensions of the widget. Swipes will always be 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
+ *
+ * Dead zones are set as percentage of a widget's total width or height where
+ * swipe start point cannot start or swipe end point cannot end. It is like a margin
+ * around the actual dimensions of the widget. Swipes will 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;
+ }
+}