diff options
author | Qiwen Zhao <zhao@google.com> | 2013-09-12 11:04:37 -0700 |
---|---|---|
committer | Qiwen Zhao <zhao@google.com> | 2013-09-12 11:04:37 -0700 |
commit | e056d9d08eddc0d64ac637b2f07cb0e4d6693c32 (patch) | |
tree | b46997ded357ee2f9af5be093cf2103d8346dd11 | |
parent | 90a8e6f62720e7767c9246972c63a950b9dd1c63 (diff) | |
parent | 911b8f36c1bca4a185be59827c618ac84d83ad00 (diff) | |
download | uiautomator-e056d9d08eddc0d64ac637b2f07cb0e4d6693c32.tar.gz |
Merge uiautomator/ from platform/frameworks/testing to /
59 files changed, 10887 insertions, 0 deletions
diff --git a/Android.mk b/Android.mk new file mode 100644 index 0000000..5391305 --- /dev/null +++ b/Android.mk @@ -0,0 +1,25 @@ +# +# 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. +# + +# don't build uiautomator in unbundled env +ifndef TARGET_BUILD_APPS +include $(call all-subdir-makefiles) +else +ifneq ($(filter uiautomator,$(TARGET_BUILD_APPS)),) +# used by the platform apps build. +include $(call all-subdir-makefiles) +endif +endif diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/MODULE_LICENSE_APACHE2 diff --git a/api/16.txt b/api/16.txt new file mode 100644 index 0000000..f3b0eb7 --- /dev/null +++ b/api/16.txt @@ -0,0 +1,174 @@ +package com.android.uiautomator.core { + + public class UiCollection extends com.android.uiautomator.core.UiObject { + ctor public UiCollection(com.android.uiautomator.core.UiSelector); + method public com.android.uiautomator.core.UiObject getChildByDescription(com.android.uiautomator.core.UiSelector, java.lang.String) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public com.android.uiautomator.core.UiObject getChildByInstance(com.android.uiautomator.core.UiSelector, int) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public com.android.uiautomator.core.UiObject getChildByText(com.android.uiautomator.core.UiSelector, java.lang.String) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public int getChildCount(com.android.uiautomator.core.UiSelector); + } + + public class UiDevice { + method public void clearLastTraversedText(); + method public boolean click(int, int); + method public void dumpWindowHierarchy(java.lang.String); + method public void freezeRotation() throws android.os.RemoteException; + method public java.lang.String getCurrentActivityName(); + method public java.lang.String getCurrentPackageName(); + method public int getDisplayHeight(); + method public int getDisplayWidth(); + method public static com.android.uiautomator.core.UiDevice getInstance(); + method public java.lang.String getLastTraversedText(); + method public boolean hasAnyWatcherTriggered(); + method public boolean hasWatcherTriggered(java.lang.String); + method public boolean isScreenOn() throws android.os.RemoteException; + method public boolean pressBack(); + method public boolean pressDPadCenter(); + method public boolean pressDPadDown(); + method public boolean pressDPadLeft(); + method public boolean pressDPadRight(); + method public boolean pressDPadUp(); + method public boolean pressDelete(); + method public boolean pressEnter(); + method public boolean pressHome(); + method public boolean pressKeyCode(int); + method public boolean pressKeyCode(int, int); + method public boolean pressMenu(); + method public boolean pressRecentApps() throws android.os.RemoteException; + method public boolean pressSearch(); + method public void registerWatcher(java.lang.String, com.android.uiautomator.core.UiWatcher); + method public void removeWatcher(java.lang.String); + method public void resetWatcherTriggers(); + method public void runWatchers(); + method public void sleep() throws android.os.RemoteException; + method public boolean swipe(int, int, int, int, int); + method public boolean swipe(android.graphics.Point[], int); + method public void unfreezeRotation() throws android.os.RemoteException; + method public void waitForIdle(); + method public void waitForIdle(long); + method public boolean waitForWindowUpdate(java.lang.String, long); + method public void wakeUp() throws android.os.RemoteException; + } + + public class UiObject { + ctor public UiObject(com.android.uiautomator.core.UiSelector); + method public void clearTextField() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean click() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean clickAndWaitForNewWindow() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean clickAndWaitForNewWindow(long) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean clickBottomRight() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean clickTopLeft() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean exists(); + method protected android.view.accessibility.AccessibilityNodeInfo findAccessibilityNodeInfo(long); + method public android.graphics.Rect getBounds() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public com.android.uiautomator.core.UiObject getChild(com.android.uiautomator.core.UiSelector) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public int getChildCount() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public java.lang.String getContentDescription() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public com.android.uiautomator.core.UiObject getFromParent(com.android.uiautomator.core.UiSelector) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public java.lang.String getPackageName() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public final com.android.uiautomator.core.UiSelector getSelector(); + method public java.lang.String getText() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean isCheckable() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean isChecked() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean isClickable() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean isEnabled() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean isFocusable() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean isFocused() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean isLongClickable() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean isScrollable() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean isSelected() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean longClick() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean longClickBottomRight() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean longClickTopLeft() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean setText(java.lang.String) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean swipeDown(int) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean swipeLeft(int) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean swipeRight(int) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean swipeUp(int) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean waitForExists(long); + method public boolean waitUntilGone(long); + field protected static final int SWIPE_MARGIN_LIMIT = 5; // 0x5 + field protected static final long WAIT_FOR_SELECTOR_POLL = 1000L; // 0x3e8L + field protected static final long WAIT_FOR_SELECTOR_TIMEOUT = 10000L; // 0x2710L + field protected static final long WAIT_FOR_WINDOW_TMEOUT = 5500L; // 0x157cL + } + + public class UiObjectNotFoundException extends java.lang.Exception { + ctor public UiObjectNotFoundException(java.lang.String); + ctor public UiObjectNotFoundException(java.lang.String, java.lang.Throwable); + ctor public UiObjectNotFoundException(java.lang.Throwable); + } + + public class UiScrollable extends com.android.uiautomator.core.UiCollection { + ctor public UiScrollable(com.android.uiautomator.core.UiSelector); + method protected boolean exists(com.android.uiautomator.core.UiSelector); + method public boolean flingBackward(); + method public boolean flingForward(); + method public boolean flingToBeginning(int); + method public boolean flingToEnd(int); + method public com.android.uiautomator.core.UiObject getChildByDescription(com.android.uiautomator.core.UiSelector, java.lang.String, boolean) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public com.android.uiautomator.core.UiObject getChildByText(com.android.uiautomator.core.UiSelector, java.lang.String, boolean) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public int getMaxSearchSwipes(); + method public double getSwipeDeadZonePercentage(); + method public boolean scrollBackward(); + method public boolean scrollBackward(int); + method public boolean scrollDescriptionIntoView(java.lang.String); + method public boolean scrollForward(); + method public boolean scrollForward(int); + method public boolean scrollIntoView(com.android.uiautomator.core.UiSelector); + method public boolean scrollTextIntoView(java.lang.String); + method public boolean scrollToBeginning(int, int); + method public boolean scrollToBeginning(int); + method public boolean scrollToEnd(int, int); + method public boolean scrollToEnd(int); + method public void setAsHorizontalList(); + method public void setAsVerticalList(); + method public void setMaxSearchSwipes(int); + method public void setSwipeDeadZonePercentage(double); + } + + public class UiSelector { + ctor public UiSelector(); + method public com.android.uiautomator.core.UiSelector checked(boolean); + method public com.android.uiautomator.core.UiSelector childSelector(com.android.uiautomator.core.UiSelector); + method public com.android.uiautomator.core.UiSelector className(java.lang.String); + method public com.android.uiautomator.core.UiSelector clickable(boolean); + method public com.android.uiautomator.core.UiSelector description(java.lang.String); + method public com.android.uiautomator.core.UiSelector descriptionContains(java.lang.String); + method public com.android.uiautomator.core.UiSelector descriptionStartsWith(java.lang.String); + method public com.android.uiautomator.core.UiSelector enabled(boolean); + method public com.android.uiautomator.core.UiSelector focusable(boolean); + method public com.android.uiautomator.core.UiSelector focused(boolean); + method public com.android.uiautomator.core.UiSelector fromParent(com.android.uiautomator.core.UiSelector); + method public com.android.uiautomator.core.UiSelector index(int); + method public com.android.uiautomator.core.UiSelector instance(int); + method public com.android.uiautomator.core.UiSelector packageName(java.lang.String); + method public com.android.uiautomator.core.UiSelector scrollable(boolean); + method public com.android.uiautomator.core.UiSelector selected(boolean); + method public com.android.uiautomator.core.UiSelector text(java.lang.String); + method public com.android.uiautomator.core.UiSelector textContains(java.lang.String); + method public com.android.uiautomator.core.UiSelector textStartsWith(java.lang.String); + } + + public abstract interface UiWatcher { + method public abstract boolean checkForCondition(); + } + +} + +package com.android.uiautomator.testrunner { + + public abstract interface IAutomationSupport { + method public abstract void sendStatus(int, android.os.Bundle); + } + + public class UiAutomatorTestCase extends junit.framework.TestCase { + ctor public UiAutomatorTestCase(); + method public com.android.uiautomator.testrunner.IAutomationSupport getAutomationSupport(); + method public android.os.Bundle getParams(); + method public com.android.uiautomator.core.UiDevice getUiDevice(); + method public void sleep(long); + } + +} + diff --git a/api/17.txt b/api/17.txt new file mode 100644 index 0000000..a1d80c4 --- /dev/null +++ b/api/17.txt @@ -0,0 +1,192 @@ +package com.android.uiautomator.core { + + public class UiCollection extends com.android.uiautomator.core.UiObject { + ctor public UiCollection(com.android.uiautomator.core.UiSelector); + method public com.android.uiautomator.core.UiObject getChildByDescription(com.android.uiautomator.core.UiSelector, java.lang.String) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public com.android.uiautomator.core.UiObject getChildByInstance(com.android.uiautomator.core.UiSelector, int) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public com.android.uiautomator.core.UiObject getChildByText(com.android.uiautomator.core.UiSelector, java.lang.String) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public int getChildCount(com.android.uiautomator.core.UiSelector); + } + + public class UiDevice { + method public void clearLastTraversedText(); + method public boolean click(int, int); + method public void dumpWindowHierarchy(java.lang.String); + method public void freezeRotation() throws android.os.RemoteException; + method public deprecated java.lang.String getCurrentActivityName(); + method public java.lang.String getCurrentPackageName(); + method public int getDisplayHeight(); + method public int getDisplayRotation(); + method public int getDisplayWidth(); + method public static com.android.uiautomator.core.UiDevice getInstance(); + method public java.lang.String getLastTraversedText(); + method public java.lang.String getProductName(); + method public boolean hasAnyWatcherTriggered(); + method public boolean hasWatcherTriggered(java.lang.String); + method public boolean isNaturalOrientation(); + method public boolean isScreenOn() throws android.os.RemoteException; + method public boolean pressBack(); + method public boolean pressDPadCenter(); + method public boolean pressDPadDown(); + method public boolean pressDPadLeft(); + method public boolean pressDPadRight(); + method public boolean pressDPadUp(); + method public boolean pressDelete(); + method public boolean pressEnter(); + method public boolean pressHome(); + method public boolean pressKeyCode(int); + method public boolean pressKeyCode(int, int); + method public boolean pressMenu(); + method public boolean pressRecentApps() throws android.os.RemoteException; + method public boolean pressSearch(); + method public void registerWatcher(java.lang.String, com.android.uiautomator.core.UiWatcher); + method public void removeWatcher(java.lang.String); + method public void resetWatcherTriggers(); + method public void runWatchers(); + method public void setOrientationLeft() throws android.os.RemoteException; + method public void setOrientationNatural() throws android.os.RemoteException; + method public void setOrientationRight() throws android.os.RemoteException; + method public void sleep() throws android.os.RemoteException; + method public boolean swipe(int, int, int, int, int); + method public boolean swipe(android.graphics.Point[], int); + method public boolean takeScreenshot(java.io.File); + method public boolean takeScreenshot(java.io.File, float, int); + method public void unfreezeRotation() throws android.os.RemoteException; + method public void waitForIdle(); + method public void waitForIdle(long); + method public boolean waitForWindowUpdate(java.lang.String, long); + method public void wakeUp() throws android.os.RemoteException; + } + + public class UiObject { + ctor public UiObject(com.android.uiautomator.core.UiSelector); + method public void clearTextField() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean click() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean clickAndWaitForNewWindow() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean clickAndWaitForNewWindow(long) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean clickBottomRight() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean clickTopLeft() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean exists(); + method protected android.view.accessibility.AccessibilityNodeInfo findAccessibilityNodeInfo(long); + method public android.graphics.Rect getBounds() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public com.android.uiautomator.core.UiObject getChild(com.android.uiautomator.core.UiSelector) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public int getChildCount() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public java.lang.String getContentDescription() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public com.android.uiautomator.core.UiObject getFromParent(com.android.uiautomator.core.UiSelector) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public java.lang.String getPackageName() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public final com.android.uiautomator.core.UiSelector getSelector(); + method public java.lang.String getText() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public android.graphics.Rect getVisibleBounds() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean isCheckable() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean isChecked() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean isClickable() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean isEnabled() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean isFocusable() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean isFocused() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean isLongClickable() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean isScrollable() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean isSelected() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean longClick() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean longClickBottomRight() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean longClickTopLeft() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean setText(java.lang.String) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean swipeDown(int) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean swipeLeft(int) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean swipeRight(int) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean swipeUp(int) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean waitForExists(long); + method public boolean waitUntilGone(long); + field protected static final int SWIPE_MARGIN_LIMIT = 5; // 0x5 + field protected static final long WAIT_FOR_EVENT_TMEOUT = 3000L; // 0xbb8L + field protected static final long WAIT_FOR_SELECTOR_POLL = 1000L; // 0x3e8L + field protected static final long WAIT_FOR_SELECTOR_TIMEOUT = 10000L; // 0x2710L + field protected static final long WAIT_FOR_WINDOW_TMEOUT = 5500L; // 0x157cL + } + + public class UiObjectNotFoundException extends java.lang.Exception { + ctor public UiObjectNotFoundException(java.lang.String); + ctor public UiObjectNotFoundException(java.lang.String, java.lang.Throwable); + ctor public UiObjectNotFoundException(java.lang.Throwable); + } + + public class UiScrollable extends com.android.uiautomator.core.UiCollection { + ctor public UiScrollable(com.android.uiautomator.core.UiSelector); + method protected boolean exists(com.android.uiautomator.core.UiSelector); + method public boolean flingBackward() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean flingForward() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean flingToBeginning(int) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean flingToEnd(int) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public com.android.uiautomator.core.UiObject getChildByDescription(com.android.uiautomator.core.UiSelector, java.lang.String, boolean) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public com.android.uiautomator.core.UiObject getChildByText(com.android.uiautomator.core.UiSelector, java.lang.String, boolean) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public int getMaxSearchSwipes(); + method public double getSwipeDeadZonePercentage(); + method public boolean scrollBackward() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean scrollBackward(int) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean scrollDescriptionIntoView(java.lang.String) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean scrollForward() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean scrollForward(int) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean scrollIntoView(com.android.uiautomator.core.UiObject) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean scrollIntoView(com.android.uiautomator.core.UiSelector) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean scrollTextIntoView(java.lang.String) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean scrollToBeginning(int, int) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean scrollToBeginning(int) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean scrollToEnd(int, int) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean scrollToEnd(int) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public com.android.uiautomator.core.UiScrollable setAsHorizontalList(); + method public com.android.uiautomator.core.UiScrollable setAsVerticalList(); + method public com.android.uiautomator.core.UiScrollable setMaxSearchSwipes(int); + method public com.android.uiautomator.core.UiScrollable setSwipeDeadZonePercentage(double); + } + + public class UiSelector { + ctor public UiSelector(); + method public com.android.uiautomator.core.UiSelector checked(boolean); + method public com.android.uiautomator.core.UiSelector childSelector(com.android.uiautomator.core.UiSelector); + method public com.android.uiautomator.core.UiSelector className(java.lang.String); + method public com.android.uiautomator.core.UiSelector className(java.lang.Class<T>); + method public com.android.uiautomator.core.UiSelector classNameMatches(java.lang.String); + method public com.android.uiautomator.core.UiSelector clickable(boolean); + method protected com.android.uiautomator.core.UiSelector cloneSelector(); + method public com.android.uiautomator.core.UiSelector description(java.lang.String); + method public com.android.uiautomator.core.UiSelector descriptionContains(java.lang.String); + method public com.android.uiautomator.core.UiSelector descriptionMatches(java.lang.String); + method public com.android.uiautomator.core.UiSelector descriptionStartsWith(java.lang.String); + method public com.android.uiautomator.core.UiSelector enabled(boolean); + method public com.android.uiautomator.core.UiSelector focusable(boolean); + method public com.android.uiautomator.core.UiSelector focused(boolean); + method public com.android.uiautomator.core.UiSelector fromParent(com.android.uiautomator.core.UiSelector); + method public com.android.uiautomator.core.UiSelector index(int); + method public com.android.uiautomator.core.UiSelector instance(int); + method public com.android.uiautomator.core.UiSelector longClickable(boolean); + method public com.android.uiautomator.core.UiSelector packageName(java.lang.String); + method public com.android.uiautomator.core.UiSelector packageNameMatches(java.lang.String); + method public com.android.uiautomator.core.UiSelector scrollable(boolean); + method public com.android.uiautomator.core.UiSelector selected(boolean); + method public com.android.uiautomator.core.UiSelector text(java.lang.String); + method public com.android.uiautomator.core.UiSelector textContains(java.lang.String); + method public com.android.uiautomator.core.UiSelector textMatches(java.lang.String); + method public com.android.uiautomator.core.UiSelector textStartsWith(java.lang.String); + } + + public abstract interface UiWatcher { + method public abstract boolean checkForCondition(); + } + +} + +package com.android.uiautomator.testrunner { + + public abstract interface IAutomationSupport { + method public abstract void sendStatus(int, android.os.Bundle); + } + + public class UiAutomatorTestCase extends junit.framework.TestCase { + ctor public UiAutomatorTestCase(); + method public com.android.uiautomator.testrunner.IAutomationSupport getAutomationSupport(); + method public android.os.Bundle getParams(); + method public com.android.uiautomator.core.UiDevice getUiDevice(); + method public void sleep(long); + } + +} + diff --git a/api/current.txt b/api/current.txt new file mode 100644 index 0000000..7eeecf5 --- /dev/null +++ b/api/current.txt @@ -0,0 +1,222 @@ +package com.android.uiautomator.core { + + public final class Configurator { + method public long getActionAcknowledgmentTimeout(); + method public static com.android.uiautomator.core.Configurator getInstance(); + method public long getKeyInjectionDelay(); + method public long getScrollAcknowledgmentTimeout(); + method public long getWaitForIdleTimeout(); + method public long getWaitForSelectorTimeout(); + method public com.android.uiautomator.core.Configurator setActionAcknowledgmentTimeout(long); + method public com.android.uiautomator.core.Configurator setKeyInjectionDelay(long); + method public com.android.uiautomator.core.Configurator setScrollAcknowledgmentTimeout(long); + method public com.android.uiautomator.core.Configurator setWaitForIdleTimeout(long); + method public com.android.uiautomator.core.Configurator setWaitForSelectorTimeout(long); + } + + public class UiCollection extends com.android.uiautomator.core.UiObject { + ctor public UiCollection(com.android.uiautomator.core.UiSelector); + method public com.android.uiautomator.core.UiObject getChildByDescription(com.android.uiautomator.core.UiSelector, java.lang.String) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public com.android.uiautomator.core.UiObject getChildByInstance(com.android.uiautomator.core.UiSelector, int) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public com.android.uiautomator.core.UiObject getChildByText(com.android.uiautomator.core.UiSelector, java.lang.String) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public int getChildCount(com.android.uiautomator.core.UiSelector); + } + + public class UiDevice { + method public void clearLastTraversedText(); + method public boolean click(int, int); + method public boolean drag(int, int, int, int, int); + method public void dumpWindowHierarchy(java.lang.String); + method public void freezeRotation() throws android.os.RemoteException; + method public deprecated java.lang.String getCurrentActivityName(); + method public java.lang.String getCurrentPackageName(); + method public int getDisplayHeight(); + method public int getDisplayRotation(); + method public android.graphics.Point getDisplaySizeDp(); + method public int getDisplayWidth(); + method public static com.android.uiautomator.core.UiDevice getInstance(); + method public java.lang.String getLastTraversedText(); + method public java.lang.String getProductName(); + method public boolean hasAnyWatcherTriggered(); + method public boolean hasWatcherTriggered(java.lang.String); + method public boolean isNaturalOrientation(); + method public boolean isScreenOn() throws android.os.RemoteException; + method public boolean openNotification(); + method public boolean openQuickSettings(); + method public boolean pressBack(); + method public boolean pressDPadCenter(); + method public boolean pressDPadDown(); + method public boolean pressDPadLeft(); + method public boolean pressDPadRight(); + method public boolean pressDPadUp(); + method public boolean pressDelete(); + method public boolean pressEnter(); + method public boolean pressHome(); + method public boolean pressKeyCode(int); + method public boolean pressKeyCode(int, int); + method public boolean pressMenu(); + method public boolean pressRecentApps() throws android.os.RemoteException; + method public boolean pressSearch(); + method public void registerWatcher(java.lang.String, com.android.uiautomator.core.UiWatcher); + method public void removeWatcher(java.lang.String); + method public void resetWatcherTriggers(); + method public void runWatchers(); + method public void setCompressedLayoutHeirarchy(boolean); + method public void setOrientationLeft() throws android.os.RemoteException; + method public void setOrientationNatural() throws android.os.RemoteException; + method public void setOrientationRight() throws android.os.RemoteException; + method public void sleep() throws android.os.RemoteException; + method public boolean swipe(int, int, int, int, int); + method public boolean swipe(android.graphics.Point[], int); + method public boolean takeScreenshot(java.io.File); + method public boolean takeScreenshot(java.io.File, float, int); + method public void unfreezeRotation() throws android.os.RemoteException; + method public void waitForIdle(); + method public void waitForIdle(long); + method public boolean waitForWindowUpdate(java.lang.String, long); + method public void wakeUp() throws android.os.RemoteException; + } + + public class UiObject { + ctor public UiObject(com.android.uiautomator.core.UiSelector); + method public void clearTextField() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean click() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean clickAndWaitForNewWindow() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean clickAndWaitForNewWindow(long) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean clickBottomRight() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean clickTopLeft() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean dragTo(com.android.uiautomator.core.UiObject, int) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean dragTo(int, int, int) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean exists(); + method protected android.view.accessibility.AccessibilityNodeInfo findAccessibilityNodeInfo(long); + method public android.graphics.Rect getBounds() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public com.android.uiautomator.core.UiObject getChild(com.android.uiautomator.core.UiSelector) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public int getChildCount() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public java.lang.String getClassName() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public java.lang.String getContentDescription() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public com.android.uiautomator.core.UiObject getFromParent(com.android.uiautomator.core.UiSelector) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public java.lang.String getPackageName() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public final com.android.uiautomator.core.UiSelector getSelector(); + method public java.lang.String getText() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public android.graphics.Rect getVisibleBounds() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean isCheckable() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean isChecked() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean isClickable() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean isEnabled() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean isFocusable() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean isFocused() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean isLongClickable() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean isScrollable() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean isSelected() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean longClick() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean longClickBottomRight() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean longClickTopLeft() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean performMultiPointerGesture(android.view.MotionEvent.PointerCoords...); + method public boolean performTwoPointerGesture(android.graphics.Point, android.graphics.Point, android.graphics.Point, android.graphics.Point, int); + method public boolean pinchIn(int, int) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean pinchOut(int, int) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean setText(java.lang.String) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean swipeDown(int) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean swipeLeft(int) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean swipeRight(int) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean swipeUp(int) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean waitForExists(long); + method public boolean waitUntilGone(long); + field protected static final int FINGER_TOUCH_HALF_WIDTH = 20; // 0x14 + field protected static final int SWIPE_MARGIN_LIMIT = 5; // 0x5 + field protected static final deprecated long WAIT_FOR_EVENT_TMEOUT = 3000L; // 0xbb8L + field protected static final long WAIT_FOR_SELECTOR_POLL = 1000L; // 0x3e8L + field protected static final deprecated long WAIT_FOR_SELECTOR_TIMEOUT = 10000L; // 0x2710L + field protected static final long WAIT_FOR_WINDOW_TMEOUT = 5500L; // 0x157cL + } + + public class UiObjectNotFoundException extends java.lang.Exception { + ctor public UiObjectNotFoundException(java.lang.String); + ctor public UiObjectNotFoundException(java.lang.String, java.lang.Throwable); + ctor public UiObjectNotFoundException(java.lang.Throwable); + } + + public class UiScrollable extends com.android.uiautomator.core.UiCollection { + ctor public UiScrollable(com.android.uiautomator.core.UiSelector); + method protected boolean exists(com.android.uiautomator.core.UiSelector); + method public boolean flingBackward() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean flingForward() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean flingToBeginning(int) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean flingToEnd(int) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public com.android.uiautomator.core.UiObject getChildByDescription(com.android.uiautomator.core.UiSelector, java.lang.String, boolean) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public com.android.uiautomator.core.UiObject getChildByText(com.android.uiautomator.core.UiSelector, java.lang.String, boolean) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public int getMaxSearchSwipes(); + method public double getSwipeDeadZonePercentage(); + method public boolean scrollBackward() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean scrollBackward(int) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean scrollDescriptionIntoView(java.lang.String) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean scrollForward() throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean scrollForward(int) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean scrollIntoView(com.android.uiautomator.core.UiObject) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean scrollIntoView(com.android.uiautomator.core.UiSelector) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean scrollTextIntoView(java.lang.String) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean scrollToBeginning(int, int) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean scrollToBeginning(int) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean scrollToEnd(int, int) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public boolean scrollToEnd(int) throws com.android.uiautomator.core.UiObjectNotFoundException; + method public com.android.uiautomator.core.UiScrollable setAsHorizontalList(); + method public com.android.uiautomator.core.UiScrollable setAsVerticalList(); + method public com.android.uiautomator.core.UiScrollable setMaxSearchSwipes(int); + method public com.android.uiautomator.core.UiScrollable setSwipeDeadZonePercentage(double); + } + + public class UiSelector { + ctor public UiSelector(); + method public com.android.uiautomator.core.UiSelector checkable(boolean); + method public com.android.uiautomator.core.UiSelector checked(boolean); + method public com.android.uiautomator.core.UiSelector childSelector(com.android.uiautomator.core.UiSelector); + method public com.android.uiautomator.core.UiSelector className(java.lang.String); + method public com.android.uiautomator.core.UiSelector className(java.lang.Class<T>); + method public com.android.uiautomator.core.UiSelector classNameMatches(java.lang.String); + method public com.android.uiautomator.core.UiSelector clickable(boolean); + method protected com.android.uiautomator.core.UiSelector cloneSelector(); + method public com.android.uiautomator.core.UiSelector description(java.lang.String); + method public com.android.uiautomator.core.UiSelector descriptionContains(java.lang.String); + method public com.android.uiautomator.core.UiSelector descriptionMatches(java.lang.String); + method public com.android.uiautomator.core.UiSelector descriptionStartsWith(java.lang.String); + method public com.android.uiautomator.core.UiSelector enabled(boolean); + method public com.android.uiautomator.core.UiSelector focusable(boolean); + method public com.android.uiautomator.core.UiSelector focused(boolean); + method public com.android.uiautomator.core.UiSelector fromParent(com.android.uiautomator.core.UiSelector); + method public com.android.uiautomator.core.UiSelector index(int); + method public com.android.uiautomator.core.UiSelector instance(int); + method public com.android.uiautomator.core.UiSelector longClickable(boolean); + method public com.android.uiautomator.core.UiSelector packageName(java.lang.String); + method public com.android.uiautomator.core.UiSelector packageNameMatches(java.lang.String); + method public com.android.uiautomator.core.UiSelector resourceId(java.lang.String); + method public com.android.uiautomator.core.UiSelector resourceIdMatches(java.lang.String); + method public com.android.uiautomator.core.UiSelector scrollable(boolean); + method public com.android.uiautomator.core.UiSelector selected(boolean); + method public com.android.uiautomator.core.UiSelector text(java.lang.String); + method public com.android.uiautomator.core.UiSelector textContains(java.lang.String); + method public com.android.uiautomator.core.UiSelector textMatches(java.lang.String); + method public com.android.uiautomator.core.UiSelector textStartsWith(java.lang.String); + } + + public abstract interface UiWatcher { + method public abstract boolean checkForCondition(); + } + +} + +package com.android.uiautomator.testrunner { + + public abstract interface IAutomationSupport { + method public abstract void sendStatus(int, android.os.Bundle); + } + + public class UiAutomatorTestCase extends junit.framework.TestCase { + ctor public UiAutomatorTestCase(); + method public com.android.uiautomator.testrunner.IAutomationSupport getAutomationSupport(); + method public android.os.Bundle getParams(); + method public com.android.uiautomator.core.UiDevice getUiDevice(); + method public void sleep(long); + } + +} + diff --git a/cmds/Android.mk b/cmds/Android.mk new file mode 100644 index 0000000..c141484 --- /dev/null +++ b/cmds/Android.mk @@ -0,0 +1,17 @@ +# +# 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. +# + +include $(call all-subdir-makefiles) diff --git a/cmds/uiautomator/Android.mk b/cmds/uiautomator/Android.mk new file mode 100644 index 0000000..5c91b52 --- /dev/null +++ b/cmds/uiautomator/Android.mk @@ -0,0 +1,33 @@ +# +# 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. +# + +LOCAL_PATH:= $(call my-dir) + +include $(CLEAR_VARS) +LOCAL_MODULE_TAGS := optional +LOCAL_SRC_FILES := $(call all-java-files-under, src) +LOCAL_STATIC_JAVA_LIBRARIES := uiautomator.core +LOCAL_MODULE := uiautomator + +include $(BUILD_JAVA_LIBRARY) + +include $(CLEAR_VARS) +LOCAL_MODULE := uiautomator +LOCAL_SRC_FILES := uiautomator +LOCAL_MODULE_CLASS := EXECUTABLES +LOCAL_MODULE_TAGS := optional + +include $(BUILD_PREBUILT) diff --git a/cmds/uiautomator/src/com/android/commands/uiautomator/DumpCommand.java b/cmds/uiautomator/src/com/android/commands/uiautomator/DumpCommand.java new file mode 100644 index 0000000..c35f7fc --- /dev/null +++ b/cmds/uiautomator/src/com/android/commands/uiautomator/DumpCommand.java @@ -0,0 +1,109 @@ +/* + * 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.commands.uiautomator; + +import android.app.UiAutomation; +import android.graphics.Point; +import android.hardware.display.DisplayManagerGlobal; +import android.os.Environment; +import android.view.Display; +import android.view.accessibility.AccessibilityNodeInfo; + +import com.android.commands.uiautomator.Launcher.Command; +import com.android.uiautomator.core.AccessibilityNodeInfoDumper; +import com.android.uiautomator.core.UiAutomationShellWrapper; + +import java.io.File; +import java.util.concurrent.TimeoutException; + +/** + * Implementation of the dump subcommand + * + * This creates an XML dump of current UI hierarchy + */ +public class DumpCommand extends Command { + + private static final File DEFAULT_DUMP_FILE = new File( + Environment.getLegacyExternalStorageDirectory(), "window_dump.xml"); + + public DumpCommand() { + super("dump"); + } + + @Override + public String shortHelp() { + return "creates an XML dump of current UI hierarchy"; + } + + @Override + public String detailedOptions() { + return " dump [--verbose][file]\n" + + " [--compressed]: dumps compressed layout information.\n" + + " [file]: the location where the dumped XML should be stored, default is\n " + + DEFAULT_DUMP_FILE.getAbsolutePath() + "\n"; + } + + @Override + public void run(String[] args) { + File dumpFile = DEFAULT_DUMP_FILE; + boolean verboseMode = true; + + for (String arg : args) { + if (arg.equals("--compressed")) + verboseMode = false; + else if (!arg.startsWith("-")) { + dumpFile = new File(arg); + } + } + + UiAutomationShellWrapper automationWrapper = new UiAutomationShellWrapper(); + automationWrapper.connect(); + if (verboseMode) { + // default + automationWrapper.setCompressedLayoutHierarchy(false); + } else { + automationWrapper.setCompressedLayoutHierarchy(true); + } + + // It appears that the bridge needs time to be ready. Making calls to the + // bridge immediately after connecting seems to cause exceptions. So let's also + // do a wait for idle in case the app is busy. + try { + UiAutomation uiAutomation = automationWrapper.getUiAutomation(); + uiAutomation.waitForIdle(1000, 1000 * 10); + AccessibilityNodeInfo info = uiAutomation.getRootInActiveWindow(); + if (info == null) { + System.err.println("ERROR: null root node returned by UiTestAutomationBridge."); + return; + } + + Display display = + DisplayManagerGlobal.getInstance().getRealDisplay(Display.DEFAULT_DISPLAY); + int rotation = display.getRotation(); + Point size = new Point(); + display.getSize(size); + AccessibilityNodeInfoDumper.dumpWindowToFile(info, dumpFile, rotation, size.x, size.y); + } catch (TimeoutException re) { + System.err.println("ERROR: could not get idle state."); + return; + } finally { + automationWrapper.disconnect(); + } + System.out.println( + String.format("UI hierchary dumped to: %s", dumpFile.getAbsolutePath())); + } +} diff --git a/cmds/uiautomator/src/com/android/commands/uiautomator/EventsCommand.java b/cmds/uiautomator/src/com/android/commands/uiautomator/EventsCommand.java new file mode 100644 index 0000000..ce55f18 --- /dev/null +++ b/cmds/uiautomator/src/com/android/commands/uiautomator/EventsCommand.java @@ -0,0 +1,75 @@ +/* + * 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.commands.uiautomator; + +import android.app.UiAutomation.OnAccessibilityEventListener; +import android.view.accessibility.AccessibilityEvent; + +import com.android.commands.uiautomator.Launcher.Command; +import com.android.uiautomator.core.UiAutomationShellWrapper; + +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * Implementation of the events subcommand + * + * Prints out accessibility events until process is stopped. + */ +public class EventsCommand extends Command { + + private Object mQuitLock = new Object(); + + public EventsCommand() { + super("events"); + } + + @Override + public String shortHelp() { + return "prints out accessibility events until terminated"; + } + + @Override + public String detailedOptions() { + return null; + } + + @Override + public void run(String[] args) { + UiAutomationShellWrapper automationWrapper = new UiAutomationShellWrapper(); + automationWrapper.connect(); + automationWrapper.getUiAutomation().setOnAccessibilityEventListener( + new OnAccessibilityEventListener() { + @Override + public void onAccessibilityEvent(AccessibilityEvent event) { + SimpleDateFormat formatter = new SimpleDateFormat("MM-dd HH:mm:ss.SSS"); + System.out.println(String.format("%s %s", + formatter.format(new Date()), event.toString())); + } + }); + // there's really no way to stop, essentially we just block indefinitely here and wait + // for user to press Ctrl+C + synchronized (mQuitLock) { + try { + mQuitLock.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + automationWrapper.disconnect(); + } +} diff --git a/cmds/uiautomator/src/com/android/commands/uiautomator/Launcher.java b/cmds/uiautomator/src/com/android/commands/uiautomator/Launcher.java new file mode 100644 index 0000000..bc1d948 --- /dev/null +++ b/cmds/uiautomator/src/com/android/commands/uiautomator/Launcher.java @@ -0,0 +1,135 @@ +/* + * 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.commands.uiautomator; + +import android.os.Process; + +import java.util.Arrays; + +/** + * Entry point into the uiautomator command line + * + * This class maintains the list of sub commands, and redirect the control into it based on the + * command line arguments. It also prints out help arguments for each sub commands. + * + * To add a new sub command, implement {@link Command} and add an instance into COMMANDS array + */ +public class Launcher { + + /** + * A simple abstraction class for supporting generic sub commands + */ + public static abstract class Command { + private String mName; + + public Command(String name) { + mName = name; + } + + /** + * Returns the name of the sub command + * @return + */ + public String name() { + return mName; + } + + /** + * Returns a one-liner of the function of this command + * @return + */ + public abstract String shortHelp(); + + /** + * Returns a detailed explanation of the command usage + * + * Usage may have multiple lines, indentation of 4 spaces recommended. + * @return + */ + public abstract String detailedOptions(); + + /** + * Starts the command with the provided arguments + * @param args + */ + public abstract void run(String args[]); + } + + public static void main(String[] args) { + // show a meaningful process name in `ps` + Process.setArgV0("uiautomator"); + if (args.length >= 1) { + Command command = findCommand(args[0]); + if (command != null) { + String[] args2 = {}; + if (args.length > 1) { + // consume the first arg + args2 = Arrays.copyOfRange(args, 1, args.length); + } + command.run(args2); + return; + } + } + HELP_COMMAND.run(args); + } + + private static Command findCommand(String name) { + for (Command command : COMMANDS) { + if (command.name().equals(name)) { + return command; + } + } + return null; + } + + private static Command HELP_COMMAND = new Command("help") { + @Override + public void run(String[] args) { + System.err.println("Usage: uiautomator <subcommand> [options]\n"); + System.err.println("Available subcommands:\n"); + for (Command command : COMMANDS) { + String shortHelp = command.shortHelp(); + String detailedOptions = command.detailedOptions(); + if (shortHelp == null) { + shortHelp = ""; + } + if (detailedOptions == null) { + detailedOptions = ""; + } + System.err.println(String.format("%s: %s", command.name(), shortHelp)); + System.err.println(detailedOptions); + } + } + + @Override + public String detailedOptions() { + return null; + } + + @Override + public String shortHelp() { + return "displays help message"; + } + }; + + private static Command[] COMMANDS = new Command[] { + HELP_COMMAND, + new RunTestCommand(), + new DumpCommand(), + new EventsCommand(), + }; +}
\ No newline at end of file diff --git a/cmds/uiautomator/src/com/android/commands/uiautomator/RunTestCommand.java b/cmds/uiautomator/src/com/android/commands/uiautomator/RunTestCommand.java new file mode 100644 index 0000000..65611ab --- /dev/null +++ b/cmds/uiautomator/src/com/android/commands/uiautomator/RunTestCommand.java @@ -0,0 +1,258 @@ +/* + * 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.commands.uiautomator; + +import android.os.Bundle; +import android.util.Log; + +import com.android.commands.uiautomator.Launcher.Command; +import com.android.uiautomator.testrunner.UiAutomatorTestRunner; + +import dalvik.system.DexFile; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; + +/** + * Implementation of the runtest sub command + * + */ +public class RunTestCommand extends Command { + private static final String LOGTAG = RunTestCommand.class.getSimpleName(); + + private static final String OUTPUT_SIMPLE = "simple"; + private static final String OUTPUT_FORMAT_KEY = "outputFormat"; + private static final String CLASS_PARAM = "class"; + private static final String JARS_PARAM = "jars"; + private static final String DEBUG_PARAM = "debug"; + private static final String RUNNER_PARAM = "runner"; + private static final String CLASS_SEPARATOR = ","; + private static final String JARS_SEPARATOR = ":"; + private static final int ARG_OK = 0; + private static final int ARG_FAIL_INCOMPLETE_E = -1; + private static final int ARG_FAIL_INCOMPLETE_C = -2; + private static final int ARG_FAIL_NO_CLASS = -3; + private static final int ARG_FAIL_RUNNER = -4; + private static final int ARG_FAIL_UNSUPPORTED = -99; + + private final Bundle mParams = new Bundle(); + private final List<String> mTestClasses = new ArrayList<String>(); + private boolean mDebug; + private boolean mMonkey = false; + private String mRunnerClassName; + private UiAutomatorTestRunner mRunner; + + public RunTestCommand() { + super("runtest"); + } + + @Override + public void run(String[] args) { + int ret = parseArgs(args); + switch (ret) { + case ARG_FAIL_INCOMPLETE_C: + System.err.println("Incomplete '-c' parameter."); + System.exit(ARG_FAIL_INCOMPLETE_C); + break; + case ARG_FAIL_INCOMPLETE_E: + System.err.println("Incomplete '-e' parameter."); + System.exit(ARG_FAIL_INCOMPLETE_E); + break; + case ARG_FAIL_UNSUPPORTED: + System.err.println("Unsupported standalone parameter."); + System.exit(ARG_FAIL_UNSUPPORTED); + break; + default: + break; + } + if (mTestClasses.isEmpty()) { + addTestClassesFromJars(); + if (mTestClasses.isEmpty()) { + System.err.println("No test classes found."); + System.exit(ARG_FAIL_NO_CLASS); + } + } + getRunner().run(mTestClasses, mParams, mDebug, mMonkey); + } + + private int parseArgs(String[] args) { + // we are parsing for these parameters: + // -e <key> <value> + // key-value pairs + // special ones are: + // key is "class", parameter is passed onto JUnit as class name to run + // key is "debug", parameter will determine whether to wait for debugger + // to attach + // -c <class name> + // -s turns on the simple output format + // equivalent to -e class <class name>, i.e. passed onto JUnit + for (int i = 0; i < args.length; i++) { + if (args[i].equals("-e")) { + if (i + 2 < args.length) { + String key = args[++i]; + String value = args[++i]; + if (CLASS_PARAM.equals(key)) { + addTestClasses(value); + } else if (DEBUG_PARAM.equals(key)) { + mDebug = "true".equals(value) || "1".equals(value); + } else if (RUNNER_PARAM.equals(key)) { + mRunnerClassName = value; + } else { + mParams.putString(key, value); + } + } else { + return ARG_FAIL_INCOMPLETE_E; + } + } else if (args[i].equals("-c")) { + if (i + 1 < args.length) { + addTestClasses(args[++i]); + } else { + return ARG_FAIL_INCOMPLETE_C; + } + } else if (args[i].equals("--monkey")) { + mMonkey = true; + } else if (args[i].equals("-s")) { + mParams.putString(OUTPUT_FORMAT_KEY, OUTPUT_SIMPLE); + } else { + return ARG_FAIL_UNSUPPORTED; + } + } + return ARG_OK; + } + + protected UiAutomatorTestRunner getRunner() { + if (mRunner != null) { + return mRunner; + } + + if (mRunnerClassName == null) { + mRunner = new UiAutomatorTestRunner(); + return mRunner; + } + // use reflection to get the runner + Object o = null; + try { + Class<?> clazz = Class.forName(mRunnerClassName); + o = clazz.newInstance(); + } catch (ClassNotFoundException cnfe) { + System.err.println("Cannot find runner: " + mRunnerClassName); + System.exit(ARG_FAIL_RUNNER); + } catch (InstantiationException ie) { + System.err.println("Cannot instantiate runner: " + mRunnerClassName); + System.exit(ARG_FAIL_RUNNER); + } catch (IllegalAccessException iae) { + System.err.println("Constructor of runner " + mRunnerClassName + " is not accessibile"); + System.exit(ARG_FAIL_RUNNER); + } + try { + UiAutomatorTestRunner runner = (UiAutomatorTestRunner)o; + mRunner = runner; + return runner; + } catch (ClassCastException cce) { + System.err.println("Specified runner is not subclass of " + + UiAutomatorTestRunner.class.getSimpleName()); + System.exit(ARG_FAIL_RUNNER); + } + // won't reach here + return null; + } + + /** + * Add test classes from a potentially comma separated list + * @param classes + */ + private void addTestClasses(String classes) { + String[] classArray = classes.split(CLASS_SEPARATOR); + for (String clazz : classArray) { + mTestClasses.add(clazz); + } + } + + /** + * Add test classes from jars passed on the command line. Use this if nothing was explicitly + * specified on the command line. + */ + private void addTestClassesFromJars() { + String jars = mParams.getString(JARS_PARAM); + if (jars == null) return; + + String[] jarFileNames = jars.split(JARS_SEPARATOR); + for (String fileName : jarFileNames) { + fileName = fileName.trim(); + if (fileName.isEmpty()) continue; + try { + DexFile dexFile = new DexFile(fileName); + for(Enumeration<String> e = dexFile.entries(); e.hasMoreElements();) { + String className = e.nextElement(); + if (isTestClass(className)) { + mTestClasses.add(className); + } + } + dexFile.close(); + } catch (IOException e) { + Log.w(LOGTAG, String.format("Could not read %s: %s", fileName, e.getMessage())); + } + } + } + + /** + * Tries to determine if a given class is a test class. A test class has to inherit from + * UiAutomator test case and it must be a top-level class. + * @param className + * @return + */ + private boolean isTestClass(String className) { + try { + Class<?> clazz = this.getClass().getClassLoader().loadClass(className); + if (clazz.getEnclosingClass() != null) return false; + return getRunner().getTestCaseFilter().accept(clazz); + } catch (ClassNotFoundException e) { + return false; + } + } + + @Override + public String detailedOptions() { + return " runtest <class spec> [options]\n" + + " <class spec>: <JARS> < -c <CLASSES> | -e class <CLASSES> >\n" + + " <JARS>: a list of jar files containing test classes and dependencies. If\n" + + " the path is relative, it's assumed to be under /data/local/tmp. Use\n" + + " absolute path if the file is elsewhere. Multiple files can be\n" + + " specified, separated by space.\n" + + " <CLASSES>: a list of test class names to run, separated by comma. To\n" + + " a single method, use TestClass#testMethod format. The -e or -c option\n" + + " may be repeated. This option is not required and if not provided then\n" + + " all the tests in provided jars will be run automatically.\n" + + " options:\n" + + " --nohup: trap SIG_HUP, so test won't terminate even if parent process\n" + + " is terminated, e.g. USB is disconnected.\n" + + " -e debug [true|false]: wait for debugger to connect before starting.\n" + + " -e runner [CLASS]: use specified test runner class instead. If\n" + + " unspecified, framework default runner will be used.\n" + + " -e <NAME> <VALUE>: other name-value pairs to be passed to test classes.\n" + + " May be repeated.\n" + + " -e outputFormat simple | -s: enabled less verbose JUnit style output.\n"; + } + + @Override + public String shortHelp() { + return "executes UI automation tests"; + } + +} diff --git a/cmds/uiautomator/uiautomator b/cmds/uiautomator/uiautomator new file mode 100755 index 0000000..9aec2c4 --- /dev/null +++ b/cmds/uiautomator/uiautomator @@ -0,0 +1,120 @@ +# +# 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. +# +# Script to start "uiautomator" on the device +# +# The script does a couple of things: +# * Use an alternative dalvik cache when running as non-root. Jar file needs +# to be dexopt'd to run in Dalvik. For plain jar files, this is done at first +# use. shell user does not have write permission to default system Dalvik +# cache so we redirect to an alternative cache +# * special processing for subcommand 'runtest': +# * '--nohup' allows process continue to run even if parent process that +# started it has already terminated. We parse for this parameter and set +# signal trap. This is useful for testing with USB disconnected +# * all jar files that the test classes resides in, or dependent on are +# provided on command line and exported to CLASSPATH environment variable +# before starting the Java code. This offloads the task of class loading +# and resolving of cross jar class dependency to Dalvik +# * all other subcommand or options are directly passed into Java code for +# further parsing + +export run_base=/data/local/tmp +export base=/system + +# if not running as root, trick dalvik into using an alternative dex cache +if [ ${USER_ID} -ne 0 ]; then + tmp_cache=${run_base}/dalvik-cache + + if [ ! -d ${tmp_cache} ]; then + mkdir -p ${tmp_cache} + fi + + export ANDROID_DATA=${run_base} +fi + +# take first parameter as the command +cmd=${1} + +if [ -z "${1}" ]; then + cmd="help" +fi + +# strip the command parameter +if [ -n "${1}" ]; then + shift +fi + +CLASSPATH=/system/framework/android.test.runner.jar:${base}/framework/uiautomator.jar + +# eventually args will be what get passed down to Java code +args= +# we also pass the list of jar files, so we can extract class names for tests +# if they are not explicitly specified +jars= + +# special case pre-processing for 'runtest' command +if [ "${cmd}" == "runtest" ]; then + # first parse the jar paths + while [ true ]; do + if [ -z "${1}" ]; then + echo "Error: more parameters expected for runtest; please see usage for details" + cmd="help" + break + fi + jar=${1} + if [ "${1:0:1}" = "-" ]; then + # we are done with jars, starting with parameters now + break + fi + # if relative path, append the default path prefix + if [ "${1:0:1}" != "/" ]; then + jar=${run_base}/${1} + fi + # about to add the file to class path, check if it's valid + if [ ! -f ${jar} ]; then + echo "Error: ${jar} does not exist" + # force to print help message + cmd="help" + break + fi + jars=${jars}:${jar} + # done processing current arg, moving on + shift + done + # look for --nohup: if found, consume it and trap SIG_HUP, otherwise just + # append the arg to args + while [ -n "${1}" ]; do + if [ "${1}" = "--nohup" ]; then + trap "" HUP + shift + else + args="${args} ${1}" + shift + fi + done +else + # if cmd is not 'runtest', just take the rest of the args + args=${@} +fi + +args="${cmd} ${args}" +if [ -n "${jars}" ]; then + args="${args} -e jars ${jars}" +fi + +CLASSPATH=${CLASSPATH}:${jars} +export CLASSPATH +exec app_process ${base}/bin com.android.commands.uiautomator.Launcher ${args} diff --git a/instrumentation/Android.mk b/instrumentation/Android.mk new file mode 100644 index 0000000..0c93b4c --- /dev/null +++ b/instrumentation/Android.mk @@ -0,0 +1,29 @@ +# +# 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. +# + +LOCAL_PATH:= $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := tests +LOCAL_SRC_FILES := $(call all-java-files-under, testrunner-src) \ + $(call all-java-files-under, ../library/core-src) +LOCAL_JAVA_LIBRARIES := android.test.runner +LOCAL_MODULE := uiautomator-instrumentation +# TODO: change this to 18 when it's available +LOCAL_SDK_VERSION := current + +include $(BUILD_STATIC_JAVA_LIBRARY) diff --git a/instrumentation/testrunner-src/com/android/uiautomator/core/InstrumentationUiAutomatorBridge.java b/instrumentation/testrunner-src/com/android/uiautomator/core/InstrumentationUiAutomatorBridge.java new file mode 100644 index 0000000..1d4fb93 --- /dev/null +++ b/instrumentation/testrunner-src/com/android/uiautomator/core/InstrumentationUiAutomatorBridge.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2013 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.app.Service; +import android.app.UiAutomation; +import android.content.Context; +import android.os.PowerManager; +import android.view.Display; +import android.view.ViewConfiguration; +import android.view.WindowManager; + +/** + * @hide + */ +public class InstrumentationUiAutomatorBridge extends UiAutomatorBridge { + + private final Context mContext; + + public InstrumentationUiAutomatorBridge(Context context, UiAutomation uiAutomation) { + super(uiAutomation); + mContext = context; + } + + public Display getDefaultDisplay() { + WindowManager windowManager = (WindowManager) + mContext.getSystemService(Service.WINDOW_SERVICE); + return windowManager.getDefaultDisplay(); + } + + @Override + public int getRotation() { + return getDefaultDisplay().getRotation(); + } + + @Override + public boolean isScreenOn() { + PowerManager pm = (PowerManager) + mContext.getSystemService(Service.POWER_SERVICE); + return pm.isScreenOn(); + } + + public long getSystemLongPressTime() { + return ViewConfiguration.getLongPressTimeout(); + } +} diff --git a/instrumentation/testrunner-src/com/android/uiautomator/testrunner/IAutomationSupport.java b/instrumentation/testrunner-src/com/android/uiautomator/testrunner/IAutomationSupport.java new file mode 100644 index 0000000..f0c60d2 --- /dev/null +++ b/instrumentation/testrunner-src/com/android/uiautomator/testrunner/IAutomationSupport.java @@ -0,0 +1,37 @@ +/* + * 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.testrunner; + +import android.os.Bundle; + +/** + * Provides auxiliary support for running test cases + * + * @since API Level 16 + */ +public interface IAutomationSupport { + + /** + * Allows the running test cases to send out interim status + * + * @param resultCode + * @param status status report, consisting of key value pairs + * @since API Level 16 + */ + public void sendStatus(int resultCode, Bundle status); + +} diff --git a/instrumentation/testrunner-src/com/android/uiautomator/testrunner/InstrumentationAutomationSupport.java b/instrumentation/testrunner-src/com/android/uiautomator/testrunner/InstrumentationAutomationSupport.java new file mode 100644 index 0000000..a70586e --- /dev/null +++ b/instrumentation/testrunner-src/com/android/uiautomator/testrunner/InstrumentationAutomationSupport.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2013 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.testrunner; + +import android.app.Instrumentation; +import android.os.Bundle; + +/** + * A wrapper around {@link Instrumentation} to provide sendStatus function + * + * Provided for backwards compatibility purpose. New code should use + * {@link Instrumentation#sendStatus(int, Bundle)} instead. + * + */ +class InstrumentationAutomationSupport implements IAutomationSupport { + + private Instrumentation mInstrumentation; + + InstrumentationAutomationSupport(Instrumentation instrumentation) { + mInstrumentation = instrumentation; + } + + @Override + public void sendStatus(int resultCode, Bundle status) { + mInstrumentation.sendStatus(resultCode, status); + } +} diff --git a/instrumentation/testrunner-src/com/android/uiautomator/testrunner/UiAutomatorInstrumentationTestRunner.java b/instrumentation/testrunner-src/com/android/uiautomator/testrunner/UiAutomatorInstrumentationTestRunner.java new file mode 100644 index 0000000..ae763f2 --- /dev/null +++ b/instrumentation/testrunner-src/com/android/uiautomator/testrunner/UiAutomatorInstrumentationTestRunner.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2013 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.testrunner; + +import android.test.AndroidTestRunner; +import android.test.InstrumentationTestRunner; + +import com.android.uiautomator.core.Tracer; + +import junit.framework.AssertionFailedError; +import junit.framework.Test; +import junit.framework.TestListener; + +/** + * Test runner for {@link UiAutomatorTestCase}s. Such tests are executed + * on the device and have access to an applications context. + */ +public class UiAutomatorInstrumentationTestRunner extends InstrumentationTestRunner { + + @Override + public void onStart() { + // process runner arguments before test starts + String traceType = getArguments().getString("traceOutputMode"); + if(traceType != null) { + Tracer.Mode mode = Tracer.Mode.valueOf(Tracer.Mode.class, traceType); + if (mode == Tracer.Mode.FILE || mode == Tracer.Mode.ALL) { + String filename = getArguments().getString("traceLogFilename"); + if (filename == null) { + throw new RuntimeException("Name of log file not specified. " + + "Please specify it using traceLogFilename parameter"); + } + Tracer.getInstance().setOutputFilename(filename); + } + Tracer.getInstance().setOutputMode(mode); + } + super.onStart(); + } + + @Override + protected AndroidTestRunner getAndroidTestRunner() { + AndroidTestRunner testRunner = super.getAndroidTestRunner(); + testRunner.addTestListener(new TestListener() { + @Override + public void startTest(Test test) { + if (test instanceof UiAutomatorTestCase) { + ((UiAutomatorTestCase)test).initialize(getArguments()); + } + } + + @Override + public void endTest(Test test) { + } + + @Override + public void addFailure(Test test, AssertionFailedError e) { + } + + @Override + public void addError(Test test, Throwable t) { + } + }); + return testRunner; + } +} diff --git a/instrumentation/testrunner-src/com/android/uiautomator/testrunner/UiAutomatorTestCase.java b/instrumentation/testrunner-src/com/android/uiautomator/testrunner/UiAutomatorTestCase.java new file mode 100644 index 0000000..b5f21c9 --- /dev/null +++ b/instrumentation/testrunner-src/com/android/uiautomator/testrunner/UiAutomatorTestCase.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2013 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.testrunner; + +import android.app.Instrumentation; +import android.os.Bundle; +import android.os.SystemClock; +import android.test.InstrumentationTestCase; + +import com.android.uiautomator.core.InstrumentationUiAutomatorBridge; +import com.android.uiautomator.core.UiDevice; + +/** + * UI Automator test case that is executed on the device. + */ +public class UiAutomatorTestCase extends InstrumentationTestCase { + + private Bundle mParams; + private IAutomationSupport mAutomationSupport; + + /** + * Get current instance of {@link UiDevice}. Works similar to calling the static + * {@link UiDevice#getInstance()} from anywhere in the test classes. + * @since API Level 16 + */ + public UiDevice getUiDevice() { + return UiDevice.getInstance(); + } + + /** + * Get command line parameters. On the command line when passing <code>-e key value</code> + * pairs, the {@link Bundle} will have the key value pairs conveniently available to the + * tests. + * @since API Level 16 + */ + public Bundle getParams() { + return mParams; + } + + void setAutomationSupport(IAutomationSupport automationSupport) { + mAutomationSupport = automationSupport; + } + + /** + * Provides support for running tests to report interim status + * + * @return IAutomationSupport + * @since API Level 16 + * @deprecated Use {@link Instrumentation#sendStatus(int, Bundle)} instead + */ + public IAutomationSupport getAutomationSupport() { + if (mAutomationSupport == null) { + mAutomationSupport = new InstrumentationAutomationSupport(getInstrumentation()); + } + return mAutomationSupport; + } + + /** + * Initializes this test case. + * + * @param params Instrumentation arguments. + */ + void initialize(Bundle params) { + mParams = params; + + // check if this is a monkey test mode + String monkeyVal = mParams.getString("monkey"); + if (monkeyVal != null) { + // only if the monkey key is specified, we alter the state of monkey + // else we should leave things as they are. + getInstrumentation().getUiAutomation().setRunAsMonkey(Boolean.valueOf(monkeyVal)); + } + + UiDevice.getInstance().initialize(new InstrumentationUiAutomatorBridge( + getInstrumentation().getContext(), + getInstrumentation().getUiAutomation())); + } + + /** + * Calls {@link SystemClock#sleep(long)} to sleep + * @param ms is in milliseconds. + * @since API Level 16 + */ + public void sleep(long ms) { + SystemClock.sleep(ms); + } +} diff --git a/library/Android.mk b/library/Android.mk new file mode 100644 index 0000000..470e8ab --- /dev/null +++ b/library/Android.mk @@ -0,0 +1,123 @@ +# +# 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. +# + +LOCAL_PATH:= $(call my-dir) + +uiautomator.core_src_files := $(call all-java-files-under, core-src) \ + $(call all-java-files-under, testrunner-src) +uiautomator.core_java_libraries := android.test.runner core-junit + +uiautomator_internal_api_file := $(TARGET_OUT_COMMON_INTERMEDIATES)/PACKAGING/uiautomator_api.txt + +############################################### +include $(CLEAR_VARS) +LOCAL_SRC_FILES := $(uiautomator.core_src_files) +LOCAL_MODULE := uiautomator.core +LOCAL_JAVA_LIBRARIES := android.test.runner +include $(BUILD_STATIC_JAVA_LIBRARY) + +############################################### +# Generate the stub source files +include $(CLEAR_VARS) +LOCAL_SRC_FILES := $(uiautomator.core_src_files) +LOCAL_JAVA_LIBRARIES := $(uiautomator.core_java_libraries) +LOCAL_MODULE_CLASS := JAVA_LIBRARIES +LOCAL_DROIDDOC_SOURCE_PATH := $(LOCAL_PATH)/core-src \ + $(LOCAL_PATH)/testrunner-src +LOCAL_DROIDDOC_HTML_DIR := + +LOCAL_DROIDDOC_OPTIONS:= \ + -stubs $(TARGET_OUT_COMMON_INTERMEDIATES)/JAVA_LIBRARIES/android_uiautomator_intermediates/src \ + -stubpackages com.android.uiautomator.core:com.android.uiautomator.testrunner \ + -api $(uiautomator_internal_api_file) + +LOCAL_DROIDDOC_CUSTOM_TEMPLATE_DIR := build/tools/droiddoc/templates-sdk +LOCAL_UNINSTALLABLE_MODULE := true + +LOCAL_MODULE := uiautomator-stubs + +include $(BUILD_DROIDDOC) +uiautomator_stubs_stamp := $(full_target) +$(uiautomator_internal_api_file) : $(full_target) + +############################################### +# Build the stub source files into a jar. +include $(CLEAR_VARS) +LOCAL_MODULE := android_uiautomator +LOCAL_JAVA_LIBRARIES := $(uiautomator.core_java_libraries) +LOCAL_SOURCE_FILES_ALL_GENERATED := true +include $(BUILD_STATIC_JAVA_LIBRARY) +# Make sure to run droiddoc first to generate the stub source files. +$(full_classes_compiled_jar) : $(uiautomator_stubs_stamp) +uiautomator_stubs_jar := $(full_classes_compiled_jar) + +############################################### +# API check +# Please refer to build/core/tasks/apicheck.mk. +uiautomator_api_dir := frameworks/testing/uiautomator/api +last_released_sdk_version := $(lastword $(call numerically_sort, \ + $(filter-out current, \ + $(patsubst $(uiautomator_api_dir)/%.txt,%, $(wildcard $(uiautomator_api_dir)/*.txt)) \ + ))) + +checkapi_last_error_level_flags := \ + -hide 2 -hide 3 -hide 4 -hide 5 -hide 6 -hide 24 -hide 25 \ + -error 7 -error 8 -error 9 -error 10 -error 11 -error 12 -error 13 -error 14 -error 15 \ + -error 16 -error 17 -error 18 + +# Check that the API we're building hasn't broken the last-released SDK version. +$(eval $(call check-api, \ + uiautomator-checkapi-last, \ + $(uiautomator_api_dir)/$(last_released_sdk_version).txt, \ + $(uiautomator_internal_api_file), \ + $(checkapi_last_error_level_flags), \ + cat $(LOCAL_PATH)/apicheck_msg_last.txt, \ + $(uiautomator_stubs_jar), \ + $(uiautomator_stubs_stamp))) + +checkapi_current_error_level_flags := \ + -error 2 -error 3 -error 4 -error 5 -error 6 \ + -error 7 -error 8 -error 9 -error 10 -error 11 -error 12 -error 13 -error 14 -error 15 \ + -error 16 -error 17 -error 18 -error 19 -error 20 -error 21 -error 23 -error 24 \ + -error 25 + +# Check that the API we're building hasn't changed from the not-yet-released +# SDK version. +$(eval $(call check-api, \ + uiautomator-checkapi-current, \ + $(uiautomator_api_dir)/current.txt, \ + $(uiautomator_internal_api_file), \ + $(checkapi_current_error_level_flags), \ + cat $(LOCAL_PATH)/apicheck_msg_current.txt, \ + $(uiautomator_stubs_jar), \ + $(uiautomator_stubs_stamp))) + +.PHONY: update-uiautomator-api +update-uiautomator-api: PRIVATE_API_DIR := $(uiautomator_api_dir) +update-uiautomator-api: $(uiautomator_internal_api_file) | $(ACP) + @echo Copying uiautomator current.txt + $(hide) $(ACP) $< $(PRIVATE_API_DIR)/current.txt + +############################################### +# clean up temp vars +uiautomator.core_src_files := +uiautomator.core_java_libraries := +uiautomator_stubs_stamp := +uiautomator_internal_api_file := +uiautomator_stubs_jar := +uiautomator_api_dir := +checkapi_last_error_level_flags := +checkapi_current_error_level_flags := diff --git a/library/apicheck_msg_current.txt b/library/apicheck_msg_current.txt new file mode 100644 index 0000000..989248d --- /dev/null +++ b/library/apicheck_msg_current.txt @@ -0,0 +1,17 @@ + +****************************** +You have tried to change the API from what has been previously approved. + +To make these errors go away, you have two choices: + 1) You can add "@hide" javadoc comments to the methods, etc. listed in the + errors above. + + 2) You can update current.txt by executing the following command: + make update-uiautomator-api + + To submit the revised current.txt to the main Android repository, + you will need approval. +****************************** + + + diff --git a/library/apicheck_msg_last.txt b/library/apicheck_msg_last.txt new file mode 100644 index 0000000..2993157 --- /dev/null +++ b/library/apicheck_msg_last.txt @@ -0,0 +1,7 @@ + +****************************** +You have tried to change the API from what has been previously released in +an SDK. Please fix the errors listed above. +****************************** + + diff --git a/library/core-src/com/android/uiautomator/core/AccessibilityNodeInfoDumper.java b/library/core-src/com/android/uiautomator/core/AccessibilityNodeInfoDumper.java new file mode 100644 index 0000000..63c51e8 --- /dev/null +++ b/library/core-src/com/android/uiautomator/core/AccessibilityNodeInfoDumper.java @@ -0,0 +1,250 @@ +/* + * 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.os.Environment; +import android.os.SystemClock; +import android.util.Log; +import android.util.Xml; +import android.view.accessibility.AccessibilityNodeInfo; + +import org.xmlpull.v1.XmlSerializer; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.StringWriter; + +/** + * + * @hide + */ +public class AccessibilityNodeInfoDumper { + + private static final String LOGTAG = AccessibilityNodeInfoDumper.class.getSimpleName(); + private static final String[] NAF_EXCLUDED_CLASSES = new String[] { + android.widget.GridView.class.getName(), android.widget.GridLayout.class.getName(), + android.widget.ListView.class.getName(), android.widget.TableLayout.class.getName() + }; + + /** + * Using {@link AccessibilityNodeInfo} this method will walk the layout hierarchy + * and generates an xml dump into the /data/local/window_dump.xml + * @param root The root accessibility node. + * @param rotation The rotaion of current display + * @param width The pixel width of current display + * @param height The pixel height of current display + */ + public static void dumpWindowToFile(AccessibilityNodeInfo root, int rotation, + int width, int height) { + File baseDir = new File(Environment.getDataDirectory(), "local"); + if (!baseDir.exists()) { + baseDir.mkdir(); + baseDir.setExecutable(true, false); + baseDir.setWritable(true, false); + baseDir.setReadable(true, false); + } + dumpWindowToFile(root, + new File(new File(Environment.getDataDirectory(), "local"), "window_dump.xml"), + rotation, width, height); + } + + /** + * Using {@link AccessibilityNodeInfo} this method will walk the layout hierarchy + * and generates an xml dump to the location specified by <code>dumpFile</code> + * @param root The root accessibility node. + * @param dumpFile The file to dump to. + * @param rotation The rotaion of current display + * @param width The pixel width of current display + * @param height The pixel height of current display + */ + public static void dumpWindowToFile(AccessibilityNodeInfo root, File dumpFile, int rotation, + int width, int height) { + if (root == null) { + return; + } + final long startTime = SystemClock.uptimeMillis(); + try { + FileWriter writer = new FileWriter(dumpFile); + XmlSerializer serializer = Xml.newSerializer(); + StringWriter stringWriter = new StringWriter(); + serializer.setOutput(stringWriter); + serializer.startDocument("UTF-8", true); + serializer.startTag("", "hierarchy"); + serializer.attribute("", "rotation", Integer.toString(rotation)); + dumpNodeRec(root, serializer, 0, width, height); + serializer.endTag("", "hierarchy"); + serializer.endDocument(); + writer.write(stringWriter.toString()); + writer.close(); + } catch (IOException e) { + Log.e(LOGTAG, "failed to dump window to file", e); + } + final long endTime = SystemClock.uptimeMillis(); + Log.w(LOGTAG, "Fetch time: " + (endTime - startTime) + "ms"); + } + + private static void dumpNodeRec(AccessibilityNodeInfo node, XmlSerializer serializer,int index, + int width, int height) throws IOException { + serializer.startTag("", "node"); + if (!nafExcludedClass(node) && !nafCheck(node)) + serializer.attribute("", "NAF", Boolean.toString(true)); + serializer.attribute("", "index", Integer.toString(index)); + serializer.attribute("", "text", safeCharSeqToString(node.getText())); + serializer.attribute("", "resource-id", safeCharSeqToString(node.getViewIdResourceName())); + serializer.attribute("", "class", safeCharSeqToString(node.getClassName())); + serializer.attribute("", "package", safeCharSeqToString(node.getPackageName())); + serializer.attribute("", "content-desc", safeCharSeqToString(node.getContentDescription())); + serializer.attribute("", "checkable", Boolean.toString(node.isCheckable())); + serializer.attribute("", "checked", Boolean.toString(node.isChecked())); + serializer.attribute("", "clickable", Boolean.toString(node.isClickable())); + serializer.attribute("", "enabled", Boolean.toString(node.isEnabled())); + serializer.attribute("", "focusable", Boolean.toString(node.isFocusable())); + serializer.attribute("", "focused", Boolean.toString(node.isFocused())); + serializer.attribute("", "scrollable", Boolean.toString(node.isScrollable())); + serializer.attribute("", "long-clickable", Boolean.toString(node.isLongClickable())); + serializer.attribute("", "password", Boolean.toString(node.isPassword())); + serializer.attribute("", "selected", Boolean.toString(node.isSelected())); + serializer.attribute("", "bounds", AccessibilityNodeInfoHelper.getVisibleBoundsInScreen( + node, width, height).toShortString()); + int count = node.getChildCount(); + for (int i = 0; i < count; i++) { + AccessibilityNodeInfo child = node.getChild(i); + if (child != null) { + if (child.isVisibleToUser()) { + dumpNodeRec(child, serializer, i, width, height); + child.recycle(); + } else { + Log.i(LOGTAG, String.format("Skipping invisible child: %s", child.toString())); + } + } else { + Log.i(LOGTAG, String.format("Null child %d/%d, parent: %s", + i, count, node.toString())); + } + } + serializer.endTag("", "node"); + } + + /** + * The list of classes to exclude my not be complete. We're attempting to + * only reduce noise from standard layout classes that may be falsely + * configured to accept clicks and are also enabled. + * + * @param node + * @return true if node is excluded. + */ + private static boolean nafExcludedClass(AccessibilityNodeInfo node) { + String className = safeCharSeqToString(node.getClassName()); + for(String excludedClassName : NAF_EXCLUDED_CLASSES) { + if(className.endsWith(excludedClassName)) + return true; + } + return false; + } + + /** + * We're looking for UI controls that are enabled, clickable but have no + * text nor content-description. Such controls configuration indicate an + * interactive control is present in the UI and is most likely not + * accessibility friendly. We refer to such controls here as NAF controls + * (Not Accessibility Friendly) + * + * @param node + * @return false if a node fails the check, true if all is OK + */ + private static boolean nafCheck(AccessibilityNodeInfo node) { + boolean isNaf = node.isClickable() && node.isEnabled() + && safeCharSeqToString(node.getContentDescription()).isEmpty() + && safeCharSeqToString(node.getText()).isEmpty(); + + if (!isNaf) + return true; + + // check children since sometimes the containing element is clickable + // and NAF but a child's text or description is available. Will assume + // such layout as fine. + return childNafCheck(node); + } + + /** + * This should be used when it's already determined that the node is NAF and + * a further check of its children is in order. A node maybe a container + * such as LinerLayout and may be set to be clickable but have no text or + * content description but it is counting on one of its children to fulfill + * the requirement for being accessibility friendly by having one or more of + * its children fill the text or content-description. Such a combination is + * considered by this dumper as acceptable for accessibility. + * + * @param node + * @return false if node fails the check. + */ + private static boolean childNafCheck(AccessibilityNodeInfo node) { + int childCount = node.getChildCount(); + for (int x = 0; x < childCount; x++) { + AccessibilityNodeInfo childNode = node.getChild(x); + + if (!safeCharSeqToString(childNode.getContentDescription()).isEmpty() + || !safeCharSeqToString(childNode.getText()).isEmpty()) + return true; + + if (childNafCheck(childNode)) + return true; + } + return false; + } + + private static String safeCharSeqToString(CharSequence cs) { + if (cs == null) + return ""; + else { + return stripInvalidXMLChars(cs); + } + } + + private static String stripInvalidXMLChars(CharSequence cs) { + StringBuffer ret = new StringBuffer(); + char ch; + /* http://www.w3.org/TR/xml11/#charsets + [#x1-#x8], [#xB-#xC], [#xE-#x1F], [#x7F-#x84], [#x86-#x9F], [#xFDD0-#xFDDF], + [#x1FFFE-#x1FFFF], [#x2FFFE-#x2FFFF], [#x3FFFE-#x3FFFF], + [#x4FFFE-#x4FFFF], [#x5FFFE-#x5FFFF], [#x6FFFE-#x6FFFF], + [#x7FFFE-#x7FFFF], [#x8FFFE-#x8FFFF], [#x9FFFE-#x9FFFF], + [#xAFFFE-#xAFFFF], [#xBFFFE-#xBFFFF], [#xCFFFE-#xCFFFF], + [#xDFFFE-#xDFFFF], [#xEFFFE-#xEFFFF], [#xFFFFE-#xFFFFF], + [#x10FFFE-#x10FFFF]. + */ + for (int i = 0; i < cs.length(); i++) { + ch = cs.charAt(i); + + if((ch >= 0x1 && ch <= 0x8) || (ch >= 0xB && ch <= 0xC) || (ch >= 0xE && ch <= 0x1F) || + (ch >= 0x7F && ch <= 0x84) || (ch >= 0x86 && ch <= 0x9f) || + (ch >= 0xFDD0 && ch <= 0xFDDF) || (ch >= 0x1FFFE && ch <= 0x1FFFF) || + (ch >= 0x2FFFE && ch <= 0x2FFFF) || (ch >= 0x3FFFE && ch <= 0x3FFFF) || + (ch >= 0x4FFFE && ch <= 0x4FFFF) || (ch >= 0x5FFFE && ch <= 0x5FFFF) || + (ch >= 0x6FFFE && ch <= 0x6FFFF) || (ch >= 0x7FFFE && ch <= 0x7FFFF) || + (ch >= 0x8FFFE && ch <= 0x8FFFF) || (ch >= 0x9FFFE && ch <= 0x9FFFF) || + (ch >= 0xAFFFE && ch <= 0xAFFFF) || (ch >= 0xBFFFE && ch <= 0xBFFFF) || + (ch >= 0xCFFFE && ch <= 0xCFFFF) || (ch >= 0xDFFFE && ch <= 0xDFFFF) || + (ch >= 0xEFFFE && ch <= 0xEFFFF) || (ch >= 0xFFFFE && ch <= 0xFFFFF) || + (ch >= 0x10FFFE && ch <= 0x10FFFF)) + ret.append("."); + else + ret.append(ch); + } + return ret.toString(); + } +} diff --git a/library/core-src/com/android/uiautomator/core/AccessibilityNodeInfoHelper.java b/library/core-src/com/android/uiautomator/core/AccessibilityNodeInfoHelper.java new file mode 100644 index 0000000..54835e3 --- /dev/null +++ b/library/core-src/com/android/uiautomator/core/AccessibilityNodeInfoHelper.java @@ -0,0 +1,52 @@ +/* + * 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.view.accessibility.AccessibilityNodeInfo; + +/** + * This class contains static helper methods to work with + * {@link AccessibilityNodeInfo} + */ +class AccessibilityNodeInfoHelper { + + /** + * Returns the node's bounds clipped to the size of the display + * + * @param node + * @param width pixel width of the display + * @param height pixel height of the display + * @return null if node is null, else a Rect containing visible bounds + */ + static Rect getVisibleBoundsInScreen(AccessibilityNodeInfo node, int width, int height) { + if (node == null) { + return null; + } + // targeted node's bounds + Rect nodeRect = new Rect(); + node.getBoundsInScreen(nodeRect); + + Rect displayRect = new Rect(); + displayRect.top = 0; + displayRect.left = 0; + displayRect.right = width; + displayRect.bottom = height; + + nodeRect.intersect(displayRect); + return nodeRect; + } +} diff --git a/library/core-src/com/android/uiautomator/core/Configurator.java b/library/core-src/com/android/uiautomator/core/Configurator.java new file mode 100644 index 0000000..249f404 --- /dev/null +++ b/library/core-src/com/android/uiautomator/core/Configurator.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2013 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; + +/** + * Allows you to set key parameters for running uiautomator tests. The new + * settings take effect immediately and can be changed any time during a test run. + * + * To modify parameters using Configurator, first obtain an instance by calling + * {@link #getInstance()}. As a best practice, make sure you always save + * the original value of any parameter that you are modifying. After running your + * tests with the modified parameters, make sure to also restore + * the original parameter values, otherwise this will impact other tests cases. + * @since API Level 18 + */ +public final class Configurator { + private long mWaitForIdleTimeout = 10 * 1000; + private long mWaitForSelector = 10 * 1000; + private long mWaitForActionAcknowledgment = 3 * 1000; + + // The events for a scroll typically complete even before touchUp occurs. + // This short timeout to make sure we get the very last in cases where the above isn't true. + private long mScrollEventWaitTimeout = 200; // ms + + // Default is inject as fast as we can + private long mKeyInjectionDelay = 0; // ms + + // reference to self + private static Configurator sConfigurator; + + private Configurator() { + /* hide constructor */ + } + + /** + * Retrieves a singleton instance of Configurator. + * + * @return Configurator instance + * @since API Level 18 + */ + public static Configurator getInstance() { + if (sConfigurator == null) { + sConfigurator = new Configurator(); + } + return sConfigurator; + } + + /** + * Sets the timeout for waiting for the user interface to go into an idle + * state before starting a uiautomator action. + * + * By default, all core uiautomator objects except {@link UiDevice} will perform + * this wait before starting to search for the widget specified by the + * object's {@link UiSelector}. Once the idle state is detected or the + * timeout elapses (whichever occurs first), the object will start to wait + * for the selector to find a match. + * See {@link #setWaitForSelectorTimeout(long)} + * + * @param timeout Timeout value in milliseconds + * @return self + * @since API Level 18 + */ + public Configurator setWaitForIdleTimeout(long timeout) { + mWaitForIdleTimeout = timeout; + return this; + } + + /** + * Gets the current timeout used for waiting for the user interface to go + * into an idle state. + * + * By default, all core uiautomator objects except {@link UiDevice} will perform + * this wait before starting to search for the widget specified by the + * object's {@link UiSelector}. Once the idle state is detected or the + * timeout elapses (whichever occurs first), the object will start to wait + * for the selector to find a match. + * See {@link #setWaitForSelectorTimeout(long)} + * + * @return Current timeout value in milliseconds + * @since API Level 18 + */ + public long getWaitForIdleTimeout() { + return mWaitForIdleTimeout; + } + + /** + * Sets the timeout for waiting for a widget to become visible in the user + * interface so that it can be matched by a selector. + * + * Because user interface content is dynamic, sometimes a widget may not + * be visible immediately and won't be detected by a selector. This timeout + * allows the uiautomator framework to wait for a match to be found, up until + * the timeout elapses. + * + * @param timeout Timeout value in milliseconds. + * @return self + * @since API Level 18 + */ + public Configurator setWaitForSelectorTimeout(long timeout) { + mWaitForSelector = timeout; + return this; + } + + /** + * Gets the current timeout for waiting for a widget to become visible in + * the user interface so that it can be matched by a selector. + * + * Because user interface content is dynamic, sometimes a widget may not + * be visible immediately and won't be detected by a selector. This timeout + * allows the uiautomator framework to wait for a match to be found, up until + * the timeout elapses. + * + * @return Current timeout value in milliseconds + * @since API Level 18 + */ + public long getWaitForSelectorTimeout() { + return mWaitForSelector; + } + + /** + * Sets the timeout for waiting for an acknowledgement of an + * uiautomtor scroll swipe action. + * + * The acknowledgment is an <a href="http://developer.android.com/reference/android/view/accessibility/AccessibilityEvent.html">AccessibilityEvent</a>, + * corresponding to the scroll action, that lets the framework determine if + * the scroll action was successful. Generally, this timeout should not be modified. + * See {@link UiScrollable} + * + * @param timeout Timeout value in milliseconds + * @return self + * @since API Level 18 + */ + public Configurator setScrollAcknowledgmentTimeout(long timeout) { + mScrollEventWaitTimeout = timeout; + return this; + } + + /** + * Gets the timeout for waiting for an acknowledgement of an + * uiautomtor scroll swipe action. + * + * The acknowledgment is an <a href="http://developer.android.com/reference/android/view/accessibility/AccessibilityEvent.html">AccessibilityEvent</a>, + * corresponding to the scroll action, that lets the framework determine if + * the scroll action was successful. Generally, this timeout should not be modified. + * See {@link UiScrollable} + * + * @return current timeout in milliseconds + * @since API Level 18 + */ + public long getScrollAcknowledgmentTimeout() { + return mScrollEventWaitTimeout; + } + + /** + * Sets the timeout for waiting for an acknowledgment of generic uiautomator + * actions, such as clicks, text setting, and menu presses. + * + * The acknowledgment is an <a href="http://developer.android.com/reference/android/view/accessibility/AccessibilityEvent.html">AccessibilityEvent</a>, + * corresponding to an action, that lets the framework determine if the + * action was successful. Generally, this timeout should not be modified. + * See {@link UiObject} + * + * @param timeout Timeout value in milliseconds + * @return self + * @since API Level 18 + */ + public Configurator setActionAcknowledgmentTimeout(long timeout) { + mWaitForActionAcknowledgment = timeout; + return this; + } + + /** + * Gets the current timeout for waiting for an acknowledgment of generic + * uiautomator actions, such as clicks, text setting, and menu presses. + * + * The acknowledgment is an <a href="http://developer.android.com/reference/android/view/accessibility/AccessibilityEvent.html">AccessibilityEvent</a>, + * corresponding to an action, that lets the framework determine if the + * action was successful. Generally, this timeout should not be modified. + * See {@link UiObject} + * + * @return current timeout in milliseconds + * @since API Level 18 + */ + public long getActionAcknowledgmentTimeout() { + return mWaitForActionAcknowledgment; + } + + /** + * Sets a delay between key presses when injecting text input. + * See {@link UiObject#setText(String)} + * + * @param delay Delay value in milliseconds + * @return self + * @since API Level 18 + */ + public Configurator setKeyInjectionDelay(long delay) { + mKeyInjectionDelay = delay; + return this; + } + + /** + * Gets the current delay between key presses when injecting text input. + * See {@link UiObject#setText(String)} + * + * @return current delay in milliseconds + * @since API Level 18 + */ + public long getKeyInjectionDelay() { + return mKeyInjectionDelay; + } +} diff --git a/library/core-src/com/android/uiautomator/core/InteractionController.java b/library/core-src/com/android/uiautomator/core/InteractionController.java new file mode 100644 index 0000000..73e46f1 --- /dev/null +++ b/library/core-src/com/android/uiautomator/core/InteractionController.java @@ -0,0 +1,795 @@ +/* + * 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.accessibilityservice.AccessibilityService; +import android.app.UiAutomation; +import android.app.UiAutomation.AccessibilityEventFilter; +import android.graphics.Point; +import android.os.RemoteException; +import android.os.SystemClock; +import android.util.Log; +import android.view.InputDevice; +import android.view.InputEvent; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.MotionEvent.PointerCoords; +import android.view.MotionEvent.PointerProperties; +import android.view.accessibility.AccessibilityEvent; + +import com.android.internal.util.Predicate; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeoutException; + +/** + * The InteractionProvider is responsible for injecting user events such as touch events + * (includes swipes) and text key events into the system. To do so, all it needs to know about + * are coordinates of the touch events and text for the text input events. + * The InteractionController performs no synchronization. It will fire touch and text input events + * as fast as it receives them. All idle synchronization is performed prior to querying the + * hierarchy. See {@link QueryController} + */ +class InteractionController { + + private static final String LOG_TAG = InteractionController.class.getSimpleName(); + + private static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG); + + private final KeyCharacterMap mKeyCharacterMap = + KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); + + private final UiAutomatorBridge mUiAutomatorBridge; + + private static final long REGULAR_CLICK_LENGTH = 100; + + private long mDownTime; + + // Inserted after each motion event injection. + private static final int MOTION_EVENT_INJECTION_DELAY_MILLIS = 5; + + public InteractionController(UiAutomatorBridge bridge) { + mUiAutomatorBridge = bridge; + } + + /** + * Predicate for waiting for any of the events specified in the mask + */ + class WaitForAnyEventPredicate implements AccessibilityEventFilter { + int mMask; + WaitForAnyEventPredicate(int mask) { + mMask = mask; + } + @Override + public boolean accept(AccessibilityEvent t) { + // check current event in the list + if ((t.getEventType() & mMask) != 0) { + return true; + } + + // no match yet + return false; + } + } + + /** + * Predicate for waiting for all the events specified in the mask and populating + * a ctor passed list with matching events. User of this Predicate must recycle + * all populated events in the events list. + */ + class EventCollectingPredicate implements AccessibilityEventFilter { + int mMask; + List<AccessibilityEvent> mEventsList; + + EventCollectingPredicate(int mask, List<AccessibilityEvent> events) { + mMask = mask; + mEventsList = events; + } + + @Override + public boolean accept(AccessibilityEvent t) { + // check current event in the list + if ((t.getEventType() & mMask) != 0) { + // For the events you need, always store a copy when returning false from + // predicates since the original will automatically be recycled after the call. + mEventsList.add(AccessibilityEvent.obtain(t)); + } + + // get more + return false; + } + } + + /** + * Predicate for waiting for every event specified in the mask to be matched at least once + */ + class WaitForAllEventPredicate implements AccessibilityEventFilter { + int mMask; + WaitForAllEventPredicate(int mask) { + mMask = mask; + } + + @Override + public boolean accept(AccessibilityEvent t) { + // check current event in the list + if ((t.getEventType() & mMask) != 0) { + // remove from mask since this condition is satisfied + mMask &= ~t.getEventType(); + + // Since we're waiting for all events to be matched at least once + if (mMask != 0) + return false; + + // all matched + return true; + } + + // no match yet + return false; + } + } + + /** + * Helper used by methods to perform actions and wait for any accessibility events and return + * predicated on predefined filter. + * + * @param command + * @param filter + * @param timeout + * @return + */ + private AccessibilityEvent runAndWaitForEvents(Runnable command, + AccessibilityEventFilter filter, long timeout) { + + try { + return mUiAutomatorBridge.executeCommandAndWaitForAccessibilityEvent(command, filter, + timeout); + } catch (TimeoutException e) { + Log.w(LOG_TAG, "runAndwaitForEvent timedout waiting for events"); + return null; + } catch (Exception e) { + Log.e(LOG_TAG, "exception from executeCommandAndWaitForAccessibilityEvent", e); + return null; + } + } + + /** + * Send keys and blocks until the first specified accessibility event. + * + * Most key presses will cause some UI change to occur. If the device is busy, this will + * block until the device begins to process the key press at which point the call returns + * and normal wait for idle processing may begin. If no events are detected for the + * timeout period specified, the call will return anyway with false. + * + * @param keyCode + * @param metaState + * @param eventType + * @param timeout + * @return true if events is received, otherwise false. + */ + public boolean sendKeyAndWaitForEvent(final int keyCode, final int metaState, + final int eventType, long timeout) { + Runnable command = new Runnable() { + @Override + public void run() { + final long eventTime = SystemClock.uptimeMillis(); + KeyEvent downEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, + keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, + InputDevice.SOURCE_KEYBOARD); + if (injectEventSync(downEvent)) { + KeyEvent upEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, + keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, + InputDevice.SOURCE_KEYBOARD); + injectEventSync(upEvent); + } + } + }; + + return runAndWaitForEvents(command, new WaitForAnyEventPredicate(eventType), timeout) + != null; + } + + /** + * Clicks at coordinates without waiting for device idle. This may be used for operations + * that require stressing the target. + * @param x + * @param y + * @return true if the click executed successfully + */ + public boolean clickNoSync(int x, int y) { + Log.d(LOG_TAG, "clickNoSync (" + x + ", " + y + ")"); + + if (touchDown(x, y)) { + SystemClock.sleep(REGULAR_CLICK_LENGTH); + if (touchUp(x, y)) + return true; + } + return false; + } + + /** + * Click at coordinates and blocks until either accessibility event TYPE_WINDOW_CONTENT_CHANGED + * or TYPE_VIEW_SELECTED are received. + * + * @param x + * @param y + * @param timeout waiting for event + * @return true if events are received, else false if timeout. + */ + public boolean clickAndSync(final int x, final int y, long timeout) { + + String logString = String.format("clickAndSync(%d, %d)", x, y); + Log.d(LOG_TAG, logString); + + return runAndWaitForEvents(clickRunnable(x, y), new WaitForAnyEventPredicate( + AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED | + AccessibilityEvent.TYPE_VIEW_SELECTED), timeout) != null; + } + + /** + * Clicks at coordinates and waits for for a TYPE_WINDOW_STATE_CHANGED event followed + * by TYPE_WINDOW_CONTENT_CHANGED. If timeout occurs waiting for TYPE_WINDOW_STATE_CHANGED, + * no further waits will be performed and the function returns. + * @param x + * @param y + * @param timeout waiting for event + * @return true if both events occurred in the expected order + */ + public boolean clickAndWaitForNewWindow(final int x, final int y, long timeout) { + String logString = String.format("clickAndWaitForNewWindow(%d, %d)", x, y); + Log.d(LOG_TAG, logString); + + return runAndWaitForEvents(clickRunnable(x, y), new WaitForAllEventPredicate( + AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED | + AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED), timeout) != null; + } + + /** + * Returns a Runnable for use in {@link #runAndWaitForEvents(Runnable, Predicate, long) to + * perform a click. + * + * @param x coordinate + * @param y coordinate + * @return Runnable + */ + private Runnable clickRunnable(final int x, final int y) { + return new Runnable() { + @Override + public void run() { + if(touchDown(x, y)) { + SystemClock.sleep(REGULAR_CLICK_LENGTH); + touchUp(x, y); + } + } + }; + } + + /** + * Touches down for a long press at the specified coordinates. + * + * @param x + * @param y + * @return true if successful. + */ + public boolean longTapNoSync(int x, int y) { + if (DEBUG) { + Log.d(LOG_TAG, "longTapNoSync (" + x + ", " + y + ")"); + } + + if (touchDown(x, y)) { + SystemClock.sleep(mUiAutomatorBridge.getSystemLongPressTime()); + if(touchUp(x, y)) { + return true; + } + } + return false; + } + + private boolean touchDown(int x, int y) { + if (DEBUG) { + Log.d(LOG_TAG, "touchDown (" + x + ", " + y + ")"); + } + mDownTime = SystemClock.uptimeMillis(); + MotionEvent event = MotionEvent.obtain( + mDownTime, mDownTime, MotionEvent.ACTION_DOWN, x, y, 1); + event.setSource(InputDevice.SOURCE_TOUCHSCREEN); + return injectEventSync(event); + } + + private boolean touchUp(int x, int y) { + if (DEBUG) { + Log.d(LOG_TAG, "touchUp (" + x + ", " + y + ")"); + } + final long eventTime = SystemClock.uptimeMillis(); + MotionEvent event = MotionEvent.obtain( + mDownTime, eventTime, MotionEvent.ACTION_UP, x, y, 1); + event.setSource(InputDevice.SOURCE_TOUCHSCREEN); + mDownTime = 0; + return injectEventSync(event); + } + + private boolean touchMove(int x, int y) { + if (DEBUG) { + Log.d(LOG_TAG, "touchMove (" + x + ", " + y + ")"); + } + final long eventTime = SystemClock.uptimeMillis(); + MotionEvent event = MotionEvent.obtain( + mDownTime, eventTime, MotionEvent.ACTION_MOVE, x, y, 1); + event.setSource(InputDevice.SOURCE_TOUCHSCREEN); + return injectEventSync(event); + } + + /** + * Handle swipes in any direction where the result is a scroll event. This call blocks + * until the UI has fired a scroll event or timeout. + * @param downX + * @param downY + * @param upX + * @param upY + * @param steps + * @return true if we are not at the beginning or end of the scrollable view. + */ + public boolean scrollSwipe(final int downX, final int downY, final int upX, final int upY, + final int steps) { + Log.d(LOG_TAG, "scrollSwipe (" + downX + ", " + downY + ", " + upX + ", " + + upY + ", " + steps +")"); + + Runnable command = new Runnable() { + @Override + public void run() { + swipe(downX, downY, upX, upY, steps); + } + }; + + // Collect all accessibility events generated during the swipe command and get the + // last event + ArrayList<AccessibilityEvent> events = new ArrayList<AccessibilityEvent>(); + runAndWaitForEvents(command, + new EventCollectingPredicate(AccessibilityEvent.TYPE_VIEW_SCROLLED, events), + Configurator.getInstance().getScrollAcknowledgmentTimeout()); + + AccessibilityEvent event = getLastMatchingEvent(events, + AccessibilityEvent.TYPE_VIEW_SCROLLED); + + if (event == null) { + // end of scroll since no new scroll events received + recycleAccessibilityEvents(events); + return false; + } + + // AdapterViews have indices we can use to check for the beginning. + boolean foundEnd = false; + if (event.getFromIndex() != -1 && event.getToIndex() != -1 && event.getItemCount() != -1) { + foundEnd = event.getFromIndex() == 0 || + (event.getItemCount() - 1) == event.getToIndex(); + Log.d(LOG_TAG, "scrollSwipe reached scroll end: " + foundEnd); + } else if (event.getScrollX() != -1 && event.getScrollY() != -1) { + // Determine if we are scrolling vertically or horizontally. + if (downX == upX) { + // Vertical + foundEnd = event.getScrollY() == 0 || + event.getScrollY() == event.getMaxScrollY(); + Log.d(LOG_TAG, "Vertical scrollSwipe reached scroll end: " + foundEnd); + } else if (downY == upY) { + // Horizontal + foundEnd = event.getScrollX() == 0 || + event.getScrollX() == event.getMaxScrollX(); + Log.d(LOG_TAG, "Horizontal scrollSwipe reached scroll end: " + foundEnd); + } + } + recycleAccessibilityEvents(events); + return !foundEnd; + } + + private AccessibilityEvent getLastMatchingEvent(List<AccessibilityEvent> events, int type) { + for (int x = events.size(); x > 0; x--) { + AccessibilityEvent event = events.get(x - 1); + if (event.getEventType() == type) + return event; + } + return null; + } + + private void recycleAccessibilityEvents(List<AccessibilityEvent> events) { + for (AccessibilityEvent event : events) + event.recycle(); + events.clear(); + } + + /** + * Handle swipes in any direction. + * @param downX + * @param downY + * @param upX + * @param upY + * @param steps + * @return true if the swipe executed successfully + */ + public boolean swipe(int downX, int downY, int upX, int upY, int steps) { + return swipe(downX, downY, upX, upY, steps, false /*drag*/); + } + + /** + * Handle swipes/drags in any direction. + * @param downX + * @param downY + * @param upX + * @param upY + * @param steps + * @param drag when true, the swipe becomes a drag swipe + * @return true if the swipe executed successfully + */ + public boolean swipe(int downX, int downY, int upX, int upY, int steps, boolean drag) { + boolean ret = false; + int swipeSteps = steps; + double xStep = 0; + double yStep = 0; + + // avoid a divide by zero + if(swipeSteps == 0) + swipeSteps = 1; + + xStep = ((double)(upX - downX)) / swipeSteps; + yStep = ((double)(upY - downY)) / swipeSteps; + + // first touch starts exactly at the point requested + ret = touchDown(downX, downY); + if (drag) + SystemClock.sleep(mUiAutomatorBridge.getSystemLongPressTime()); + for(int i = 1; i < swipeSteps; i++) { + ret &= touchMove(downX + (int)(xStep * i), downY + (int)(yStep * i)); + if(ret == false) + break; + // set some known constant delay between steps as without it this + // become completely dependent on the speed of the system and results + // may vary on different devices. This guarantees at minimum we have + // a preset delay. + SystemClock.sleep(MOTION_EVENT_INJECTION_DELAY_MILLIS); + } + if (drag) + SystemClock.sleep(REGULAR_CLICK_LENGTH); + ret &= touchUp(upX, upY); + return(ret); + } + + /** + * Performs a swipe between points in the Point array. + * @param segments is Point array containing at least one Point object + * @param segmentSteps steps to inject between two Points + * @return true on success + */ + public boolean swipe(Point[] segments, int segmentSteps) { + boolean ret = false; + int swipeSteps = segmentSteps; + double xStep = 0; + double yStep = 0; + + // avoid a divide by zero + if(segmentSteps == 0) + segmentSteps = 1; + + // must have some points + if(segments.length == 0) + return false; + + // first touch starts exactly at the point requested + ret = touchDown(segments[0].x, segments[0].y); + for(int seg = 0; seg < segments.length; seg++) { + if(seg + 1 < segments.length) { + + xStep = ((double)(segments[seg+1].x - segments[seg].x)) / segmentSteps; + yStep = ((double)(segments[seg+1].y - segments[seg].y)) / segmentSteps; + + for(int i = 1; i < swipeSteps; i++) { + ret &= touchMove(segments[seg].x + (int)(xStep * i), + segments[seg].y + (int)(yStep * i)); + if(ret == false) + break; + // set some known constant delay between steps as without it this + // become completely dependent on the speed of the system and results + // may vary on different devices. This guarantees at minimum we have + // a preset delay. + SystemClock.sleep(MOTION_EVENT_INJECTION_DELAY_MILLIS); + } + } + } + ret &= touchUp(segments[segments.length - 1].x, segments[segments.length -1].y); + return(ret); + } + + + public boolean sendText(String text) { + if (DEBUG) { + Log.d(LOG_TAG, "sendText (" + text + ")"); + } + + KeyEvent[] events = mKeyCharacterMap.getEvents(text.toCharArray()); + + if (events != null) { + long keyDelay = Configurator.getInstance().getKeyInjectionDelay(); + for (KeyEvent event2 : events) { + // We have to change the time of an event before injecting it because + // all KeyEvents returned by KeyCharacterMap.getEvents() have the same + // time stamp and the system rejects too old events. Hence, it is + // possible for an event to become stale before it is injected if it + // takes too long to inject the preceding ones. + KeyEvent event = KeyEvent.changeTimeRepeat(event2, + SystemClock.uptimeMillis(), 0); + if (!injectEventSync(event)) { + return false; + } + SystemClock.sleep(keyDelay); + } + } + return true; + } + + public boolean sendKey(int keyCode, int metaState) { + if (DEBUG) { + Log.d(LOG_TAG, "sendKey (" + keyCode + ", " + metaState + ")"); + } + + final long eventTime = SystemClock.uptimeMillis(); + KeyEvent downEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, + keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, + InputDevice.SOURCE_KEYBOARD); + if (injectEventSync(downEvent)) { + KeyEvent upEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, + keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, + InputDevice.SOURCE_KEYBOARD); + if(injectEventSync(upEvent)) { + return true; + } + } + return false; + } + + /** + * Rotates right and also freezes rotation in that position by + * disabling the sensors. If you want to un-freeze the rotation + * and re-enable the sensors see {@link #unfreezeRotation()}. Note + * that doing so may cause the screen contents to rotate + * depending on the current physical position of the test device. + * @throws RemoteException + */ + public void setRotationRight() { + mUiAutomatorBridge.setRotation(UiAutomation.ROTATION_FREEZE_270); + } + + /** + * Rotates left and also freezes rotation in that position by + * disabling the sensors. If you want to un-freeze the rotation + * and re-enable the sensors see {@link #unfreezeRotation()}. Note + * that doing so may cause the screen contents to rotate + * depending on the current physical position of the test device. + * @throws RemoteException + */ + public void setRotationLeft() { + mUiAutomatorBridge.setRotation(UiAutomation.ROTATION_FREEZE_90); + } + + /** + * Rotates up and also freezes rotation in that position by + * disabling the sensors. If you want to un-freeze the rotation + * and re-enable the sensors see {@link #unfreezeRotation()}. Note + * that doing so may cause the screen contents to rotate + * depending on the current physical position of the test device. + * @throws RemoteException + */ + public void setRotationNatural() { + mUiAutomatorBridge.setRotation(UiAutomation.ROTATION_FREEZE_0); + } + + /** + * Disables the sensors and freezes the device rotation at its + * current rotation state. + * @throws RemoteException + */ + public void freezeRotation() { + mUiAutomatorBridge.setRotation(UiAutomation.ROTATION_FREEZE_CURRENT); + } + + /** + * Re-enables the sensors and un-freezes the device rotation + * allowing its contents to rotate with the device physical rotation. + * @throws RemoteException + */ + public void unfreezeRotation() { + mUiAutomatorBridge.setRotation(UiAutomation.ROTATION_UNFREEZE); + } + + /** + * This method simply presses the power button if the screen is OFF else + * it does nothing if the screen is already ON. + * @return true if the device was asleep else false + * @throws RemoteException + */ + public boolean wakeDevice() throws RemoteException { + if(!isScreenOn()) { + sendKey(KeyEvent.KEYCODE_POWER, 0); + return true; + } + return false; + } + + /** + * This method simply presses the power button if the screen is ON else + * it does nothing if the screen is already OFF. + * @return true if the device was awake else false + * @throws RemoteException + */ + public boolean sleepDevice() throws RemoteException { + if(isScreenOn()) { + this.sendKey(KeyEvent.KEYCODE_POWER, 0); + return true; + } + return false; + } + + /** + * Checks the power manager if the screen is ON + * @return true if the screen is ON else false + * @throws RemoteException + */ + public boolean isScreenOn() throws RemoteException { + return mUiAutomatorBridge.isScreenOn(); + } + + private boolean injectEventSync(InputEvent event) { + return mUiAutomatorBridge.injectInputEvent(event, true); + } + + private int getPointerAction(int motionEnvent, int index) { + return motionEnvent + (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT); + } + + /** + * Performs a multi-touch gesture + * + * Takes a series of touch coordinates for at least 2 pointers. Each pointer must have + * all of its touch steps defined in an array of {@link PointerCoords}. By having the ability + * to specify the touch points along the path of a pointer, the caller is able to specify + * complex gestures like circles, irregular shapes etc, where each pointer may take a + * different path. + * + * To create a single point on a pointer's touch path + * <code> + * PointerCoords p = new PointerCoords(); + * p.x = stepX; + * p.y = stepY; + * p.pressure = 1; + * p.size = 1; + * </code> + * @param touches each array of {@link PointerCoords} constitute a single pointer's touch path. + * Multiple {@link PointerCoords} arrays constitute multiple pointers, each with its own + * path. Each {@link PointerCoords} in an array constitute a point on a pointer's path. + * @return <code>true</code> if all points on all paths are injected successfully, <code>false + * </code>otherwise + * @since API Level 18 + */ + public boolean performMultiPointerGesture(PointerCoords[] ... touches) { + boolean ret = true; + if (touches.length < 2) { + throw new IllegalArgumentException("Must provide coordinates for at least 2 pointers"); + } + + // Get the pointer with the max steps to inject. + int maxSteps = 0; + for (int x = 0; x < touches.length; x++) + maxSteps = (maxSteps < touches[x].length) ? touches[x].length : maxSteps; + + // specify the properties for each pointer as finger touch + PointerProperties[] properties = new PointerProperties[touches.length]; + PointerCoords[] pointerCoords = new PointerCoords[touches.length]; + for (int x = 0; x < touches.length; x++) { + PointerProperties prop = new PointerProperties(); + prop.id = x; + prop.toolType = MotionEvent.TOOL_TYPE_FINGER; + properties[x] = prop; + + // for each pointer set the first coordinates for touch down + pointerCoords[x] = touches[x][0]; + } + + // Touch down all pointers + long downTime = SystemClock.uptimeMillis(); + MotionEvent event; + event = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, 1, + properties, pointerCoords, 0, 0, 1, 1, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0); + ret &= injectEventSync(event); + + for (int x = 1; x < touches.length; x++) { + event = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(), + getPointerAction(MotionEvent.ACTION_POINTER_DOWN, x), x + 1, properties, + pointerCoords, 0, 0, 1, 1, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0); + ret &= injectEventSync(event); + } + + // Move all pointers + for (int i = 1; i < maxSteps - 1; i++) { + // for each pointer + for (int x = 0; x < touches.length; x++) { + // check if it has coordinates to move + if (touches[x].length > i) + pointerCoords[x] = touches[x][i]; + else + pointerCoords[x] = touches[x][touches[x].length - 1]; + } + + event = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(), + MotionEvent.ACTION_MOVE, touches.length, properties, pointerCoords, 0, 0, 1, 1, + 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0); + + ret &= injectEventSync(event); + SystemClock.sleep(MOTION_EVENT_INJECTION_DELAY_MILLIS); + } + + // For each pointer get the last coordinates + for (int x = 0; x < touches.length; x++) + pointerCoords[x] = touches[x][touches[x].length - 1]; + + // touch up + for (int x = 1; x < touches.length; x++) { + event = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(), + getPointerAction(MotionEvent.ACTION_POINTER_UP, x), x + 1, properties, + pointerCoords, 0, 0, 1, 1, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0); + ret &= injectEventSync(event); + } + + Log.i(LOG_TAG, "x " + pointerCoords[0].x); + // first to touch down is last up + event = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, 1, + properties, pointerCoords, 0, 0, 1, 1, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0); + ret &= injectEventSync(event); + return ret; + } + + /** + * Simulates a short press on the Recent Apps button. + * + * @return true if successful, else return false + * @since API Level 18 + */ + public boolean toggleRecentApps() { + return mUiAutomatorBridge.performGlobalAction( + AccessibilityService.GLOBAL_ACTION_RECENTS); + } + + /** + * Opens the notification shade + * + * @return true if successful, else return false + * @since API Level 18 + */ + public boolean openNotification() { + return mUiAutomatorBridge.performGlobalAction( + AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS); + } + + /** + * Opens the quick settings shade + * + * @return true if successful, else return false + * @since API Level 18 + */ + public boolean openQuickSettings() { + return mUiAutomatorBridge.performGlobalAction( + AccessibilityService.GLOBAL_ACTION_QUICK_SETTINGS); + } +} diff --git a/library/core-src/com/android/uiautomator/core/QueryController.java b/library/core-src/com/android/uiautomator/core/QueryController.java new file mode 100644 index 0000000..6931528 --- /dev/null +++ b/library/core-src/com/android/uiautomator/core/QueryController.java @@ -0,0 +1,521 @@ +/* + * 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.app.UiAutomation.OnAccessibilityEventListener; +import android.os.SystemClock; +import android.util.Log; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; + + +/** + * The QueryController main purpose is to translate a {@link UiSelector} selectors to + * {@link AccessibilityNodeInfo}. This is all this controller does. + */ +class QueryController { + + private static final String LOG_TAG = QueryController.class.getSimpleName(); + + private static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG); + private static final boolean VERBOSE = Log.isLoggable(LOG_TAG, Log.VERBOSE); + + private final UiAutomatorBridge mUiAutomatorBridge; + + private final Object mLock = new Object(); + + private String mLastActivityName = null; + + // During a pattern selector search, the recursive pattern search + // methods will track their counts and indexes here. + private int mPatternCounter = 0; + private int mPatternIndexer = 0; + + // These help show each selector's search context as it relates to the previous sub selector + // matched. When a compound selector fails, it is hard to tell which part of it is failing. + // Seeing how a selector is being parsed and which sub selector failed within a long list + // of compound selectors is very helpful. + private int mLogIndent = 0; + private int mLogParentIndent = 0; + + private String mLastTraversedText = ""; + + public QueryController(UiAutomatorBridge bridge) { + mUiAutomatorBridge = bridge; + bridge.setOnAccessibilityEventListener(new OnAccessibilityEventListener() { + @Override + public void onAccessibilityEvent(AccessibilityEvent event) { + synchronized (mLock) { + switch(event.getEventType()) { + case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: + // don't trust event.getText(), check for nulls + if (event.getText() != null && event.getText().size() > 0) { + if(event.getText().get(0) != null) + mLastActivityName = event.getText().get(0).toString(); + } + break; + case AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY: + // don't trust event.getText(), check for nulls + if (event.getText() != null && event.getText().size() > 0) + if(event.getText().get(0) != null) + mLastTraversedText = event.getText().get(0).toString(); + if (DEBUG) + Log.d(LOG_TAG, "Last text selection reported: " + + mLastTraversedText); + break; + } + mLock.notifyAll(); + } + } + }); + } + + /** + * Returns the last text selection reported by accessibility + * event TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY. One way to cause + * this event is using a DPad arrows to focus on UI elements. + */ + public String getLastTraversedText() { + mUiAutomatorBridge.waitForIdle(); + synchronized (mLock) { + if (mLastTraversedText.length() > 0) { + return mLastTraversedText; + } + } + return null; + } + + /** + * Clears the last text selection value saved from the TYPE_VIEW_TEXT_SELECTION_CHANGED + * event + */ + public void clearLastTraversedText() { + mUiAutomatorBridge.waitForIdle(); + synchronized (mLock) { + mLastTraversedText = ""; + } + } + + private void initializeNewSearch() { + mPatternCounter = 0; + mPatternIndexer = 0; + mLogIndent = 0; + mLogParentIndent = 0; + } + + /** + * Counts the instances of the selector group. The selector must be in the following + * format: [container_selector, PATTERN=[INSTANCE=x, PATTERN=[the_pattern]] + * where the container_selector is used to find the containment region to search for patterns + * and the INSTANCE=x is the instance of the_pattern to return. + * @param selector + * @return number of pattern matches. Returns 0 for all other cases. + */ + public int getPatternCount(UiSelector selector) { + findAccessibilityNodeInfo(selector, true /*counting*/); + return mPatternCounter; + } + + /** + * Main search method for translating By selectors to AccessibilityInfoNodes + * @param selector + * @return AccessibilityNodeInfo + */ + public AccessibilityNodeInfo findAccessibilityNodeInfo(UiSelector selector) { + return findAccessibilityNodeInfo(selector, false); + } + + protected AccessibilityNodeInfo findAccessibilityNodeInfo(UiSelector selector, + boolean isCounting) { + mUiAutomatorBridge.waitForIdle(); + initializeNewSearch(); + + if (DEBUG) + Log.d(LOG_TAG, "Searching: " + selector); + + synchronized (mLock) { + AccessibilityNodeInfo rootNode = getRootNode(); + if (rootNode == null) { + Log.e(LOG_TAG, "Cannot proceed when root node is null. Aborted search"); + return null; + } + + // Copy so that we don't modify the original's sub selectors + UiSelector uiSelector = new UiSelector(selector); + return translateCompoundSelector(uiSelector, rootNode, isCounting); + } + } + + /** + * Gets the root node from accessibility and if it fails to get one it will + * retry every 250ms for up to 1000ms. + * @return null if no root node is obtained + */ + protected AccessibilityNodeInfo getRootNode() { + final int maxRetry = 4; + final long waitInterval = 250; + AccessibilityNodeInfo rootNode = null; + for(int x = 0; x < maxRetry; x++) { + rootNode = mUiAutomatorBridge.getRootInActiveWindow(); + if (rootNode != null) { + return rootNode; + } + if(x < maxRetry - 1) { + Log.e(LOG_TAG, "Got null root node from accessibility - Retrying..."); + SystemClock.sleep(waitInterval); + } + } + return rootNode; + } + + /** + * A compoundSelector encapsulate both Regular and Pattern selectors. The formats follows: + * <p/> + * regular_selector = By[attributes... CHILD=By[attributes... CHILD=By[....]]] + * <br/> + * pattern_selector = ...CONTAINER=By[..] PATTERN=By[instance=x PATTERN=[regular_selector] + * <br/> + * compound_selector = [regular_selector [pattern_selector]] + * <p/> + * regular_selectors are the most common form of selectors and the search for them + * is straightforward. On the other hand pattern_selectors requires search to be + * performed as in regular_selector but where regular_selector search returns immediately + * upon a successful match, the search for pattern_selector continues until the + * requested matched _instance_ of that pattern is matched. + * <p/> + * Counting UI objects requires using pattern_selectors. The counting search is the same + * as a pattern_search however we're not looking to match an instance of the pattern but + * rather continuously walking the accessibility node hierarchy while counting matched + * patterns, until the end of the tree. + * <p/> + * If both present, order of parsing begins with CONTAINER followed by PATTERN then the + * top most selector is processed as regular_selector within the context of the previous + * CONTAINER and its PATTERN information. If neither is present then the top selector is + * directly treated as regular_selector. So the presence of a CONTAINER and PATTERN within + * a selector simply dictates that the selector matching will be constraint to the sub tree + * node where the CONTAINER and its child PATTERN have identified. + * @param selector + * @param fromNode + * @param isCounting + * @return AccessibilityNodeInfo + */ + private AccessibilityNodeInfo translateCompoundSelector(UiSelector selector, + AccessibilityNodeInfo fromNode, boolean isCounting) { + + // Start translating compound selectors by translating the regular_selector first + // The regular_selector is then used as a container for any optional pattern_selectors + // that may or may not be specified. + if(selector.hasContainerSelector()) + // nested pattern selectors + if(selector.getContainerSelector().hasContainerSelector()) { + fromNode = translateCompoundSelector( + selector.getContainerSelector(), fromNode, false); + initializeNewSearch(); + } else + fromNode = translateReqularSelector(selector.getContainerSelector(), fromNode); + else + fromNode = translateReqularSelector(selector, fromNode); + + if(fromNode == null) { + if (DEBUG) + Log.d(LOG_TAG, "Container selector not found: " + selector.dumpToString(false)); + return null; + } + + if(selector.hasPatternSelector()) { + fromNode = translatePatternSelector(selector.getPatternSelector(), + fromNode, isCounting); + + if (isCounting) { + Log.i(LOG_TAG, String.format( + "Counted %d instances of: %s", mPatternCounter, selector)); + return null; + } else { + if(fromNode == null) { + if (DEBUG) + Log.d(LOG_TAG, "Pattern selector not found: " + + selector.dumpToString(false)); + return null; + } + } + } + + // translate any additions to the selector that may have been added by tests + // with getChild(By selector) after a container and pattern selectors + if(selector.hasContainerSelector() || selector.hasPatternSelector()) { + if(selector.hasChildSelector() || selector.hasParentSelector()) + fromNode = translateReqularSelector(selector, fromNode); + } + + if(fromNode == null) { + if (DEBUG) + Log.d(LOG_TAG, "Object Not Found for selector " + selector); + return null; + } + Log.i(LOG_TAG, String.format("Matched selector: %s <<==>> [%s]", selector, fromNode)); + return fromNode; + } + + /** + * Used by the {@link #translateCompoundSelector(UiSelector, AccessibilityNodeInfo, boolean)} + * to translate the regular_selector portion. It has the following format: + * <p/> + * regular_selector = By[attributes... CHILD=By[attributes... CHILD=By[....]]]<br/> + * <p/> + * regular_selectors are the most common form of selectors and the search for them + * is straightforward. This method will only look for CHILD or PARENT sub selectors. + * <p/> + * @param selector + * @param fromNode + * @return AccessibilityNodeInfo if found else null + */ + private AccessibilityNodeInfo translateReqularSelector(UiSelector selector, + AccessibilityNodeInfo fromNode) { + + return findNodeRegularRecursive(selector, fromNode, 0); + } + + private AccessibilityNodeInfo findNodeRegularRecursive(UiSelector subSelector, + AccessibilityNodeInfo fromNode, int index) { + + if (subSelector.isMatchFor(fromNode, index)) { + if (DEBUG) { + Log.d(LOG_TAG, formatLog(String.format("%s", + subSelector.dumpToString(false)))); + } + if(subSelector.isLeaf()) { + return fromNode; + } + if(subSelector.hasChildSelector()) { + mLogIndent++; // next selector + subSelector = subSelector.getChildSelector(); + if(subSelector == null) { + Log.e(LOG_TAG, "Error: A child selector without content"); + return null; // there is an implementation fault + } + } else if(subSelector.hasParentSelector()) { + mLogIndent++; // next selector + subSelector = subSelector.getParentSelector(); + if(subSelector == null) { + Log.e(LOG_TAG, "Error: A parent selector without content"); + return null; // there is an implementation fault + } + // the selector requested we start at this level from + // the parent node from the one we just matched + fromNode = fromNode.getParent(); + if(fromNode == null) + return null; + } + } + + int childCount = fromNode.getChildCount(); + boolean hasNullChild = false; + for (int i = 0; i < childCount; i++) { + AccessibilityNodeInfo childNode = fromNode.getChild(i); + if (childNode == null) { + Log.w(LOG_TAG, String.format( + "AccessibilityNodeInfo returned a null child (%d of %d)", i, childCount)); + if (!hasNullChild) { + Log.w(LOG_TAG, String.format("parent = %s", fromNode.toString())); + } + hasNullChild = true; + continue; + } + if (!childNode.isVisibleToUser()) { + if (VERBOSE) + Log.v(LOG_TAG, + String.format("Skipping invisible child: %s", childNode.toString())); + continue; + } + AccessibilityNodeInfo retNode = findNodeRegularRecursive(subSelector, childNode, i); + if (retNode != null) { + return retNode; + } + } + return null; + } + + /** + * Used by the {@link #translateCompoundSelector(UiSelector, AccessibilityNodeInfo, boolean)} + * to translate the pattern_selector portion. It has the following format: + * <p/> + * pattern_selector = ... PATTERN=By[instance=x PATTERN=[regular_selector]]<br/> + * <p/> + * pattern_selectors requires search to be performed as regular_selector but where + * regular_selector search returns immediately upon a successful match, the search for + * pattern_selector continues until the requested matched instance of that pattern is + * encountered. + * <p/> + * Counting UI objects requires using pattern_selectors. The counting search is the same + * as a pattern_search however we're not looking to match an instance of the pattern but + * rather continuously walking the accessibility node hierarchy while counting patterns + * until the end of the tree. + * @param subSelector + * @param fromNode + * @param isCounting + * @return null of node is not found or if counting mode is true. + * See {@link #translateCompoundSelector(UiSelector, AccessibilityNodeInfo, boolean)} + */ + private AccessibilityNodeInfo translatePatternSelector(UiSelector subSelector, + AccessibilityNodeInfo fromNode, boolean isCounting) { + + if(subSelector.hasPatternSelector()) { + // Since pattern_selectors are also the type of selectors used when counting, + // we check if this is a counting run or an indexing run + if(isCounting) + //since we're counting, we reset the indexer so to terminates the search when + // the end of tree is reached. The count will be in mPatternCount + mPatternIndexer = -1; + else + // terminates the search once we match the pattern's instance + mPatternIndexer = subSelector.getInstance(); + + // A pattern is wrapped in a PATTERN[instance=x PATTERN[the_pattern]] + subSelector = subSelector.getPatternSelector(); + if(subSelector == null) { + Log.e(LOG_TAG, "Pattern portion of the selector is null or not defined"); + return null; // there is an implementation fault + } + // save the current indent level as parent indent before pattern searches + // begin under the current tree position. + mLogParentIndent = ++mLogIndent; + return findNodePatternRecursive(subSelector, fromNode, 0, subSelector); + } + + Log.e(LOG_TAG, "Selector must have a pattern selector defined"); // implementation fault? + return null; + } + + private AccessibilityNodeInfo findNodePatternRecursive( + UiSelector subSelector, AccessibilityNodeInfo fromNode, int index, + UiSelector originalPattern) { + + if (subSelector.isMatchFor(fromNode, index)) { + if(subSelector.isLeaf()) { + if(mPatternIndexer == 0) { + if (DEBUG) + Log.d(LOG_TAG, formatLog( + String.format("%s", subSelector.dumpToString(false)))); + return fromNode; + } else { + if (DEBUG) + Log.d(LOG_TAG, formatLog( + String.format("%s", subSelector.dumpToString(false)))); + mPatternCounter++; //count the pattern matched + mPatternIndexer--; //decrement until zero for the instance requested + + // At a leaf selector within a group and still not instance matched + // then reset the selector to continue search from current position + // in the accessibility tree for the next pattern match up until the + // pattern index hits 0. + subSelector = originalPattern; + // starting over with next pattern search so reset to parent level + mLogIndent = mLogParentIndent; + } + } else { + if (DEBUG) + Log.d(LOG_TAG, formatLog( + String.format("%s", subSelector.dumpToString(false)))); + + if(subSelector.hasChildSelector()) { + mLogIndent++; // next selector + subSelector = subSelector.getChildSelector(); + if(subSelector == null) { + Log.e(LOG_TAG, "Error: A child selector without content"); + return null; + } + } else if(subSelector.hasParentSelector()) { + mLogIndent++; // next selector + subSelector = subSelector.getParentSelector(); + if(subSelector == null) { + Log.e(LOG_TAG, "Error: A parent selector without content"); + return null; + } + fromNode = fromNode.getParent(); + if(fromNode == null) + return null; + } + } + } + + int childCount = fromNode.getChildCount(); + boolean hasNullChild = false; + for (int i = 0; i < childCount; i++) { + AccessibilityNodeInfo childNode = fromNode.getChild(i); + if (childNode == null) { + Log.w(LOG_TAG, String.format( + "AccessibilityNodeInfo returned a null child (%d of %d)", i, childCount)); + if (!hasNullChild) { + Log.w(LOG_TAG, String.format("parent = %s", fromNode.toString())); + } + hasNullChild = true; + continue; + } + if (!childNode.isVisibleToUser()) { + if (DEBUG) + Log.d(LOG_TAG, + String.format("Skipping invisible child: %s", childNode.toString())); + continue; + } + AccessibilityNodeInfo retNode = findNodePatternRecursive( + subSelector, childNode, i, originalPattern); + if (retNode != null) { + return retNode; + } + } + return null; + } + + public AccessibilityNodeInfo getAccessibilityRootNode() { + return mUiAutomatorBridge.getRootInActiveWindow(); + } + + /** + * Last activity to report accessibility events. + * @deprecated The results returned should be considered unreliable + * @return String name of activity + */ + @Deprecated + public String getCurrentActivityName() { + mUiAutomatorBridge.waitForIdle(); + synchronized (mLock) { + return mLastActivityName; + } + } + + /** + * Last package to report accessibility events + * @return String name of package + */ + public String getCurrentPackageName() { + mUiAutomatorBridge.waitForIdle(); + AccessibilityNodeInfo rootNode = getRootNode(); + if (rootNode == null) + return null; + return rootNode.getPackageName() != null ? rootNode.getPackageName().toString() : null; + } + + private String formatLog(String str) { + StringBuilder l = new StringBuilder(); + for(int space = 0; space < mLogIndent; space++) + l.append(". . "); + if(mLogIndent > 0) + l.append(String.format(". . [%d]: %s", mPatternCounter, str)); + else + l.append(String.format(". . [%d]: %s", mPatternCounter, str)); + return l.toString(); + } +} diff --git a/library/core-src/com/android/uiautomator/core/Tracer.java b/library/core-src/com/android/uiautomator/core/Tracer.java new file mode 100644 index 0000000..d574fc0 --- /dev/null +++ b/library/core-src/com/android/uiautomator/core/Tracer.java @@ -0,0 +1,285 @@ +/* + * 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.util.Log; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.PrintWriter; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +/** + * Class that creates traces of the calls to the UiAutomator API and outputs the + * traces either to logcat or a logfile. Each public method in the UiAutomator + * that needs to be traced should include a call to Tracer.trace in the + * beginning. Tracing is turned off by defualt and needs to be enabled + * explicitly. + * @hide + */ +public class Tracer { + private static final String UNKNOWN_METHOD_STRING = "(unknown method)"; + private static final String UIAUTOMATOR_PACKAGE = "com.android.uiautomator.core"; + private static final int CALLER_LOCATION = 6; + private static final int METHOD_TO_TRACE_LOCATION = 5; + private static final int MIN_STACK_TRACE_LENGTH = 7; + + /** + * Enum that determines where the trace output goes. It can go to either + * logcat, log file or both. + */ + public enum Mode { + NONE, + FILE, + LOGCAT, + ALL + } + + private interface TracerSink { + public void log(String message); + + public void close(); + } + + private class FileSink implements TracerSink { + private PrintWriter mOut; + private SimpleDateFormat mDateFormat; + + public FileSink(File file) throws FileNotFoundException { + mOut = new PrintWriter(file); + mDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US); + } + + public void log(String message) { + mOut.printf("%s %s\n", mDateFormat.format(new Date()), message); + } + + public void close() { + mOut.close(); + } + } + + private class LogcatSink implements TracerSink { + + private static final String LOGCAT_TAG = "UiAutomatorTrace"; + + public void log(String message) { + Log.i(LOGCAT_TAG, message); + } + + public void close() { + // nothing is needed + } + } + + private Mode mCurrentMode = Mode.NONE; + private List<TracerSink> mSinks = new ArrayList<TracerSink>(); + private File mOutputFile; + + private static Tracer mInstance = null; + + /** + * Returns a reference to an instance of the tracer. Useful to set the + * parameters before the trace is collected. + * + * @return + */ + public static Tracer getInstance() { + if (mInstance == null) { + mInstance = new Tracer(); + } + return mInstance; + } + + /** + * Sets where the trace output will go. Can be either be logcat or a file or + * both. Setting this to NONE will turn off tracing. + * + * @param mode + */ + public void setOutputMode(Mode mode) { + closeSinks(); + mCurrentMode = mode; + try { + switch (mode) { + case FILE: + if (mOutputFile == null) { + throw new IllegalArgumentException("Please provide a filename before " + + "attempting write trace to a file"); + } + mSinks.add(new FileSink(mOutputFile)); + break; + case LOGCAT: + mSinks.add(new LogcatSink()); + break; + case ALL: + mSinks.add(new LogcatSink()); + if (mOutputFile == null) { + throw new IllegalArgumentException("Please provide a filename before " + + "attempting write trace to a file"); + } + mSinks.add(new FileSink(mOutputFile)); + break; + default: + break; + } + } catch (FileNotFoundException e) { + Log.w("Tracer", "Could not open log file: " + e.getMessage()); + } + } + + private void closeSinks() { + for (TracerSink sink : mSinks) { + sink.close(); + } + mSinks.clear(); + } + + /** + * Sets the name of the log file where tracing output will be written if the + * tracer is set to write to a file. + * + * @param filename name of the log file. + */ + public void setOutputFilename(String filename) { + mOutputFile = new File(filename); + } + + private void doTrace(Object[] arguments) { + if (mCurrentMode == Mode.NONE) { + return; + } + + String caller = getCaller(); + if (caller == null) { + return; + } + + log(String.format("%s (%s)", caller, join(", ", arguments))); + } + + private void log(String message) { + for (TracerSink sink : mSinks) { + sink.log(message); + } + } + + /** + * Queries whether the tracing is enabled. + * @return true if tracing is enabled, false otherwise. + */ + public boolean isTracingEnabled() { + return mCurrentMode != Mode.NONE; + } + + /** + * Public methods in the UiAutomator should call this function to generate a + * trace. The trace will include the method thats is being called, it's + * arguments and where in the user's code the method is called from. If a + * public method is called internally from UIAutomator then this will not + * output a trace entry. Only calls from outise the UiAutomator package will + * produce output. + * + * Special note about array arguments. You can safely pass arrays of reference types + * to this function. Like String[] or Integer[]. The trace function will print their + * contents by calling toString() on each of the elements. This will not work for + * array of primitive types like int[] or float[]. Before passing them to this function + * convert them to arrays of reference types manually. Example: convert int[] to Integer[]. + * + * @param arguments arguments of the method being traced. + */ + public static void trace(Object... arguments) { + Tracer.getInstance().doTrace(arguments); + } + + private static String join(String separator, Object[] strings) { + if (strings.length == 0) + return ""; + + StringBuilder builder = new StringBuilder(objectToString(strings[0])); + for (int i = 1; i < strings.length; i++) { + builder.append(separator); + builder.append(objectToString(strings[i])); + } + return builder.toString(); + } + + /** + * Special toString method to handle arrays. If the argument is a normal object then this will + * return normal output of obj.toString(). If the argument is an array this will return a + * string representation of the elements of the array. + * + * This method will not work for arrays of primitive types. Arrays of primitive types are + * expected to be converted manually by the caller. If the array is not converter then + * this function will only output "[...]" instead of the contents of the array. + * + * @param obj object to convert to a string + * @return String representation of the object. + */ + private static String objectToString(Object obj) { + if (obj.getClass().isArray()) { + if (obj instanceof Object[]) { + return Arrays.deepToString((Object[])obj); + } else { + return "[...]"; + } + } else { + return obj.toString(); + } + } + + /** + * This method outputs which UiAutomator method was called and where in the + * user code it was called from. If it can't deside which method is called + * it will output "(unknown method)". If the method was called from inside + * the UiAutomator then it returns null. + * + * @return name of the method called and where it was called from. Null if + * method was called from inside UiAutomator. + */ + private static String getCaller() { + StackTraceElement stackTrace[] = Thread.currentThread().getStackTrace(); + if (stackTrace.length < MIN_STACK_TRACE_LENGTH) { + return UNKNOWN_METHOD_STRING; + } + + StackTraceElement caller = stackTrace[METHOD_TO_TRACE_LOCATION]; + StackTraceElement previousCaller = stackTrace[CALLER_LOCATION]; + + if (previousCaller.getClassName().startsWith(UIAUTOMATOR_PACKAGE)) { + return null; + } + + int indexOfDot = caller.getClassName().lastIndexOf('.'); + if (indexOfDot < 0) { + indexOfDot = 0; + } + + if (indexOfDot + 1 >= caller.getClassName().length()) { + return UNKNOWN_METHOD_STRING; + } + + String shortClassName = caller.getClassName().substring(indexOfDot + 1); + return String.format("%s.%s from %s() at %s:%d", shortClassName, caller.getMethodName(), + previousCaller.getMethodName(), previousCaller.getFileName(), + previousCaller.getLineNumber()); + } +} diff --git a/library/core-src/com/android/uiautomator/core/UiAutomatorBridge.java b/library/core-src/com/android/uiautomator/core/UiAutomatorBridge.java new file mode 100644 index 0000000..bc5bc8e --- /dev/null +++ b/library/core-src/com/android/uiautomator/core/UiAutomatorBridge.java @@ -0,0 +1,143 @@ +package com.android.uiautomator.core; + +import android.accessibilityservice.AccessibilityServiceInfo; +import android.app.UiAutomation; +import android.app.UiAutomation.AccessibilityEventFilter; +import android.app.UiAutomation.OnAccessibilityEventListener; +import android.graphics.Bitmap; +import android.util.Log; +import android.view.Display; +import android.view.InputEvent; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +/** + * @hide + */ +public abstract class UiAutomatorBridge { + + private static final String LOG_TAG = UiAutomatorBridge.class.getSimpleName(); + + /** + * This value has the greatest bearing on the appearance of test execution speeds. + * This value is used as the minimum time to wait before considering the UI idle after + * each action. + */ + private static final long QUIET_TIME_TO_BE_CONSIDERD_IDLE_STATE = 500;//ms + + /** + * This is the maximum time the automation will wait for the UI to go idle. Execution + * will resume normally anyway. This is to prevent waiting forever on display updates + * that may be related to spinning wheels or progress updates of sorts etc... + */ + private static final long TOTAL_TIME_TO_WAIT_FOR_IDLE_STATE = 1000 * 10;//ms + + private final UiAutomation mUiAutomation; + + private final InteractionController mInteractionController; + + private final QueryController mQueryController; + + UiAutomatorBridge(UiAutomation uiAutomation) { + mUiAutomation = uiAutomation; + mInteractionController = new InteractionController(this); + mQueryController = new QueryController(this); + } + + InteractionController getInteractionController() { + return mInteractionController; + } + + QueryController getQueryController() { + return mQueryController; + } + + public void setOnAccessibilityEventListener(OnAccessibilityEventListener listener) { + mUiAutomation.setOnAccessibilityEventListener(listener); + } + + public AccessibilityNodeInfo getRootInActiveWindow() { + return mUiAutomation.getRootInActiveWindow(); + } + + public boolean injectInputEvent(InputEvent event, boolean sync) { + return mUiAutomation.injectInputEvent(event, sync); + } + + public boolean setRotation(int rotation) { + return mUiAutomation.setRotation(rotation); + } + + public void setCompressedLayoutHierarchy(boolean compressed) { + AccessibilityServiceInfo info = mUiAutomation.getServiceInfo(); + if (compressed) + info.flags &= ~AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS; + else + info.flags |= AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS; + mUiAutomation.setServiceInfo(info); + } + + public abstract int getRotation(); + + public abstract boolean isScreenOn(); + + public void waitForIdle() { + waitForIdle(TOTAL_TIME_TO_WAIT_FOR_IDLE_STATE); + } + + public void waitForIdle(long timeout) { + try { + mUiAutomation.waitForIdle(QUIET_TIME_TO_BE_CONSIDERD_IDLE_STATE, timeout); + } catch (TimeoutException te) { + Log.w(LOG_TAG, "Could not detect idle state.", te); + } + } + + public AccessibilityEvent executeCommandAndWaitForAccessibilityEvent(Runnable command, + AccessibilityEventFilter filter, long timeoutMillis) throws TimeoutException { + return mUiAutomation.executeAndWaitForEvent(command, + filter, timeoutMillis); + } + + public boolean takeScreenshot(File storePath, int quality) { + Bitmap screenshot = mUiAutomation.takeScreenshot(); + if (screenshot == null) { + return false; + } + BufferedOutputStream bos = null; + try { + bos = new BufferedOutputStream(new FileOutputStream(storePath)); + if (bos != null) { + screenshot.compress(Bitmap.CompressFormat.PNG, quality, bos); + bos.flush(); + } + } catch (IOException ioe) { + Log.e(LOG_TAG, "failed to save screen shot to file", ioe); + return false; + } finally { + if (bos != null) { + try { + bos.close(); + } catch (IOException ioe) { + /* ignore */ + } + } + screenshot.recycle(); + } + return true; + } + + public boolean performGlobalAction(int action) { + return mUiAutomation.performGlobalAction(action); + } + + public abstract Display getDefaultDisplay(); + + public abstract long getSystemLongPressTime(); +} diff --git a/library/core-src/com/android/uiautomator/core/UiCollection.java b/library/core-src/com/android/uiautomator/core/UiCollection.java new file mode 100644 index 0000000..e15beb2 --- /dev/null +++ b/library/core-src/com/android/uiautomator/core/UiCollection.java @@ -0,0 +1,145 @@ +/* + * 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; + +/** + * Used to enumerate a container's UI elements for the purpose of counting, + * or targeting a sub elements by a child's text or description. + * @since API Level 16 + */ +public class UiCollection extends UiObject { + + /** + * Constructs an instance as described by the selector + * + * @param selector + * @since API Level 16 + */ + public UiCollection(UiSelector selector) { + super(selector); + } + + /** + * Searches for child UI element within the constraints of this UiCollection {@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> + * + * @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 + */ + public UiObject getChildByDescription(UiSelector childPattern, String text) + throws UiObjectNotFoundException { + Tracer.trace(childPattern, text); + if (text != null) { + int count = getChildCount(childPattern); + for (int x = 0; x < count; x++) { + UiObject row = getChildByInstance(childPattern, x); + String nodeDesc = row.getContentDescription(); + if(nodeDesc != null && nodeDesc.contains(text)) { + return row; + } + UiObject item = row.getChild(new UiSelector().descriptionContains(text)); + if (item.exists()) { + return row; + } + } + } + throw new UiObjectNotFoundException("for description= \"" + text + "\""); + } + + /** + * Searches for child UI element within the constraints of this UiCollection {@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 is at 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 + */ + 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 UiCollection {@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> + * + * @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 + */ + public UiObject getChildByText(UiSelector childPattern, String text) + throws UiObjectNotFoundException { + Tracer.trace(childPattern, text); + if (text != null) { + int count = getChildCount(childPattern); + for (int x = 0; x < count; x++) { + UiObject row = getChildByInstance(childPattern, x); + String nodeText = row.getText(); + if(text.equals(nodeText)) { + return row; + } + UiObject item = row.getChild(new UiSelector().text(text)); + if (item.exists()) { + return row; + } + } + } + throw new UiObjectNotFoundException("for text= \"" + text + "\""); + } + + /** + * Counts child UI element instances matching the <code>childPattern</code> + * argument. The method returns the number of matching UI elements that are + * currently visible. The count does not include items of a scrollable list + * that are off-screen. + * + * @param childPattern a {@link UiSelector} that represents the matching child UI + * elements to count + * @return the number of matched childPattern under the current {@link UiCollection} + * @since API Level 16 + */ + public int getChildCount(UiSelector childPattern) { + Tracer.trace(childPattern); + UiSelector patternSelector = + UiSelector.patternBuilder(getSelector(), UiSelector.patternBuilder(childPattern)); + return getQueryController().getPatternCount(patternSelector); + } +} diff --git a/library/core-src/com/android/uiautomator/core/UiDevice.java b/library/core-src/com/android/uiautomator/core/UiDevice.java new file mode 100644 index 0000000..a930eb4 --- /dev/null +++ b/library/core-src/com/android/uiautomator/core/UiDevice.java @@ -0,0 +1,851 @@ +/* + * 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.app.UiAutomation; +import android.app.UiAutomation.AccessibilityEventFilter; +import android.graphics.Point; +import android.os.Build; +import android.os.Environment; +import android.os.RemoteException; +import android.os.SystemClock; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.Display; +import android.view.KeyEvent; +import android.view.Surface; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.TimeoutException; + +/** + * UiDevice provides access to state information about the device. + * You can also use this class to simulate user actions on the device, + * such as pressing the d-pad or pressing the Home and Menu buttons. + * @since API Level 16 + */ +public class UiDevice { + private static final String LOG_TAG = UiDevice.class.getSimpleName(); + + // Sometimes HOME and BACK key presses will generate no events if already on + // home page or there is nothing to go back to, Set low timeouts. + private static final long KEY_PRESS_EVENT_TIMEOUT = 1 * 1000; + + // store for registered UiWatchers + private final HashMap<String, UiWatcher> mWatchers = new HashMap<String, UiWatcher>(); + private final List<String> mWatchersTriggers = new ArrayList<String>(); + + // remember if we're executing in the context of a UiWatcher + private boolean mInWatcherContext = false; + + // provides access the {@link QueryController} and {@link InteractionController} + private UiAutomatorBridge mUiAutomationBridge; + + // reference to self + private static UiDevice sDevice; + + private UiDevice() { + /* hide constructor */ + } + + /** + * @hide + */ + public void initialize(UiAutomatorBridge uiAutomatorBridge) { + mUiAutomationBridge = uiAutomatorBridge; + } + + boolean isInWatcherContext() { + return mInWatcherContext; + } + + /** + * Provides access the {@link QueryController} and {@link InteractionController} + * @return {@link ShellUiAutomatorBridge} + */ + UiAutomatorBridge getAutomatorBridge() { + if (mUiAutomationBridge == null) { + throw new RuntimeException("UiDevice not initialized"); + } + return mUiAutomationBridge; + } + + /** + * Enables or disables layout hierarchy compression. + * + * If compression is enabled, the layout hierarchy derived from the Acessibility + * framework will only contain nodes that are important for uiautomator + * testing. Any unnecessary surrounding layout nodes that make viewing + * and searching the hierarchy inefficient are removed. + * + * @param compressed true to enable compression; else, false to disable + * @since API Level 18 + */ + public void setCompressedLayoutHeirarchy(boolean compressed) { + getAutomatorBridge().setCompressedLayoutHierarchy(compressed); + } + + /** + * Retrieves a singleton instance of UiDevice + * + * @return UiDevice instance + * @since API Level 16 + */ + public static UiDevice getInstance() { + if (sDevice == null) { + sDevice = new UiDevice(); + } + return sDevice; + } + + /** + * Returns the display size in dp (device-independent pixel) + * + * The returned display size is adjusted per screen rotation. Also this will return the actual + * size of the screen, rather than adjusted per system decorations (like status bar). + * + * @return a Point containing the display size in dp + */ + public Point getDisplaySizeDp() { + Tracer.trace(); + Display display = getAutomatorBridge().getDefaultDisplay(); + Point p = new Point(); + display.getRealSize(p); + DisplayMetrics metrics = new DisplayMetrics(); + display.getRealMetrics(metrics); + float dpx = p.x / metrics.density; + float dpy = p.y / metrics.density; + p.x = Math.round(dpx); + p.y = Math.round(dpy); + return p; + } + + /** + * Retrieves the product name of the device. + * + * This method provides information on what type of device the test is running on. This value is + * the same as returned by invoking #adb shell getprop ro.product.name. + * + * @return product name of the device + * @since API Level 17 + */ + public String getProductName() { + Tracer.trace(); + return Build.PRODUCT; + } + + /** + * Retrieves the text from the last UI traversal event received. + * + * You can use this method to read the contents in a WebView container + * because the accessibility framework fires events + * as each text is highlighted. You can write a test to perform + * directional arrow presses to focus on different elements inside a WebView, + * and call this method to get the text from each traversed element. + * If you are testing a view container that can return a reference to a + * Document Object Model (DOM) object, your test should use the view's + * DOM instead. + * + * @return text of the last traversal event, else return an empty string + * @since API Level 16 + */ + public String getLastTraversedText() { + Tracer.trace(); + return getAutomatorBridge().getQueryController().getLastTraversedText(); + } + + /** + * Clears the text from the last UI traversal event. + * See {@link #getLastTraversedText()}. + * @since API Level 16 + */ + public void clearLastTraversedText() { + Tracer.trace(); + getAutomatorBridge().getQueryController().clearLastTraversedText(); + } + + /** + * Simulates a short press on the MENU button. + * @return true if successful, else return false + * @since API Level 16 + */ + public boolean pressMenu() { + Tracer.trace(); + waitForIdle(); + return getAutomatorBridge().getInteractionController().sendKeyAndWaitForEvent( + KeyEvent.KEYCODE_MENU, 0, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED, + KEY_PRESS_EVENT_TIMEOUT); + } + + /** + * Simulates a short press on the BACK button. + * @return true if successful, else return false + * @since API Level 16 + */ + public boolean pressBack() { + Tracer.trace(); + waitForIdle(); + return getAutomatorBridge().getInteractionController().sendKeyAndWaitForEvent( + KeyEvent.KEYCODE_BACK, 0, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED, + KEY_PRESS_EVENT_TIMEOUT); + } + + /** + * Simulates a short press on the HOME button. + * @return true if successful, else return false + * @since API Level 16 + */ + public boolean pressHome() { + Tracer.trace(); + waitForIdle(); + return getAutomatorBridge().getInteractionController().sendKeyAndWaitForEvent( + KeyEvent.KEYCODE_HOME, 0, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED, + KEY_PRESS_EVENT_TIMEOUT); + } + + /** + * Simulates a short press on the SEARCH button. + * @return true if successful, else return false + * @since API Level 16 + */ + public boolean pressSearch() { + Tracer.trace(); + return pressKeyCode(KeyEvent.KEYCODE_SEARCH); + } + + /** + * Simulates a short press on the CENTER button. + * @return true if successful, else return false + * @since API Level 16 + */ + public boolean pressDPadCenter() { + Tracer.trace(); + return pressKeyCode(KeyEvent.KEYCODE_DPAD_CENTER); + } + + /** + * Simulates a short press on the DOWN button. + * @return true if successful, else return false + * @since API Level 16 + */ + public boolean pressDPadDown() { + Tracer.trace(); + return pressKeyCode(KeyEvent.KEYCODE_DPAD_DOWN); + } + + /** + * Simulates a short press on the UP button. + * @return true if successful, else return false + * @since API Level 16 + */ + public boolean pressDPadUp() { + Tracer.trace(); + return pressKeyCode(KeyEvent.KEYCODE_DPAD_UP); + } + + /** + * Simulates a short press on the LEFT button. + * @return true if successful, else return false + * @since API Level 16 + */ + public boolean pressDPadLeft() { + Tracer.trace(); + return pressKeyCode(KeyEvent.KEYCODE_DPAD_LEFT); + } + + /** + * Simulates a short press on the RIGHT button. + * @return true if successful, else return false + * @since API Level 16 + */ + public boolean pressDPadRight() { + Tracer.trace(); + return pressKeyCode(KeyEvent.KEYCODE_DPAD_RIGHT); + } + + /** + * Simulates a short press on the DELETE key. + * @return true if successful, else return false + * @since API Level 16 + */ + public boolean pressDelete() { + Tracer.trace(); + return pressKeyCode(KeyEvent.KEYCODE_DEL); + } + + /** + * Simulates a short press on the ENTER key. + * @return true if successful, else return false + * @since API Level 16 + */ + public boolean pressEnter() { + Tracer.trace(); + return pressKeyCode(KeyEvent.KEYCODE_ENTER); + } + + /** + * Simulates a short press using a key code. + * + * See {@link KeyEvent} + * @return true if successful, else return false + * @since API Level 16 + */ + public boolean pressKeyCode(int keyCode) { + Tracer.trace(keyCode); + waitForIdle(); + return getAutomatorBridge().getInteractionController().sendKey(keyCode, 0); + } + + /** + * Simulates a short press using a key code. + * + * See {@link KeyEvent}. + * @param keyCode the key code of the event. + * @param metaState an integer in which each bit set to 1 represents a pressed meta key + * @return true if successful, else return false + * @since API Level 16 + */ + public boolean pressKeyCode(int keyCode, int metaState) { + Tracer.trace(keyCode, metaState); + waitForIdle(); + return getAutomatorBridge().getInteractionController().sendKey(keyCode, metaState); + } + + /** + * Simulates a short press on the Recent Apps button. + * + * @return true if successful, else return false + * @throws RemoteException + * @since API Level 16 + */ + public boolean pressRecentApps() throws RemoteException { + Tracer.trace(); + waitForIdle(); + return getAutomatorBridge().getInteractionController().toggleRecentApps(); + } + + /** + * Opens the notification shade. + * + * @return true if successful, else return false + * @since API Level 18 + */ + public boolean openNotification() { + Tracer.trace(); + waitForIdle(); + return getAutomatorBridge().getInteractionController().openNotification(); + } + + /** + * Opens the Quick Settings shade. + * + * @return true if successful, else return false + * @since API Level 18 + */ + public boolean openQuickSettings() { + Tracer.trace(); + waitForIdle(); + return getAutomatorBridge().getInteractionController().openQuickSettings(); + } + + /** + * Gets the width of the display, in pixels. The width and height details + * are reported based on the current orientation of the display. + * @return width in pixels or zero on failure + * @since API Level 16 + */ + public int getDisplayWidth() { + Tracer.trace(); + Display display = getAutomatorBridge().getDefaultDisplay(); + Point p = new Point(); + display.getSize(p); + return p.x; + } + + /** + * Gets the height of the display, in pixels. The size is adjusted based + * on the current orientation of the display. + * @return height in pixels or zero on failure + * @since API Level 16 + */ + public int getDisplayHeight() { + Tracer.trace(); + Display display = getAutomatorBridge().getDefaultDisplay(); + Point p = new Point(); + display.getSize(p); + return p.y; + } + + /** + * Perform a click at arbitrary coordinates specified by the user + * + * @param x coordinate + * @param y coordinate + * @return true if the click succeeded else false + * @since API Level 16 + */ + public boolean click(int x, int y) { + Tracer.trace(x, y); + if (x >= getDisplayWidth() || y >= getDisplayHeight()) { + return (false); + } + return getAutomatorBridge().getInteractionController().clickNoSync(x, y); + } + + /** + * Performs a swipe from one coordinate to another using the number of steps + * to determine smoothness and speed. Each step execution is throttled to 5ms + * per step. So for a 100 steps, the swipe will take about 1/2 second to complete. + * + * @param startX + * @param startY + * @param endX + * @param endY + * @param steps is the number of move steps sent to the system + * @return false if the operation fails or the coordinates are invalid + * @since API Level 16 + */ + public boolean swipe(int startX, int startY, int endX, int endY, int steps) { + Tracer.trace(startX, startY, endX, endY, steps); + return getAutomatorBridge().getInteractionController() + .swipe(startX, startY, endX, endY, steps); + } + + /** + * Performs a swipe from one coordinate to another coordinate. You can control + * the smoothness and speed of the swipe by specifying the number of steps. + * Each step execution is throttled to 5 milliseconds per step, so for a 100 + * steps, the swipe will take around 0.5 seconds to complete. + * + * @param startX X-axis value for the starting coordinate + * @param startY Y-axis value for the starting coordinate + * @param endX X-axis value for the ending coordinate + * @param endY Y-axis value for the ending coordinate + * @param steps is the number of steps for the swipe action + * @return true if swipe is performed, false if the operation fails + * or the coordinates are invalid + * @since API Level 18 + */ + public boolean drag(int startX, int startY, int endX, int endY, int steps) { + Tracer.trace(startX, startY, endX, endY, steps); + return getAutomatorBridge().getInteractionController() + .swipe(startX, startY, endX, endY, steps, true); + } + + /** + * Performs a swipe between points in the Point array. Each step execution is throttled + * to 5ms per step. So for a 100 steps, the swipe will take about 1/2 second to complete + * + * @param segments is Point array containing at least one Point object + * @param segmentSteps steps to inject between two Points + * @return true on success + * @since API Level 16 + */ + public boolean swipe(Point[] segments, int segmentSteps) { + Tracer.trace(segments, segmentSteps); + return getAutomatorBridge().getInteractionController().swipe(segments, segmentSteps); + } + + /** + * Waits for the current application to idle. + * Default wait timeout is 10 seconds + * @since API Level 16 + */ + public void waitForIdle() { + Tracer.trace(); + waitForIdle(Configurator.getInstance().getWaitForIdleTimeout()); + } + + /** + * Waits for the current application to idle. + * @param timeout in milliseconds + * @since API Level 16 + */ + public void waitForIdle(long timeout) { + Tracer.trace(timeout); + getAutomatorBridge().waitForIdle(timeout); + } + + /** + * Retrieves the last activity to report accessibility events. + * @deprecated The results returned should be considered unreliable + * @return String name of activity + * @since API Level 16 + */ + @Deprecated + public String getCurrentActivityName() { + Tracer.trace(); + return getAutomatorBridge().getQueryController().getCurrentActivityName(); + } + + /** + * Retrieves the name of the last package to report accessibility events. + * @return String name of package + * @since API Level 16 + */ + public String getCurrentPackageName() { + Tracer.trace(); + return getAutomatorBridge().getQueryController().getCurrentPackageName(); + } + + /** + * Registers a {@link UiWatcher} to run automatically when the testing framework is unable to + * find a match using a {@link UiSelector}. See {@link #runWatchers()} + * + * @param name to register the UiWatcher + * @param watcher {@link UiWatcher} + * @since API Level 16 + */ + public void registerWatcher(String name, UiWatcher watcher) { + Tracer.trace(name, watcher); + if (mInWatcherContext) { + throw new IllegalStateException("Cannot register new watcher from within another"); + } + mWatchers.put(name, watcher); + } + + /** + * Removes a previously registered {@link UiWatcher}. + * + * See {@link #registerWatcher(String, UiWatcher)} + * @param name used to register the UiWatcher + * @since API Level 16 + */ + public void removeWatcher(String name) { + Tracer.trace(name); + if (mInWatcherContext) { + throw new IllegalStateException("Cannot remove a watcher from within another"); + } + mWatchers.remove(name); + } + + /** + * This method forces all registered watchers to run. + * See {@link #registerWatcher(String, UiWatcher)} + * @since API Level 16 + */ + public void runWatchers() { + Tracer.trace(); + if (mInWatcherContext) { + return; + } + + for (String watcherName : mWatchers.keySet()) { + UiWatcher watcher = mWatchers.get(watcherName); + if (watcher != null) { + try { + mInWatcherContext = true; + if (watcher.checkForCondition()) { + setWatcherTriggered(watcherName); + } + } catch (Exception e) { + Log.e(LOG_TAG, "Exceuting watcher: " + watcherName, e); + } finally { + mInWatcherContext = false; + } + } + } + } + + /** + * Resets a {@link UiWatcher} that has been triggered. + * If a UiWatcher runs and its {@link UiWatcher#checkForCondition()} call + * returned <code>true</code>, then the UiWatcher is considered triggered. + * See {@link #registerWatcher(String, UiWatcher)} + * @since API Level 16 + */ + public void resetWatcherTriggers() { + Tracer.trace(); + mWatchersTriggers.clear(); + } + + /** + * Checks if a specific registered {@link UiWatcher} has triggered. + * See {@link #registerWatcher(String, UiWatcher)}. If a UiWatcher runs and its + * {@link UiWatcher#checkForCondition()} call returned <code>true</code>, then + * the UiWatcher is considered triggered. This is helpful if a watcher is detecting errors + * from ANR or crash dialogs and the test needs to know if a UiWatcher has been triggered. + * + * @param watcherName + * @return true if triggered else false + * @since API Level 16 + */ + public boolean hasWatcherTriggered(String watcherName) { + Tracer.trace(watcherName); + return mWatchersTriggers.contains(watcherName); + } + + /** + * Checks if any registered {@link UiWatcher} have triggered. + * + * See {@link #registerWatcher(String, UiWatcher)} + * See {@link #hasWatcherTriggered(String)} + * @since API Level 16 + */ + public boolean hasAnyWatcherTriggered() { + Tracer.trace(); + return mWatchersTriggers.size() > 0; + } + + /** + * Used internally by this class to set a {@link UiWatcher} state as triggered. + * @param watcherName + */ + private void setWatcherTriggered(String watcherName) { + Tracer.trace(watcherName); + if (!hasWatcherTriggered(watcherName)) { + mWatchersTriggers.add(watcherName); + } + } + + /** + * Check if the device is in its natural orientation. This is determined by checking if the + * orientation is at 0 or 180 degrees. + * @return true if it is in natural orientation + * @since API Level 17 + */ + public boolean isNaturalOrientation() { + Tracer.trace(); + waitForIdle(); + int ret = getAutomatorBridge().getRotation(); + return ret == UiAutomation.ROTATION_FREEZE_0 || + ret == UiAutomation.ROTATION_FREEZE_180; + } + + /** + * Returns the current rotation of the display, as defined in {@link Surface} + * @since API Level 17 + */ + public int getDisplayRotation() { + Tracer.trace(); + waitForIdle(); + return getAutomatorBridge().getRotation(); + } + + /** + * Disables the sensors and freezes the device rotation at its + * current rotation state. + * @throws RemoteException + * @since API Level 16 + */ + public void freezeRotation() throws RemoteException { + Tracer.trace(); + getAutomatorBridge().getInteractionController().freezeRotation(); + } + + /** + * Re-enables the sensors and un-freezes the device rotation allowing its contents + * to rotate with the device physical rotation. During a test execution, it is best to + * keep the device frozen in a specific orientation until the test case execution has completed. + * @throws RemoteException + */ + public void unfreezeRotation() throws RemoteException { + Tracer.trace(); + getAutomatorBridge().getInteractionController().unfreezeRotation(); + } + + /** + * Simulates orienting the device to the left and also freezes rotation + * by disabling the sensors. + * + * If you want to un-freeze the rotation and re-enable the sensors + * see {@link #unfreezeRotation()}. + * @throws RemoteException + * @since API Level 17 + */ + public void setOrientationLeft() throws RemoteException { + Tracer.trace(); + getAutomatorBridge().getInteractionController().setRotationLeft(); + waitForIdle(); // we don't need to check for idle on entry for this. We'll sync on exit + } + + /** + * Simulates orienting the device to the right and also freezes rotation + * by disabling the sensors. + * + * If you want to un-freeze the rotation and re-enable the sensors + * see {@link #unfreezeRotation()}. + * @throws RemoteException + * @since API Level 17 + */ + public void setOrientationRight() throws RemoteException { + Tracer.trace(); + getAutomatorBridge().getInteractionController().setRotationRight(); + waitForIdle(); // we don't need to check for idle on entry for this. We'll sync on exit + } + + /** + * Simulates orienting the device into its natural orientation and also freezes rotation + * by disabling the sensors. + * + * If you want to un-freeze the rotation and re-enable the sensors + * see {@link #unfreezeRotation()}. + * @throws RemoteException + * @since API Level 17 + */ + public void setOrientationNatural() throws RemoteException { + Tracer.trace(); + getAutomatorBridge().getInteractionController().setRotationNatural(); + waitForIdle(); // we don't need to check for idle on entry for this. We'll sync on exit + } + + /** + * This method simulates pressing the power button if the screen is OFF else + * it does nothing if the screen is already ON. + * + * If the screen was OFF and it just got turned ON, this method will insert a 500ms delay + * to allow the device time to wake up and accept input. + * @throws RemoteException + * @since API Level 16 + */ + public void wakeUp() throws RemoteException { + Tracer.trace(); + if(getAutomatorBridge().getInteractionController().wakeDevice()) { + // sync delay to allow the window manager to start accepting input + // after the device is awakened. + SystemClock.sleep(500); + } + } + + /** + * Checks the power manager if the screen is ON. + * + * @return true if the screen is ON else false + * @throws RemoteException + * @since API Level 16 + */ + public boolean isScreenOn() throws RemoteException { + Tracer.trace(); + return getAutomatorBridge().getInteractionController().isScreenOn(); + } + + /** + * This method simply presses the power button if the screen is ON else + * it does nothing if the screen is already OFF. + * + * @throws RemoteException + * @since API Level 16 + */ + public void sleep() throws RemoteException { + Tracer.trace(); + getAutomatorBridge().getInteractionController().sleepDevice(); + } + + /** + * Helper method used for debugging to dump the current window's layout hierarchy. + * The file root location is /data/local/tmp + * + * @param fileName + * @since API Level 16 + */ + public void dumpWindowHierarchy(String fileName) { + Tracer.trace(fileName); + AccessibilityNodeInfo root = + getAutomatorBridge().getQueryController().getAccessibilityRootNode(); + if(root != null) { + Display display = getAutomatorBridge().getDefaultDisplay(); + Point size = new Point(); + display.getSize(size); + AccessibilityNodeInfoDumper.dumpWindowToFile(root, + new File(new File(Environment.getDataDirectory(), "local/tmp"), fileName), + display.getRotation(), size.x, size.y); + } + } + + /** + * Waits for a window content update event to occur. + * + * If a package name for the window is specified, but the current window + * does not have the same package name, the function returns immediately. + * + * @param packageName the specified window package name (can be <code>null</code>). + * If <code>null</code>, a window update from any front-end window will end the wait + * @param timeout the timeout for the wait + * + * @return true if a window update occurred, false if timeout has elapsed or if the current + * window does not have the specified package name + * @since API Level 16 + */ + public boolean waitForWindowUpdate(final String packageName, long timeout) { + Tracer.trace(packageName, timeout); + if (packageName != null) { + if (!packageName.equals(getCurrentPackageName())) { + return false; + } + } + Runnable emptyRunnable = new Runnable() { + @Override + public void run() { + } + }; + AccessibilityEventFilter checkWindowUpdate = new AccessibilityEventFilter() { + @Override + public boolean accept(AccessibilityEvent t) { + if (t.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) { + return packageName == null || packageName.equals(t.getPackageName()); + } + return false; + } + }; + try { + getAutomatorBridge().executeCommandAndWaitForAccessibilityEvent( + emptyRunnable, checkWindowUpdate, timeout); + } catch (TimeoutException e) { + return false; + } catch (Exception e) { + Log.e(LOG_TAG, "waitForWindowUpdate: general exception from bridge", e); + return false; + } + return true; + } + + /** + * Take a screenshot of current window and store it as PNG + * + * Default scale of 1.0f (original size) and 90% quality is used + * The screenshot is adjusted per screen rotation + * + * @param storePath where the PNG should be written to + * @return true if screen shot is created successfully, false otherwise + * @since API Level 17 + */ + public boolean takeScreenshot(File storePath) { + Tracer.trace(storePath); + return takeScreenshot(storePath, 1.0f, 90); + } + + /** + * Take a screenshot of current window and store it as PNG + * + * The screenshot is adjusted per screen rotation + * + * @param storePath where the PNG should be written to + * @param scale scale the screenshot down if needed; 1.0f for original size + * @param quality quality of the PNG compression; range: 0-100 + * @return true if screen shot is created successfully, false otherwise + * @since API Level 17 + */ + public boolean takeScreenshot(File storePath, float scale, int quality) { + Tracer.trace(storePath, scale, quality); + return getAutomatorBridge().takeScreenshot(storePath, quality); + } +} diff --git a/library/core-src/com/android/uiautomator/core/UiObject.java b/library/core-src/com/android/uiautomator/core/UiObject.java new file mode 100644 index 0000000..4bb99cd --- /dev/null +++ b/library/core-src/com/android/uiautomator/core/UiObject.java @@ -0,0 +1,1083 @@ +/* + * 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.Point; +import android.graphics.Rect; +import android.os.SystemClock; +import android.util.Log; +import android.view.KeyEvent; +import android.view.MotionEvent.PointerCoords; +import android.view.accessibility.AccessibilityNodeInfo; + +/** + * A UiObject is a representation of a view. It is not in any way directly bound to a + * view as an object reference. A UiObject contains information to help it + * locate a matching view at runtime based on the {@link UiSelector} properties specified in + * its constructor. Once you create an instance of a UiObject, it can + * be reused for different views that match the selector criteria. + * @since API Level 16 + */ +public class UiObject { + private static final String LOG_TAG = UiObject.class.getSimpleName(); + /** + * @since API Level 16 + * @deprecated use {@link Configurator#setWaitForSelectorTimeout(long)} + **/ + @Deprecated + protected static final long WAIT_FOR_SELECTOR_TIMEOUT = 10 * 1000; + /** + * @since API Level 16 + **/ + protected static final long WAIT_FOR_SELECTOR_POLL = 1000; + // set a default timeout to 5.5s, since ANR threshold is 5s + /** + * @since API Level 16 + **/ + protected static final long WAIT_FOR_WINDOW_TMEOUT = 5500; + /** + * @since API Level 16 + **/ + protected static final int SWIPE_MARGIN_LIMIT = 5; + /** + * @since API Level 17 + * @deprecated use {@link Configurator#setScrollAcknowledgmentTimeout(long)} + **/ + @Deprecated + protected static final long WAIT_FOR_EVENT_TMEOUT = 3 * 1000; + /** + * @since API Level 18 + **/ + protected static final int FINGER_TOUCH_HALF_WIDTH = 20; + + private final UiSelector mSelector; + + private final Configurator mConfig = Configurator.getInstance(); + + /** + * Constructs a UiObject to represent a view that matches the specified + * selector criteria. + * @param selector + * @since API Level 16 + */ + public UiObject(UiSelector selector) { + mSelector = selector; + } + + /** + * Debugging helper. A test can dump the properties of a selector as a string + * to its logs if needed. <code>getSelector().toString();</code> + * + * @return {@link UiSelector} + * @since API Level 16 + */ + public final UiSelector getSelector() { + Tracer.trace(); + return new UiSelector(mSelector); + } + + /** + * Retrieves the {@link QueryController} to translate a {@link UiSelector} selector + * into an {@link AccessibilityNodeInfo}. + * + * @return {@link QueryController} + */ + QueryController getQueryController() { + return UiDevice.getInstance().getAutomatorBridge().getQueryController(); + } + + /** + * Retrieves the {@link InteractionController} to perform finger actions such as tapping, + * swiping, or entering text. + * + * @return {@link InteractionController} + */ + InteractionController getInteractionController() { + return UiDevice.getInstance().getAutomatorBridge().getInteractionController(); + } + + /** + * Creates a new UiObject for a child view that is under the present UiObject. + * + * @param selector for child view to match + * @return a new UiObject representing the child view + * @since API Level 16 + */ + public UiObject getChild(UiSelector selector) throws UiObjectNotFoundException { + Tracer.trace(selector); + return new UiObject(getSelector().childSelector(selector)); + } + + /** + * Creates a new UiObject for a sibling view or a child of the sibling view, + * relative to the present UiObject. + * + * @param selector for a sibling view or children of the sibling view + * @return a new UiObject representing the matched view + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public UiObject getFromParent(UiSelector selector) throws UiObjectNotFoundException { + Tracer.trace(selector); + return new UiObject(getSelector().fromParent(selector)); + } + + /** + * Counts the child views immediately under the present UiObject. + * + * @return the count of child views. + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public int getChildCount() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + return node.getChildCount(); + } + + /** + * Finds a matching UI element in the accessibility hierarchy, by + * using the selector for this UiObject. + * + * @param timeout in milliseconds + * @return AccessibilityNodeInfo if found else null + * @since API Level 16 + */ + protected AccessibilityNodeInfo findAccessibilityNodeInfo(long timeout) { + AccessibilityNodeInfo node = null; + long startMills = SystemClock.uptimeMillis(); + long currentMills = 0; + while (currentMills <= timeout) { + node = getQueryController().findAccessibilityNodeInfo(getSelector()); + if (node != null) { + break; + } else { + // does nothing if we're reentering another runWatchers() + UiDevice.getInstance().runWatchers(); + } + currentMills = SystemClock.uptimeMillis() - startMills; + if(timeout > 0) { + SystemClock.sleep(WAIT_FOR_SELECTOR_POLL); + } + } + return node; + } + + /** + * Drags this object to a destination UiObject. + * The number of steps specified in your input parameter can influence the + * drag speed, and varying speeds may impact the results. Consider + * evaluating different speeds when using this method in your tests. + * + * @param destObj the destination UiObject. + * @param steps usually 40 steps. You can increase or decrease the steps to change the speed. + * @return true if successful + * @throws UiObjectNotFoundException + * @since API Level 18 + */ + public boolean dragTo(UiObject destObj, int steps) throws UiObjectNotFoundException { + Rect srcRect = getVisibleBounds(); + Rect dstRect = destObj.getVisibleBounds(); + return getInteractionController().swipe(srcRect.centerX(), srcRect.centerY(), + dstRect.centerX(), dstRect.centerY(), steps, true); + } + + /** + * Drags this object to arbitrary coordinates. + * The number of steps specified in your input parameter can influence the + * drag speed, and varying speeds may impact the results. Consider + * evaluating different speeds when using this method in your tests. + * + * @param destX the X-axis coordinate. + * @param destY the Y-axis coordinate. + * @param steps usually 40 steps. You can increase or decrease the steps to change the speed. + * @return true if successful + * @throws UiObjectNotFoundException + * @since API Level 18 + */ + public boolean dragTo(int destX, int destY, int steps) throws UiObjectNotFoundException { + Rect srcRect = getVisibleBounds(); + return getInteractionController().swipe(srcRect.centerX(), srcRect.centerY(), destX, destY, + steps, true); + } + + /** + * Performs the swipe up action on the UiObject. + * See also: + * <ul> + * <li>{@link UiScrollable#scrollToBeginning(int)}</li> + * <li>{@link UiScrollable#scrollToEnd(int)}</li> + * <li>{@link UiScrollable#scrollBackward()}</li> + * <li>{@link UiScrollable#scrollForward()}</li> + * </ul> + * + * @param steps indicates the number of injected move steps into the system. Steps are + * injected about 5ms apart. So a 100 steps may take about 1/2 second to complete. + * @return true of successful + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean swipeUp(int steps) throws UiObjectNotFoundException { + Tracer.trace(steps); + Rect rect = getVisibleBounds(); + if(rect.height() <= SWIPE_MARGIN_LIMIT * 2) + return false; // too small to swipe + return getInteractionController().swipe(rect.centerX(), + rect.bottom - SWIPE_MARGIN_LIMIT, rect.centerX(), rect.top + SWIPE_MARGIN_LIMIT, + steps); + } + + /** + * Performs the swipe down action on the UiObject. + * The swipe gesture can be performed over any surface. The targeted + * UI element does not need to be scrollable. + * See also: + * <ul> + * <li>{@link UiScrollable#scrollToBeginning(int)}</li> + * <li>{@link UiScrollable#scrollToEnd(int)}</li> + * <li>{@link UiScrollable#scrollBackward()}</li> + * <li>{@link UiScrollable#scrollForward()}</li> + * </ul> + * + * @param steps indicates the number of injected move steps into the system. Steps are + * injected about 5ms apart. So a 100 steps may take about 1/2 second to complete. + * @return true if successful + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean swipeDown(int steps) throws UiObjectNotFoundException { + Tracer.trace(steps); + Rect rect = getVisibleBounds(); + if(rect.height() <= SWIPE_MARGIN_LIMIT * 2) + return false; // too small to swipe + return getInteractionController().swipe(rect.centerX(), + rect.top + SWIPE_MARGIN_LIMIT, rect.centerX(), + rect.bottom - SWIPE_MARGIN_LIMIT, steps); + } + + /** + * Performs the swipe left action on the UiObject. + * The swipe gesture can be performed over any surface. The targeted + * UI element does not need to be scrollable. + * See also: + * <ul> + * <li>{@link UiScrollable#scrollToBeginning(int)}</li> + * <li>{@link UiScrollable#scrollToEnd(int)}</li> + * <li>{@link UiScrollable#scrollBackward()}</li> + * <li>{@link UiScrollable#scrollForward()}</li> + * </ul> + * + * @param steps indicates the number of injected move steps into the system. Steps are + * injected about 5ms apart. So a 100 steps may take about 1/2 second to complete. + * @return true if successful + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean swipeLeft(int steps) throws UiObjectNotFoundException { + Tracer.trace(steps); + Rect rect = getVisibleBounds(); + if(rect.width() <= SWIPE_MARGIN_LIMIT * 2) + return false; // too small to swipe + return getInteractionController().swipe(rect.right - SWIPE_MARGIN_LIMIT, + rect.centerY(), rect.left + SWIPE_MARGIN_LIMIT, rect.centerY(), steps); + } + + /** + * Performs the swipe right action on the UiObject. + * The swipe gesture can be performed over any surface. The targeted + * UI element does not need to be scrollable. + * See also: + * <ul> + * <li>{@link UiScrollable#scrollToBeginning(int)}</li> + * <li>{@link UiScrollable#scrollToEnd(int)}</li> + * <li>{@link UiScrollable#scrollBackward()}</li> + * <li>{@link UiScrollable#scrollForward()}</li> + * </ul> + * + * @param steps indicates the number of injected move steps into the system. Steps are + * injected about 5ms apart. So a 100 steps may take about 1/2 second to complete. + * @return true if successful + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean swipeRight(int steps) throws UiObjectNotFoundException { + Tracer.trace(steps); + Rect rect = getVisibleBounds(); + if(rect.width() <= SWIPE_MARGIN_LIMIT * 2) + return false; // too small to swipe + return getInteractionController().swipe(rect.left + SWIPE_MARGIN_LIMIT, + rect.centerY(), rect.right - SWIPE_MARGIN_LIMIT, rect.centerY(), steps); + } + + /** + * Finds the visible bounds of a partially visible UI element + * + * @param node + * @return null if node is null, else a Rect containing visible bounds + */ + private Rect getVisibleBounds(AccessibilityNodeInfo node) { + if (node == null) { + return null; + } + + // targeted node's bounds + int w = UiDevice.getInstance().getDisplayWidth(); + int h = UiDevice.getInstance().getDisplayHeight(); + Rect nodeRect = AccessibilityNodeInfoHelper.getVisibleBoundsInScreen(node, w, h); + + // is the targeted node within a scrollable container? + AccessibilityNodeInfo scrollableParentNode = getScrollableParent(node); + if(scrollableParentNode == null) { + // nothing to adjust for so return the node's Rect as is + return nodeRect; + } + + // Scrollable parent's visible bounds + Rect parentRect = AccessibilityNodeInfoHelper + .getVisibleBoundsInScreen(scrollableParentNode, w, h); + // adjust for partial clipping of targeted by parent node if required + nodeRect.intersect(parentRect); + return nodeRect; + } + + /** + * Walks up the layout hierarchy to find a scrollable parent. A scrollable parent + * indicates that this node might be in a container where it is partially + * visible due to scrolling. In this case, its clickable center might not be visible and + * the click coordinates should be adjusted. + * + * @param node + * @return The accessibility node info. + */ + private AccessibilityNodeInfo getScrollableParent(AccessibilityNodeInfo node) { + AccessibilityNodeInfo parent = node; + while(parent != null) { + parent = parent.getParent(); + if (parent != null && parent.isScrollable()) { + return parent; + } + } + return null; + } + + /** + * Performs a click at the center of the visible bounds of the UI element represented + * by this UiObject. + * + * @return true id successful else false + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean click() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + Rect rect = getVisibleBounds(node); + return getInteractionController().clickAndSync(rect.centerX(), rect.centerY(), + mConfig.getActionAcknowledgmentTimeout()); + } + + /** + * Waits for window transitions that would typically take longer than the + * usual default timeouts. + * See {@link #clickAndWaitForNewWindow(long)} + * + * @return true if the event was triggered, else false + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean clickAndWaitForNewWindow() throws UiObjectNotFoundException { + Tracer.trace(); + return clickAndWaitForNewWindow(WAIT_FOR_WINDOW_TMEOUT); + } + + /** + * Performs a click at the center of the visible bounds of the UI element represented + * by this UiObject and waits for window transitions. + * + * This method differ from {@link UiObject#click()} only in that this method waits for a + * a new window transition as a result of the click. Some examples of a window transition: + * <li>launching a new activity</li> + * <li>bringing up a pop-up menu</li> + * <li>bringing up a dialog</li> + * + * @param timeout timeout before giving up on waiting for a new window + * @return true if the event was triggered, else false + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean clickAndWaitForNewWindow(long timeout) throws UiObjectNotFoundException { + Tracer.trace(timeout); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + Rect rect = getVisibleBounds(node); + return getInteractionController().clickAndWaitForNewWindow(rect.centerX(), rect.centerY(), + mConfig.getActionAcknowledgmentTimeout()); + } + + /** + * Clicks the top and left corner of the UI element + * + * @return true on success + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean clickTopLeft() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + Rect rect = getVisibleBounds(node); + return getInteractionController().clickNoSync(rect.left + 5, rect.top + 5); + } + + /** + * Long clicks bottom and right corner of the UI element + * + * @return true if operation was successful + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean longClickBottomRight() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + Rect rect = getVisibleBounds(node); + return getInteractionController().longTapNoSync(rect.right - 5, rect.bottom - 5); + } + + /** + * Clicks the bottom and right corner of the UI element + * + * @return true on success + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean clickBottomRight() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + Rect rect = getVisibleBounds(node); + return getInteractionController().clickNoSync(rect.right - 5, rect.bottom - 5); + } + + /** + * Long clicks the center of the visible bounds of the UI element + * + * @return true if operation was successful + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean longClick() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + Rect rect = getVisibleBounds(node); + return getInteractionController().longTapNoSync(rect.centerX(), rect.centerY()); + } + + /** + * Long clicks on the top and left corner of the UI element + * + * @return true if operation was successful + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean longClickTopLeft() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + Rect rect = getVisibleBounds(node); + return getInteractionController().longTapNoSync(rect.left + 5, rect.top + 5); + } + + /** + * Reads the <code>text</code> property of the UI element + * + * @return text value of the current node represented by this UiObject + * @throws UiObjectNotFoundException if no match could be found + * @since API Level 16 + */ + public String getText() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + String retVal = safeStringReturn(node.getText()); + Log.d(LOG_TAG, String.format("getText() = %s", retVal)); + return retVal; + } + + /** + * Retrieves the <code>className</code> property of the UI element. + * + * @return class name of the current node represented by this UiObject + * @throws UiObjectNotFoundException if no match was found + * @since API Level 18 + */ + public String getClassName() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + String retVal = safeStringReturn(node.getClassName()); + Log.d(LOG_TAG, String.format("getClassName() = %s", retVal)); + return retVal; + } + + /** + * Reads the <code>content_desc</code> property of the UI element + * + * @return value of node attribute "content_desc" + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public String getContentDescription() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + return safeStringReturn(node.getContentDescription()); + } + + /** + * Sets the text in an editable field, after clearing the field's content. + * + * The {@link UiSelector} selector of this object must reference a UI element that is editable. + * + * When you call this method, the method first simulates a {@link #click()} on + * editable field to set focus. The method then clears the field's contents + * and injects your specified text into the field. + * + * If you want to capture the original contents of the field, call {@link #getText()} first. + * You can then modify the text and use this method to update the field. + * + * @param text string to set + * @return true if operation is successful + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean setText(String text) throws UiObjectNotFoundException { + Tracer.trace(text); + clearTextField(); + return getInteractionController().sendText(text); + } + + /** + * Clears the existing text contents in an editable field. + * + * The {@link UiSelector} of this object must reference a UI element that is editable. + * + * When you call this method, the method first sets focus at the start edge of the field. + * The method then simulates a long-press to select the existing text, and deletes the + * selected text. + * + * If a "Select-All" option is displayed, the method will automatically attempt to use it + * to ensure full text selection. + * + * Note that it is possible that not all the text in the field is selected; for example, + * if the text contains separators such as spaces, slashes, at symbol etc. + * Also, not all editable fields support the long-press functionality. + * + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public void clearTextField() throws UiObjectNotFoundException { + Tracer.trace(); + // long click left + center + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + Rect rect = getVisibleBounds(node); + getInteractionController().longTapNoSync(rect.left + 20, rect.centerY()); + // check if the edit menu is open + UiObject selectAll = new UiObject(new UiSelector().descriptionContains("Select all")); + if(selectAll.waitForExists(50)) + selectAll.click(); + // wait for the selection + SystemClock.sleep(250); + // delete it + getInteractionController().sendKey(KeyEvent.KEYCODE_DEL, 0); + } + + /** + * Check if the UI element's <code>checked</code> property is currently true + * + * @return true if it is else false + * @since API Level 16 + */ + public boolean isChecked() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + return node.isChecked(); + } + + /** + * Checks if the UI element's <code>selected</code> property is currently true. + * + * @return true if it is else false + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean isSelected() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + return node.isSelected(); + } + + /** + * Checks if the UI element's <code>checkable</code> property is currently true. + * + * @return true if it is else false + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean isCheckable() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + return node.isCheckable(); + } + + /** + * Checks if the UI element's <code>enabled</code> property is currently true. + * + * @return true if it is else false + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean isEnabled() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + return node.isEnabled(); + } + + /** + * Checks if the UI element's <code>clickable</code> property is currently true. + * + * @return true if it is else false + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean isClickable() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + return node.isClickable(); + } + + /** + * Check if the UI element's <code>focused</code> property is currently true + * + * @return true if it is else false + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean isFocused() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + return node.isFocused(); + } + + /** + * Check if the UI element's <code>focusable</code> property is currently true. + * + * @return true if it is else false + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean isFocusable() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + return node.isFocusable(); + } + + /** + * Check if the view's <code>scrollable</code> property is currently true + * + * @return true if it is else false + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean isScrollable() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + return node.isScrollable(); + } + + /** + * Check if the view's <code>long-clickable</code> property is currently true + * + * @return true if it is else false + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public boolean isLongClickable() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + return node.isLongClickable(); + } + + /** + * Reads the view's <code>package</code> property + * + * @return true if it is else false + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public String getPackageName() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + return safeStringReturn(node.getPackageName()); + } + + /** + * Returns the visible bounds of the view. + * + * If a portion of the view is visible, only the bounds of the visible portion are + * reported. + * + * @return Rect + * @throws UiObjectNotFoundException + * @see {@link #getBounds()} + * @since API Level 17 + */ + public Rect getVisibleBounds() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + return getVisibleBounds(node); + } + + /** + * Returns the view's <code>bounds</code> property. See {@link #getVisibleBounds()} + * + * @return Rect + * @throws UiObjectNotFoundException + * @since API Level 16 + */ + public Rect getBounds() throws UiObjectNotFoundException { + Tracer.trace(); + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if(node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + Rect nodeRect = new Rect(); + node.getBoundsInScreen(nodeRect); + + return nodeRect; + } + + /** + * Waits a specified length of time for a view to become visible. + * + * This method waits until the view becomes visible on the display, or + * until the timeout has elapsed. You can use this method in situations where + * the content that you want to select is not immediately displayed. + * + * @param timeout the amount of time to wait (in milliseconds) + * @return true if the view is displayed, else false if timeout elapsed while waiting + * @since API Level 16 + */ + public boolean waitForExists(long timeout) { + Tracer.trace(timeout); + if(findAccessibilityNodeInfo(timeout) != null) { + return true; + } + return false; + } + + /** + * Waits a specified length of time for a view to become undetectable. + * + * This method waits until a view is no longer matchable, or until the + * timeout has elapsed. + * + * A view becomes undetectable when the {@link UiSelector} of the object is + * unable to find a match because the element has either changed its state or is no + * longer displayed. + * + * You can use this method when attempting to wait for some long operation + * to compete, such as downloading a large file or connecting to a remote server. + * + * @param timeout time to wait (in milliseconds) + * @return true if the element is gone before timeout elapsed, else false if timeout elapsed + * but a matching element is still found. + * @since API Level 16 + */ + public boolean waitUntilGone(long timeout) { + Tracer.trace(timeout); + long startMills = SystemClock.uptimeMillis(); + long currentMills = 0; + while (currentMills <= timeout) { + if(findAccessibilityNodeInfo(0) == null) + return true; + currentMills = SystemClock.uptimeMillis() - startMills; + if(timeout > 0) + SystemClock.sleep(WAIT_FOR_SELECTOR_POLL); + } + return false; + } + + /** + * Check if view exists. + * + * This methods performs a {@link #waitForExists(long)} with zero timeout. This + * basically returns immediately whether the view represented by this UiObject + * exists or not. If you need to wait longer for this view, then see + * {@link #waitForExists(long)}. + * + * @return true if the view represented by this UiObject does exist + * @since API Level 16 + */ + public boolean exists() { + Tracer.trace(); + return waitForExists(0); + } + + private String safeStringReturn(CharSequence cs) { + if(cs == null) + return ""; + return cs.toString(); + } + + /** + * Performs a two-pointer gesture, where each pointer moves diagonally + * opposite across the other, from the center out towards the edges of the + * this UiObject. + * @param percent percentage of the object's diagonal length for the pinch gesture + * @param steps the number of steps for the gesture. Steps are injected + * about 5 milliseconds apart, so 100 steps may take around 0.5 seconds to complete. + * @return <code>true</code> if all touch events for this gesture are injected successfully, + * <code>false</code> otherwise + * @throws UiObjectNotFoundException + * @since API Level 18 + */ + public boolean pinchOut(int percent, int steps) throws UiObjectNotFoundException { + // make value between 1 and 100 + percent = (percent < 0) ? 1 : (percent > 100) ? 100 : percent; + float percentage = percent / 100f; + + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if (node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + + Rect rect = getVisibleBounds(node); + if (rect.width() <= FINGER_TOUCH_HALF_WIDTH * 2) + throw new IllegalStateException("Object width is too small for operation"); + + // start from the same point at the center of the control + Point startPoint1 = new Point(rect.centerX() - FINGER_TOUCH_HALF_WIDTH, rect.centerY()); + Point startPoint2 = new Point(rect.centerX() + FINGER_TOUCH_HALF_WIDTH, rect.centerY()); + + // End at the top-left and bottom-right corners of the control + Point endPoint1 = new Point(rect.centerX() - (int)((rect.width()/2) * percentage), + rect.centerY()); + Point endPoint2 = new Point(rect.centerX() + (int)((rect.width()/2) * percentage), + rect.centerY()); + + return performTwoPointerGesture(startPoint1, startPoint2, endPoint1, endPoint2, steps); + } + + /** + * Performs a two-pointer gesture, where each pointer moves diagonally + * toward the other, from the edges to the center of this UiObject . + * @param percent percentage of the object's diagonal length for the pinch gesture + * @param steps the number of steps for the gesture. Steps are injected + * about 5 milliseconds apart, so 100 steps may take around 0.5 seconds to complete. + * @return <code>true</code> if all touch events for this gesture are injected successfully, + * <code>false</code> otherwise + * @throws UiObjectNotFoundException + * @since API Level 18 + */ + public boolean pinchIn(int percent, int steps) throws UiObjectNotFoundException { + // make value between 1 and 100 + percent = (percent < 0) ? 0 : (percent > 100) ? 100 : percent; + float percentage = percent / 100f; + + AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout()); + if (node == null) { + throw new UiObjectNotFoundException(getSelector().toString()); + } + + Rect rect = getVisibleBounds(node); + if (rect.width() <= FINGER_TOUCH_HALF_WIDTH * 2) + throw new IllegalStateException("Object width is too small for operation"); + + Point startPoint1 = new Point(rect.centerX() - (int)((rect.width()/2) * percentage), + rect.centerY()); + Point startPoint2 = new Point(rect.centerX() + (int)((rect.width()/2) * percentage), + rect.centerY()); + + Point endPoint1 = new Point(rect.centerX() - FINGER_TOUCH_HALF_WIDTH, rect.centerY()); + Point endPoint2 = new Point(rect.centerX() + FINGER_TOUCH_HALF_WIDTH, rect.centerY()); + + return performTwoPointerGesture(startPoint1, startPoint2, endPoint1, endPoint2, steps); + } + + /** + * Generates a two-pointer gesture with arbitrary starting and ending points. + * + * @param startPoint1 start point of pointer 1 + * @param startPoint2 start point of pointer 2 + * @param endPoint1 end point of pointer 1 + * @param endPoint2 end point of pointer 2 + * @param steps the number of steps for the gesture. Steps are injected + * about 5 milliseconds apart, so 100 steps may take around 0.5 seconds to complete. + * @return <code>true</code> if all touch events for this gesture are injected successfully, + * <code>false</code> otherwise + * @since API Level 18 + */ + public boolean performTwoPointerGesture(Point startPoint1, Point startPoint2, Point endPoint1, + Point endPoint2, int steps) { + + // avoid a divide by zero + if(steps == 0) + steps = 1; + + final float stepX1 = (endPoint1.x - startPoint1.x) / steps; + final float stepY1 = (endPoint1.y - startPoint1.y) / steps; + final float stepX2 = (endPoint2.x - startPoint2.x) / steps; + final float stepY2 = (endPoint2.y - startPoint2.y) / steps; + + int eventX1, eventY1, eventX2, eventY2; + eventX1 = startPoint1.x; + eventY1 = startPoint1.y; + eventX2 = startPoint2.x; + eventY2 = startPoint2.y; + + // allocate for steps plus first down and last up + PointerCoords[] points1 = new PointerCoords[steps + 2]; + PointerCoords[] points2 = new PointerCoords[steps + 2]; + + // Include the first and last touch downs in the arrays of steps + for (int i = 0; i < steps + 1; i++) { + PointerCoords p1 = new PointerCoords(); + p1.x = eventX1; + p1.y = eventY1; + p1.pressure = 1; + p1.size = 1; + points1[i] = p1; + + PointerCoords p2 = new PointerCoords(); + p2.x = eventX2; + p2.y = eventY2; + p2.pressure = 1; + p2.size = 1; + points2[i] = p2; + + eventX1 += stepX1; + eventY1 += stepY1; + eventX2 += stepX2; + eventY2 += stepY2; + } + + // ending pointers coordinates + PointerCoords p1 = new PointerCoords(); + p1.x = endPoint1.x; + p1.y = endPoint1.y; + p1.pressure = 1; + p1.size = 1; + points1[steps + 1] = p1; + + PointerCoords p2 = new PointerCoords(); + p2.x = endPoint2.x; + p2.y = endPoint2.y; + p2.pressure = 1; + p2.size = 1; + points2[steps + 1] = p2; + + return performMultiPointerGesture(points1, points2); + } + + /** + * Performs a multi-touch gesture. You must specify touch coordinates for + * at least 2 pointers. Each pointer must have all of its touch steps + * defined in an array of {@link PointerCoords}. You can use this method to + * specify complex gestures, like circles and irregular shapes, where each + * pointer may take a different path. + * + * To create a single point on a pointer's touch path: + * <code> + * PointerCoords p = new PointerCoords(); + * p.x = stepX; + * p.y = stepY; + * p.pressure = 1; + * p.size = 1; + * </code> + * @param touches represents the pointers' paths. Each {@link PointerCoords} + * array represents a different pointer. Each {@link PointerCoords} in an + * array element represents a touch point on a pointer's path. + * @return <code>true</code> if all touch events for this gesture are injected successfully, + * <code>false</code> otherwise + * @since API Level 18 + */ + public boolean performMultiPointerGesture(PointerCoords[] ...touches) { + return getInteractionController().performMultiPointerGesture(touches); + } +} diff --git a/library/core-src/com/android/uiautomator/core/UiObjectNotFoundException.java b/library/core-src/com/android/uiautomator/core/UiObjectNotFoundException.java new file mode 100644 index 0000000..fc0891b --- /dev/null +++ b/library/core-src/com/android/uiautomator/core/UiObjectNotFoundException.java @@ -0,0 +1,48 @@ +/* + * 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; + +/** + * Generated in test runs when a {@link UiSelector} selector could not be matched + * to any UI element displayed. + * @since API Level 16 + */ +public class UiObjectNotFoundException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * @since API Level 16 + **/ + public UiObjectNotFoundException(String msg) { + super(msg); + } + + /** + * @since API Level 16 + **/ + public UiObjectNotFoundException(String detailMessage, Throwable throwable) { + super(detailMessage, throwable); + } + + /** + * @since API Level 16 + **/ + public UiObjectNotFoundException(Throwable throwable) { + super(throwable); + } +} 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..a8d20c3 --- /dev/null +++ b/library/core-src/com/android/uiautomator/core/UiScrollable.java @@ -0,0 +1,665 @@ +/* + * 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; + } +} diff --git a/library/core-src/com/android/uiautomator/core/UiSelector.java b/library/core-src/com/android/uiautomator/core/UiSelector.java new file mode 100644 index 0000000..bd61bfd --- /dev/null +++ b/library/core-src/com/android/uiautomator/core/UiSelector.java @@ -0,0 +1,1022 @@ +/* + * 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.util.SparseArray; +import android.view.accessibility.AccessibilityNodeInfo; + +import java.util.regex.Pattern; + +/** + * Specifies the elements in the layout hierarchy for tests to target, filtered + * by properties such as text value, content-description, class name, and state + * information. You can also target an element by its location in a layout + * hierarchy. + * @since API Level 16 + */ +public class UiSelector { + static final int SELECTOR_NIL = 0; + static final int SELECTOR_TEXT = 1; + static final int SELECTOR_START_TEXT = 2; + static final int SELECTOR_CONTAINS_TEXT = 3; + static final int SELECTOR_CLASS = 4; + static final int SELECTOR_DESCRIPTION = 5; + static final int SELECTOR_START_DESCRIPTION = 6; + static final int SELECTOR_CONTAINS_DESCRIPTION = 7; + static final int SELECTOR_INDEX = 8; + static final int SELECTOR_INSTANCE = 9; + static final int SELECTOR_ENABLED = 10; + static final int SELECTOR_FOCUSED = 11; + static final int SELECTOR_FOCUSABLE = 12; + static final int SELECTOR_SCROLLABLE = 13; + static final int SELECTOR_CLICKABLE = 14; + static final int SELECTOR_CHECKED = 15; + static final int SELECTOR_SELECTED = 16; + static final int SELECTOR_ID = 17; + static final int SELECTOR_PACKAGE_NAME = 18; + static final int SELECTOR_CHILD = 19; + static final int SELECTOR_CONTAINER = 20; + static final int SELECTOR_PATTERN = 21; + static final int SELECTOR_PARENT = 22; + static final int SELECTOR_COUNT = 23; + static final int SELECTOR_LONG_CLICKABLE = 24; + static final int SELECTOR_TEXT_REGEX = 25; + static final int SELECTOR_CLASS_REGEX = 26; + static final int SELECTOR_DESCRIPTION_REGEX = 27; + static final int SELECTOR_PACKAGE_NAME_REGEX = 28; + static final int SELECTOR_RESOURCE_ID = 29; + static final int SELECTOR_CHECKABLE = 30; + static final int SELECTOR_RESOURCE_ID_REGEX = 31; + + private SparseArray<Object> mSelectorAttributes = new SparseArray<Object>(); + + /** + * @since API Level 16 + */ + public UiSelector() { + } + + UiSelector(UiSelector selector) { + mSelectorAttributes = selector.cloneSelector().mSelectorAttributes; + } + + /** + * @since API Level 17 + */ + protected UiSelector cloneSelector() { + UiSelector ret = new UiSelector(); + ret.mSelectorAttributes = mSelectorAttributes.clone(); + if (hasChildSelector()) + ret.mSelectorAttributes.put(SELECTOR_CHILD, new UiSelector(getChildSelector())); + if (hasParentSelector()) + ret.mSelectorAttributes.put(SELECTOR_PARENT, new UiSelector(getParentSelector())); + if (hasPatternSelector()) + ret.mSelectorAttributes.put(SELECTOR_PATTERN, new UiSelector(getPatternSelector())); + return ret; + } + + static UiSelector patternBuilder(UiSelector selector) { + if (!selector.hasPatternSelector()) { + return new UiSelector().patternSelector(selector); + } + return selector; + } + + static UiSelector patternBuilder(UiSelector container, UiSelector pattern) { + return new UiSelector( + new UiSelector().containerSelector(container).patternSelector(pattern)); + } + + /** + * Set the search criteria to match the visible text displayed + * in a widget (for example, the text label to launch an app). + * + * The text for the element must match exactly with the string in your input + * argument. Matching is case-sensitive. + * + * @param text Value to match + * @return UiSelector with the specified search criteria + * @since API Level 16 + */ + public UiSelector text(String text) { + return buildSelector(SELECTOR_TEXT, text); + } + + /** + * Set the search criteria to match the visible text displayed in a layout + * element, using a regular expression. + * + * The text in the widget must match exactly with the string in your + * input argument. + * + * @param regex a regular expression + * @return UiSelector with the specified search criteria + * @since API Level 17 + */ + public UiSelector textMatches(String regex) { + return buildSelector(SELECTOR_TEXT_REGEX, Pattern.compile(regex)); + } + + /** + * Set the search criteria to match visible text in a widget that is + * prefixed by the text parameter. + * + * The matching is case-insensitive. + * + * @param text Value to match + * @return UiSelector with the specified search criteria + * @since API Level 16 + */ + public UiSelector textStartsWith(String text) { + return buildSelector(SELECTOR_START_TEXT, text); + } + + /** + * Set the search criteria to match the visible text in a widget + * where the visible text must contain the string in your input argument. + * + * The matching is case-sensitive. + * + * @param text Value to match + * @return UiSelector with the specified search criteria + * @since API Level 16 + */ + public UiSelector textContains(String text) { + return buildSelector(SELECTOR_CONTAINS_TEXT, text); + } + + /** + * Set the search criteria to match the class property + * for a widget (for example, "android.widget.Button"). + * + * @param className Value to match + * @return UiSelector with the specified search criteria + * @since API Level 16 + */ + public UiSelector className(String className) { + return buildSelector(SELECTOR_CLASS, className); + } + + /** + * Set the search criteria to match the class property + * for a widget, using a regular expression. + * + * @param regex a regular expression + * @return UiSelector with the specified search criteria + * @since API Level 17 + */ + public UiSelector classNameMatches(String regex) { + return buildSelector(SELECTOR_CLASS_REGEX, Pattern.compile(regex)); + } + + /** + * Set the search criteria to match the class property + * for a widget (for example, "android.widget.Button"). + * + * @param type type + * @return UiSelector with the specified search criteria + * @since API Level 17 + */ + public <T> UiSelector className(Class<T> type) { + return buildSelector(SELECTOR_CLASS, type.getName()); + } + + /** + * Set the search criteria to match the content-description + * property for a widget. + * + * The content-description is typically used + * by the Android Accessibility framework to + * provide an audio prompt for the widget when + * the widget is selected. The content-description + * for the widget must match exactly + * with the string in your input argument. + * + * Matching is case-sensitive. + * + * @param desc Value to match + * @return UiSelector with the specified search criteria + * @since API Level 16 + */ + public UiSelector description(String desc) { + return buildSelector(SELECTOR_DESCRIPTION, desc); + } + + /** + * Set the search criteria to match the content-description + * property for a widget. + * + * The content-description is typically used + * by the Android Accessibility framework to + * provide an audio prompt for the widget when + * the widget is selected. The content-description + * for the widget must match exactly + * with the string in your input argument. + * + * @param regex a regular expression + * @return UiSelector with the specified search criteria + * @since API Level 17 + */ + public UiSelector descriptionMatches(String regex) { + return buildSelector(SELECTOR_DESCRIPTION_REGEX, Pattern.compile(regex)); + } + + /** + * Set the search criteria to match the content-description + * property for a widget. + * + * The content-description is typically used + * by the Android Accessibility framework to + * provide an audio prompt for the widget when + * the widget is selected. The content-description + * for the widget must start + * with the string in your input argument. + * + * Matching is case-insensitive. + * + * @param desc Value to match + * @return UiSelector with the specified search criteria + * @since API Level 16 + */ + public UiSelector descriptionStartsWith(String desc) { + return buildSelector(SELECTOR_START_DESCRIPTION, desc); + } + + /** + * Set the search criteria to match the content-description + * property for a widget. + * + * The content-description is typically used + * by the Android Accessibility framework to + * provide an audio prompt for the widget when + * the widget is selected. The content-description + * for the widget must contain + * the string in your input argument. + * + * Matching is case-insensitive. + * + * @param desc Value to match + * @return UiSelector with the specified search criteria + * @since API Level 16 + */ + public UiSelector descriptionContains(String desc) { + return buildSelector(SELECTOR_CONTAINS_DESCRIPTION, desc); + } + + /** + * Set the search criteria to match the given resource ID. + * + * @param id Value to match + * @return UiSelector with the specified search criteria + * @since API Level 18 + */ + public UiSelector resourceId(String id) { + return buildSelector(SELECTOR_RESOURCE_ID, id); + } + + /** + * Set the search criteria to match the resource ID + * of the widget, using a regular expression.http://blog.bettersoftwaretesting.com/ + * + * @param regex a regular expression + * @return UiSelector with the specified search criteria + * @since API Level 18 + */ + public UiSelector resourceIdMatches(String regex) { + return buildSelector(SELECTOR_RESOURCE_ID_REGEX, Pattern.compile(regex)); + } + + /** + * Set the search criteria to match the widget by its node + * index in the layout hierarchy. + * + * The index value must be 0 or greater. + * + * Using the index can be unreliable and should only + * be used as a last resort for matching. Instead, + * consider using the {@link #instance(int)} method. + * + * @param index Value to match + * @return UiSelector with the specified search criteria + * @since API Level 16 + */ + public UiSelector index(final int index) { + return buildSelector(SELECTOR_INDEX, index); + } + + /** + * Set the search criteria to match the + * widget by its instance number. + * + * The instance value must be 0 or greater, where + * the first instance is 0. + * + * For example, to simulate a user click on + * the third image that is enabled in a UI screen, you + * could specify a a search criteria where the instance is + * 2, the {@link #className(String)} matches the image + * widget class, and {@link #enabled(boolean)} is true. + * The code would look like this: + * <code> + * new UiSelector().className("android.widget.ImageView") + * .enabled(true).instance(2); + * </code> + * + * @param instance Value to match + * @return UiSelector with the specified search criteria + * @since API Level 16 + */ + public UiSelector instance(final int instance) { + return buildSelector(SELECTOR_INSTANCE, instance); + } + + /** + * Set the search criteria to match widgets that are enabled. + * + * Typically, using this search criteria alone is not useful. + * You should also include additional criteria, such as text, + * content-description, or the class name for a widget. + * + * If no other search criteria is specified, and there is more + * than one matching widget, the first widget in the tree + * is selected. + * + * @param val Value to match + * @return UiSelector with the specified search criteria + * @since API Level 16 + */ + public UiSelector enabled(boolean val) { + return buildSelector(SELECTOR_ENABLED, val); + } + + /** + * Set the search criteria to match widgets that have focus. + * + * Typically, using this search criteria alone is not useful. + * You should also include additional criteria, such as text, + * content-description, or the class name for a widget. + * + * If no other search criteria is specified, and there is more + * than one matching widget, the first widget in the tree + * is selected. + * + * @param val Value to match + * @return UiSelector with the specified search criteria + * @since API Level 16 + */ + public UiSelector focused(boolean val) { + return buildSelector(SELECTOR_FOCUSED, val); + } + + /** + * Set the search criteria to match widgets that are focusable. + * + * Typically, using this search criteria alone is not useful. + * You should also include additional criteria, such as text, + * content-description, or the class name for a widget. + * + * If no other search criteria is specified, and there is more + * than one matching widget, the first widget in the tree + * is selected. + * + * @param val Value to match + * @return UiSelector with the specified search criteria + * @since API Level 16 + */ + public UiSelector focusable(boolean val) { + return buildSelector(SELECTOR_FOCUSABLE, val); + } + + /** + * Set the search criteria to match widgets that are scrollable. + * + * Typically, using this search criteria alone is not useful. + * You should also include additional criteria, such as text, + * content-description, or the class name for a widget. + * + * If no other search criteria is specified, and there is more + * than one matching widget, the first widget in the tree + * is selected. + * + * @param val Value to match + * @return UiSelector with the specified search criteria + * @since API Level 16 + */ + public UiSelector scrollable(boolean val) { + return buildSelector(SELECTOR_SCROLLABLE, val); + } + + /** + * Set the search criteria to match widgets that + * are currently selected. + * + * Typically, using this search criteria alone is not useful. + * You should also include additional criteria, such as text, + * content-description, or the class name for a widget. + * + * If no other search criteria is specified, and there is more + * than one matching widget, the first widget in the tree + * is selected. + * + * @param val Value to match + * @return UiSelector with the specified search criteria + * @since API Level 16 + */ + public UiSelector selected(boolean val) { + return buildSelector(SELECTOR_SELECTED, val); + } + + /** + * Set the search criteria to match widgets that + * are currently checked (usually for checkboxes). + * + * Typically, using this search criteria alone is not useful. + * You should also include additional criteria, such as text, + * content-description, or the class name for a widget. + * + * If no other search criteria is specified, and there is more + * than one matching widget, the first widget in the tree + * is selected. + * + * @param val Value to match + * @return UiSelector with the specified search criteria + * @since API Level 16 + */ + public UiSelector checked(boolean val) { + return buildSelector(SELECTOR_CHECKED, val); + } + + /** + * Set the search criteria to match widgets that are clickable. + * + * Typically, using this search criteria alone is not useful. + * You should also include additional criteria, such as text, + * content-description, or the class name for a widget. + * + * If no other search criteria is specified, and there is more + * than one matching widget, the first widget in the tree + * is selected. + * + * @param val Value to match + * @return UiSelector with the specified search criteria + * @since API Level 16 + */ + public UiSelector clickable(boolean val) { + return buildSelector(SELECTOR_CLICKABLE, val); + } + + /** + * Set the search criteria to match widgets that are checkable. + * + * Typically, using this search criteria alone is not useful. + * You should also include additional criteria, such as text, + * content-description, or the class name for a widget. + * + * If no other search criteria is specified, and there is more + * than one matching widget, the first widget in the tree + * is selected. + * + * @param val Value to match + * @return UiSelector with the specified search criteria + * @since API Level 18 + */ + public UiSelector checkable(boolean val) { + return buildSelector(SELECTOR_CHECKABLE, val); + } + + /** + * Set the search criteria to match widgets that are long-clickable. + * + * Typically, using this search criteria alone is not useful. + * You should also include additional criteria, such as text, + * content-description, or the class name for a widget. + * + * If no other search criteria is specified, and there is more + * than one matching widget, the first widget in the tree + * is selected. + * + * @param val Value to match + * @return UiSelector with the specified search criteria + * @since API Level 17 + */ + public UiSelector longClickable(boolean val) { + return buildSelector(SELECTOR_LONG_CLICKABLE, val); + } + + /** + * Adds a child UiSelector criteria to this selector. + * + * Use this selector to narrow the search scope to + * child widgets under a specific parent widget. + * + * @param selector + * @return UiSelector with this added search criterion + * @since API Level 16 + */ + public UiSelector childSelector(UiSelector selector) { + return buildSelector(SELECTOR_CHILD, selector); + } + + private UiSelector patternSelector(UiSelector selector) { + return buildSelector(SELECTOR_PATTERN, selector); + } + + private UiSelector containerSelector(UiSelector selector) { + return buildSelector(SELECTOR_CONTAINER, selector); + } + + /** + * Adds a child UiSelector criteria to this selector which is used to + * start search from the parent widget. + * + * Use this selector to narrow the search scope to + * sibling widgets as well all child widgets under a parent. + * + * @param selector + * @return UiSelector with this added search criterion + * @since API Level 16 + */ + public UiSelector fromParent(UiSelector selector) { + return buildSelector(SELECTOR_PARENT, selector); + } + + /** + * Set the search criteria to match the package name + * of the application that contains the widget. + * + * @param name Value to match + * @return UiSelector with the specified search criteria + * @since API Level 16 + */ + public UiSelector packageName(String name) { + return buildSelector(SELECTOR_PACKAGE_NAME, name); + } + + /** + * Set the search criteria to match the package name + * of the application that contains the widget. + * + * @param regex a regular expression + * @return UiSelector with the specified search criteria + * @since API Level 17 + */ + public UiSelector packageNameMatches(String regex) { + return buildSelector(SELECTOR_PACKAGE_NAME_REGEX, Pattern.compile(regex)); + } + + /** + * Building a UiSelector always returns a new UiSelector and never modifies the + * existing UiSelector being used. + */ + private UiSelector buildSelector(int selectorId, Object selectorValue) { + UiSelector selector = new UiSelector(this); + if (selectorId == SELECTOR_CHILD || selectorId == SELECTOR_PARENT) + selector.getLastSubSelector().mSelectorAttributes.put(selectorId, selectorValue); + else + selector.mSelectorAttributes.put(selectorId, selectorValue); + return selector; + } + + /** + * Selectors may have a hierarchy defined by specifying child nodes to be matched. + * It is not necessary that every selector have more than one level. A selector + * can also be a single level referencing only one node. In such cases the return + * it null. + * + * @return a child selector if one exists. Else null if this selector does not + * reference child node. + */ + UiSelector getChildSelector() { + UiSelector selector = (UiSelector)mSelectorAttributes.get(UiSelector.SELECTOR_CHILD, null); + if (selector != null) + return new UiSelector(selector); + return null; + } + + UiSelector getPatternSelector() { + UiSelector selector = + (UiSelector)mSelectorAttributes.get(UiSelector.SELECTOR_PATTERN, null); + if (selector != null) + return new UiSelector(selector); + return null; + } + + UiSelector getContainerSelector() { + UiSelector selector = + (UiSelector)mSelectorAttributes.get(UiSelector.SELECTOR_CONTAINER, null); + if (selector != null) + return new UiSelector(selector); + return null; + } + + UiSelector getParentSelector() { + UiSelector selector = + (UiSelector) mSelectorAttributes.get(UiSelector.SELECTOR_PARENT, null); + if (selector != null) + return new UiSelector(selector); + return null; + } + + int getInstance() { + return getInt(UiSelector.SELECTOR_INSTANCE); + } + + String getString(int criterion) { + return (String) mSelectorAttributes.get(criterion, null); + } + + boolean getBoolean(int criterion) { + return (Boolean) mSelectorAttributes.get(criterion, false); + } + + int getInt(int criterion) { + return (Integer) mSelectorAttributes.get(criterion, 0); + } + + Pattern getPattern(int criterion) { + return (Pattern) mSelectorAttributes.get(criterion, null); + } + + boolean isMatchFor(AccessibilityNodeInfo node, int index) { + int size = mSelectorAttributes.size(); + for(int x = 0; x < size; x++) { + CharSequence s = null; + int criterion = mSelectorAttributes.keyAt(x); + switch(criterion) { + case UiSelector.SELECTOR_INDEX: + if (index != this.getInt(criterion)) + return false; + break; + case UiSelector.SELECTOR_CHECKED: + if (node.isChecked() != getBoolean(criterion)) { + return false; + } + break; + case UiSelector.SELECTOR_CLASS: + s = node.getClassName(); + if (s == null || !s.toString().contentEquals(getString(criterion))) { + return false; + } + break; + case UiSelector.SELECTOR_CLASS_REGEX: + s = node.getClassName(); + if (s == null || !getPattern(criterion).matcher(s).matches()) { + return false; + } + break; + case UiSelector.SELECTOR_CLICKABLE: + if (node.isClickable() != getBoolean(criterion)) { + return false; + } + break; + case UiSelector.SELECTOR_CHECKABLE: + if (node.isCheckable() != getBoolean(criterion)) { + return false; + } + break; + case UiSelector.SELECTOR_LONG_CLICKABLE: + if (node.isLongClickable() != getBoolean(criterion)) { + return false; + } + break; + case UiSelector.SELECTOR_CONTAINS_DESCRIPTION: + s = node.getContentDescription(); + if (s == null || !s.toString().toLowerCase() + .contains(getString(criterion).toLowerCase())) { + return false; + } + break; + case UiSelector.SELECTOR_START_DESCRIPTION: + s = node.getContentDescription(); + if (s == null || !s.toString().toLowerCase() + .startsWith(getString(criterion).toLowerCase())) { + return false; + } + break; + case UiSelector.SELECTOR_DESCRIPTION: + s = node.getContentDescription(); + if (s == null || !s.toString().contentEquals(getString(criterion))) { + return false; + } + break; + case UiSelector.SELECTOR_DESCRIPTION_REGEX: + s = node.getContentDescription(); + if (s == null || !getPattern(criterion).matcher(s).matches()) { + return false; + } + break; + case UiSelector.SELECTOR_CONTAINS_TEXT: + s = node.getText(); + if (s == null || !s.toString().toLowerCase() + .contains(getString(criterion).toLowerCase())) { + return false; + } + break; + case UiSelector.SELECTOR_START_TEXT: + s = node.getText(); + if (s == null || !s.toString().toLowerCase() + .startsWith(getString(criterion).toLowerCase())) { + return false; + } + break; + case UiSelector.SELECTOR_TEXT: + s = node.getText(); + if (s == null || !s.toString().contentEquals(getString(criterion))) { + return false; + } + break; + case UiSelector.SELECTOR_TEXT_REGEX: + s = node.getText(); + if (s == null || !getPattern(criterion).matcher(s).matches()) { + return false; + } + break; + case UiSelector.SELECTOR_ENABLED: + if (node.isEnabled() != getBoolean(criterion)) { + return false; + } + break; + case UiSelector.SELECTOR_FOCUSABLE: + if (node.isFocusable() != getBoolean(criterion)) { + return false; + } + break; + case UiSelector.SELECTOR_FOCUSED: + if (node.isFocused() != getBoolean(criterion)) { + return false; + } + break; + case UiSelector.SELECTOR_ID: + break; //TODO: do we need this for AccessibilityNodeInfo.id? + case UiSelector.SELECTOR_PACKAGE_NAME: + s = node.getPackageName(); + if (s == null || !s.toString().contentEquals(getString(criterion))) { + return false; + } + break; + case UiSelector.SELECTOR_PACKAGE_NAME_REGEX: + s = node.getPackageName(); + if (s == null || !getPattern(criterion).matcher(s).matches()) { + return false; + } + break; + case UiSelector.SELECTOR_SCROLLABLE: + if (node.isScrollable() != getBoolean(criterion)) { + return false; + } + break; + case UiSelector.SELECTOR_SELECTED: + if (node.isSelected() != getBoolean(criterion)) { + return false; + } + break; + case UiSelector.SELECTOR_RESOURCE_ID: + s = node.getViewIdResourceName(); + if (s == null || !s.toString().contentEquals(getString(criterion))) { + return false; + } + break; + case UiSelector.SELECTOR_RESOURCE_ID_REGEX: + s = node.getViewIdResourceName(); + if (s == null || !getPattern(criterion).matcher(s).matches()) { + return false; + } + break; + } + } + return matchOrUpdateInstance(); + } + + private boolean matchOrUpdateInstance() { + int currentSelectorCounter = 0; + int currentSelectorInstance = 0; + + // matched attributes - now check for matching instance number + if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_INSTANCE) >= 0) { + currentSelectorInstance = + (Integer)mSelectorAttributes.get(UiSelector.SELECTOR_INSTANCE); + } + + // instance is required. Add count if not already counting + if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_COUNT) >= 0) { + currentSelectorCounter = (Integer)mSelectorAttributes.get(UiSelector.SELECTOR_COUNT); + } + + // Verify + if (currentSelectorInstance == currentSelectorCounter) { + return true; + } + // Update count + if (currentSelectorInstance > currentSelectorCounter) { + mSelectorAttributes.put(UiSelector.SELECTOR_COUNT, ++currentSelectorCounter); + } + return false; + } + + /** + * Leaf selector indicates no more child or parent selectors + * are declared in the this selector. + * @return true if is leaf. + */ + boolean isLeaf() { + if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_CHILD) < 0 && + mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_PARENT) < 0) { + return true; + } + return false; + } + + boolean hasChildSelector() { + if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_CHILD) < 0) { + return false; + } + return true; + } + + boolean hasPatternSelector() { + if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_PATTERN) < 0) { + return false; + } + return true; + } + + boolean hasContainerSelector() { + if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_CONTAINER) < 0) { + return false; + } + return true; + } + + boolean hasParentSelector() { + if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_PARENT) < 0) { + return false; + } + return true; + } + + /** + * Returns the deepest selector in the chain of possible sub selectors. + * A chain of selector is created when either of {@link UiSelector#childSelector(UiSelector)} + * or {@link UiSelector#fromParent(UiSelector)} are used once or more in the construction of + * a selector. + * @return last UiSelector in chain + */ + private UiSelector getLastSubSelector() { + if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_CHILD) >= 0) { + UiSelector child = (UiSelector)mSelectorAttributes.get(UiSelector.SELECTOR_CHILD); + if (child.getLastSubSelector() == null) { + return child; + } + return child.getLastSubSelector(); + } else if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_PARENT) >= 0) { + UiSelector parent = (UiSelector)mSelectorAttributes.get(UiSelector.SELECTOR_PARENT); + if (parent.getLastSubSelector() == null) { + return parent; + } + return parent.getLastSubSelector(); + } + return this; + } + + @Override + public String toString() { + return dumpToString(true); + } + + String dumpToString(boolean all) { + StringBuilder builder = new StringBuilder(); + builder.append(UiSelector.class.getSimpleName() + "["); + final int criterionCount = mSelectorAttributes.size(); + for (int i = 0; i < criterionCount; i++) { + if (i > 0) { + builder.append(", "); + } + final int criterion = mSelectorAttributes.keyAt(i); + switch (criterion) { + case SELECTOR_TEXT: + builder.append("TEXT=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_TEXT_REGEX: + builder.append("TEXT_REGEX=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_START_TEXT: + builder.append("START_TEXT=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_CONTAINS_TEXT: + builder.append("CONTAINS_TEXT=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_CLASS: + builder.append("CLASS=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_CLASS_REGEX: + builder.append("CLASS_REGEX=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_DESCRIPTION: + builder.append("DESCRIPTION=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_DESCRIPTION_REGEX: + builder.append("DESCRIPTION_REGEX=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_START_DESCRIPTION: + builder.append("START_DESCRIPTION=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_CONTAINS_DESCRIPTION: + builder.append("CONTAINS_DESCRIPTION=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_INDEX: + builder.append("INDEX=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_INSTANCE: + builder.append("INSTANCE=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_ENABLED: + builder.append("ENABLED=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_FOCUSED: + builder.append("FOCUSED=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_FOCUSABLE: + builder.append("FOCUSABLE=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_SCROLLABLE: + builder.append("SCROLLABLE=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_CLICKABLE: + builder.append("CLICKABLE=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_CHECKABLE: + builder.append("CHECKABLE=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_LONG_CLICKABLE: + builder.append("LONG_CLICKABLE=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_CHECKED: + builder.append("CHECKED=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_SELECTED: + builder.append("SELECTED=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_ID: + builder.append("ID=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_CHILD: + if (all) + builder.append("CHILD=").append(mSelectorAttributes.valueAt(i)); + else + builder.append("CHILD[..]"); + break; + case SELECTOR_PATTERN: + if (all) + builder.append("PATTERN=").append(mSelectorAttributes.valueAt(i)); + else + builder.append("PATTERN[..]"); + break; + case SELECTOR_CONTAINER: + if (all) + builder.append("CONTAINER=").append(mSelectorAttributes.valueAt(i)); + else + builder.append("CONTAINER[..]"); + break; + case SELECTOR_PARENT: + if (all) + builder.append("PARENT=").append(mSelectorAttributes.valueAt(i)); + else + builder.append("PARENT[..]"); + break; + case SELECTOR_COUNT: + builder.append("COUNT=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_PACKAGE_NAME: + builder.append("PACKAGE NAME=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_PACKAGE_NAME_REGEX: + builder.append("PACKAGE_NAME_REGEX=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_RESOURCE_ID: + builder.append("RESOURCE_ID=").append(mSelectorAttributes.valueAt(i)); + break; + case SELECTOR_RESOURCE_ID_REGEX: + builder.append("RESOURCE_ID_REGEX=").append(mSelectorAttributes.valueAt(i)); + break; + default: + builder.append("UNDEFINED="+criterion+" ").append(mSelectorAttributes.valueAt(i)); + } + } + builder.append("]"); + return builder.toString(); + } +} diff --git a/library/core-src/com/android/uiautomator/core/UiWatcher.java b/library/core-src/com/android/uiautomator/core/UiWatcher.java new file mode 100644 index 0000000..5403e30 --- /dev/null +++ b/library/core-src/com/android/uiautomator/core/UiWatcher.java @@ -0,0 +1,48 @@ +/* + * 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; + +/** + * See {@link UiDevice#registerWatcher(String, UiWatcher)} on how to register a + * a condition watcher to be called by the automation library. The automation library will + * invoke checkForCondition() only when a regular API call is in retry mode because it is unable + * to locate its selector yet. Only during this time, the watchers are invoked to check if there is + * something else unexpected on the screen. + * @since API Level 16 + */ +public interface UiWatcher { + + /** + * Custom handler that is automatically called when the testing framework is unable to + * find a match using the {@link UiSelector} + * + * When the framework is in the process of matching a {@link UiSelector} and it + * is unable to match any widget based on the specified criteria in the selector, + * the framework will perform retries for a predetermined time, waiting for the display + * to update and show the desired widget. While the framework is in this state, it will call + * registered watchers' checkForCondition(). This gives the registered watchers a chance + * to take a look at the display and see if there is a recognized condition that can be + * handled and in doing so allowing the current test to continue. + * + * An example usage would be to look for dialogs popped due to other background + * processes requesting user attention and have nothing to do with the application + * currently under test. + * + * @return true to indicate a matched condition or false for nothing was matched + * @since API Level 16 + */ + public boolean checkForCondition(); +} diff --git a/library/testrunner-src/com/android/uiautomator/core/ShellUiAutomatorBridge.java b/library/testrunner-src/com/android/uiautomator/core/ShellUiAutomatorBridge.java new file mode 100644 index 0000000..1afa513 --- /dev/null +++ b/library/testrunner-src/com/android/uiautomator/core/ShellUiAutomatorBridge.java @@ -0,0 +1,122 @@ +/* + * 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.app.ActivityManagerNative; +import android.app.IActivityManager; +import android.app.IActivityManager.ContentProviderHolder; +import android.app.UiAutomation; +import android.content.Context; +import android.content.IContentProvider; +import android.database.Cursor; +import android.hardware.display.DisplayManagerGlobal; +import android.os.Binder; +import android.os.IBinder; +import android.os.IPowerManager; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.Log; +import android.view.Display; +import android.view.IWindowManager; + +/** + * @hide + */ +public class ShellUiAutomatorBridge extends UiAutomatorBridge { + + private static final String LOG_TAG = ShellUiAutomatorBridge.class.getSimpleName(); + + public ShellUiAutomatorBridge(UiAutomation uiAutomation) { + super(uiAutomation); + } + + public Display getDefaultDisplay() { + return DisplayManagerGlobal.getInstance().getRealDisplay(Display.DEFAULT_DISPLAY); + } + + public long getSystemLongPressTime() { + // Read the long press timeout setting. + long longPressTimeout = 0; + try { + IContentProvider provider = null; + Cursor cursor = null; + IActivityManager activityManager = ActivityManagerNative.getDefault(); + String providerName = Settings.Secure.CONTENT_URI.getAuthority(); + IBinder token = new Binder(); + try { + ContentProviderHolder holder = activityManager.getContentProviderExternal( + providerName, UserHandle.USER_OWNER, token); + if (holder == null) { + throw new IllegalStateException("Could not find provider: " + providerName); + } + provider = holder.provider; + cursor = provider.query(null, Settings.Secure.CONTENT_URI, + new String[] { + Settings.Secure.VALUE + }, "name=?", + new String[] { + Settings.Secure.LONG_PRESS_TIMEOUT + }, null, null); + if (cursor.moveToFirst()) { + longPressTimeout = cursor.getInt(0); + } + } finally { + if (cursor != null) { + cursor.close(); + } + if (provider != null) { + activityManager.removeContentProviderExternal(providerName, token); + } + } + } catch (RemoteException e) { + String message = "Error reading long press timeout setting."; + Log.e(LOG_TAG, message, e); + throw new RuntimeException(message, e); + } + return longPressTimeout; + } + + @Override + public int getRotation() { + IWindowManager wm = + IWindowManager.Stub.asInterface(ServiceManager.getService(Context.WINDOW_SERVICE)); + int ret = -1; + try { + ret = wm.getRotation(); + } catch (RemoteException e) { + Log.e(LOG_TAG, "Error getting screen rotation", e); + throw new RuntimeException(e); + } + return ret; + } + + @Override + public boolean isScreenOn() { + IPowerManager pm = + IPowerManager.Stub.asInterface(ServiceManager.getService(Context.POWER_SERVICE)); + boolean ret = false; + try { + ret = pm.isScreenOn(); + } catch (RemoteException e) { + Log.e(LOG_TAG, "Error getting screen status", e); + throw new RuntimeException(e); + } + return ret; + } +} diff --git a/library/testrunner-src/com/android/uiautomator/core/UiAutomationShellWrapper.java b/library/testrunner-src/com/android/uiautomator/core/UiAutomationShellWrapper.java new file mode 100644 index 0000000..1fa9bac --- /dev/null +++ b/library/testrunner-src/com/android/uiautomator/core/UiAutomationShellWrapper.java @@ -0,0 +1,127 @@ +package com.android.uiautomator.core; + +import android.accessibilityservice.AccessibilityServiceInfo; +import android.app.ActivityManager; +import android.app.ActivityManagerNative; +import android.app.IActivityController; +import android.app.IActivityManager; +import android.app.UiAutomation; +import android.app.UiAutomationConnection; +import android.content.Intent; +import android.os.HandlerThread; +import android.os.RemoteException; + +/** + * @hide + */ +public class UiAutomationShellWrapper { + + private static final String HANDLER_THREAD_NAME = "UiAutomatorHandlerThread"; + + private final HandlerThread mHandlerThread = new HandlerThread(HANDLER_THREAD_NAME); + + private UiAutomation mUiAutomation; + + public void connect() { + if (mHandlerThread.isAlive()) { + throw new IllegalStateException("Already connected!"); + } + mHandlerThread.start(); + mUiAutomation = new UiAutomation(mHandlerThread.getLooper(), + new UiAutomationConnection()); + mUiAutomation.connect(); + } + + /** + * Enable or disable monkey test mode. + * + * Setting test as "monkey" indicates to some applications that a test framework is + * running as a "monkey" type. Such applications may choose not to perform actions that + * do submits so to avoid allowing monkey tests from doing harm or performing annoying + * actions such as dialing 911 or posting messages to public forums, etc. + * + * @param isSet True to set as monkey test. False to set as regular functional test (default). + * @see {@link ActivityManager#isUserAMonkey()} + */ + public void setRunAsMonkey(boolean isSet) { + IActivityManager am = ActivityManagerNative.getDefault(); + if (am == null) { + throw new RuntimeException("Can't manage monkey status; is the system running?"); + } + try { + if (isSet) { + am.setActivityController(new DummyActivityController()); + } else { + am.setActivityController(null); + } + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + public void disconnect() { + if (!mHandlerThread.isAlive()) { + throw new IllegalStateException("Already disconnected!"); + } + mUiAutomation.disconnect(); + mHandlerThread.quit(); + } + + public UiAutomation getUiAutomation() { + return mUiAutomation; + } + + public void setCompressedLayoutHierarchy(boolean compressed) { + AccessibilityServiceInfo info = mUiAutomation.getServiceInfo(); + if (compressed) + info.flags &= ~AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS; + else + info.flags |= AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS; + mUiAutomation.setServiceInfo(info); + } + + /** + * Dummy, no interference, activity controller. + */ + private class DummyActivityController extends IActivityController.Stub { + @Override + public boolean activityStarting(Intent intent, String pkg) throws RemoteException { + /* do nothing and let activity proceed normally */ + return true; + } + + @Override + public boolean activityResuming(String pkg) throws RemoteException { + /* do nothing and let activity proceed normally */ + return true; + } + + @Override + public boolean appCrashed(String processName, int pid, String shortMsg, String longMsg, + long timeMillis, String stackTrace) throws RemoteException { + /* do nothing and let activity proceed normally */ + return true; + } + + @Override + public int appEarlyNotResponding(String processName, int pid, String annotation) + throws RemoteException { + /* do nothing and let activity proceed normally */ + return 0; + } + + @Override + public int appNotResponding(String processName, int pid, String processStats) + throws RemoteException { + /* do nothing and let activity proceed normally */ + return 0; + } + + @Override + public int systemNotResponding(String message) + throws RemoteException { + /* do nothing and let system proceed normally */ + return 0; + } + } +} diff --git a/library/testrunner-src/com/android/uiautomator/testrunner/IAutomationSupport.java b/library/testrunner-src/com/android/uiautomator/testrunner/IAutomationSupport.java new file mode 100644 index 0000000..f0c60d2 --- /dev/null +++ b/library/testrunner-src/com/android/uiautomator/testrunner/IAutomationSupport.java @@ -0,0 +1,37 @@ +/* + * 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.testrunner; + +import android.os.Bundle; + +/** + * Provides auxiliary support for running test cases + * + * @since API Level 16 + */ +public interface IAutomationSupport { + + /** + * Allows the running test cases to send out interim status + * + * @param resultCode + * @param status status report, consisting of key value pairs + * @since API Level 16 + */ + public void sendStatus(int resultCode, Bundle status); + +} diff --git a/library/testrunner-src/com/android/uiautomator/testrunner/TestCaseCollector.java b/library/testrunner-src/com/android/uiautomator/testrunner/TestCaseCollector.java new file mode 100644 index 0000000..cda49f6 --- /dev/null +++ b/library/testrunner-src/com/android/uiautomator/testrunner/TestCaseCollector.java @@ -0,0 +1,152 @@ +/* + * 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.testrunner; + +import junit.framework.TestCase; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A convenient class that encapsulates functions for adding test classes + * + * @hide + */ +public class TestCaseCollector { + + private ClassLoader mClassLoader; + private List<TestCase> mTestCases; + private TestCaseFilter mFilter; + + public TestCaseCollector(ClassLoader classLoader, TestCaseFilter filter) { + mClassLoader = classLoader; + mTestCases = new ArrayList<TestCase>(); + mFilter = filter; + } + + /** + * Adds classes to test by providing a list of class names in string + * + * The class name may be in "<class name>#<method name>" format + * + * @param classNames class must be subclass of {@link UiAutomatorTestCase} + * @throws ClassNotFoundException + */ + public void addTestClasses(List<String> classNames) throws ClassNotFoundException { + for (String className : classNames) { + addTestClass(className); + } + } + + /** + * Adds class to test by providing class name in string. + * + * The class name may be in "<class name>#<method name>" format + * + * @param className classes must be subclass of {@link UiAutomatorTestCase} + * @throws ClassNotFoundException + */ + public void addTestClass(String className) throws ClassNotFoundException { + int hashPos = className.indexOf('#'); + String methodName = null; + if (hashPos != -1) { + methodName = className.substring(hashPos + 1); + className = className.substring(0, hashPos); + } + addTestClass(className, methodName); + } + + /** + * Adds class to test by providing class name and method name in separate strings + * + * @param className class must be subclass of {@link UiAutomatorTestCase} + * @param methodName may be null, in which case all "public void testNNN(void)" functions + * will be added + * @throws ClassNotFoundException + */ + public void addTestClass(String className, String methodName) throws ClassNotFoundException { + Class<?> clazz = mClassLoader.loadClass(className); + if (methodName != null) { + addSingleTestMethod(clazz, methodName); + } else { + Method[] methods = clazz.getMethods(); + for (Method method : methods) { + if (mFilter.accept(method)) { + addSingleTestMethod(clazz, method.getName()); + } + } + } + } + + /** + * Gets the list of added test cases so far + * @return a list of {@link TestCase} + */ + public List<TestCase> getTestCases() { + return Collections.unmodifiableList(mTestCases); + } + + protected void addSingleTestMethod(Class<?> clazz, String method) { + if (!(mFilter.accept(clazz))) { + throw new RuntimeException("Test class must be derived from UiAutomatorTestCase"); + } + try { + TestCase testCase = (TestCase) clazz.newInstance(); + testCase.setName(method); + mTestCases.add(testCase); + } catch (InstantiationException e) { + mTestCases.add(error(clazz, "InstantiationException: could not instantiate " + + "test class. Class: " + clazz.getName())); + } catch (IllegalAccessException e) { + mTestCases.add(error(clazz, "IllegalAccessException: could not instantiate " + + "test class. Class: " + clazz.getName())); + } + } + + private UiAutomatorTestCase error(Class<?> clazz, final String message) { + UiAutomatorTestCase warning = new UiAutomatorTestCase() { + protected void runTest() { + fail(message); + } + }; + + warning.setName(clazz.getName()); + return warning; + } + + /** + * Determine if a class and its method should be accepted into test suite + * + */ + public interface TestCaseFilter { + + /** + * Determine that based on the method signature, if it can be accepted + * @param method + */ + public boolean accept(Method method); + + /** + * Determine that based on the class type, if it can be accepted + * @param clazz + * @return + */ + public boolean accept(Class<?> clazz); + } +} diff --git a/library/testrunner-src/com/android/uiautomator/testrunner/UiAutomatorTestCase.java b/library/testrunner-src/com/android/uiautomator/testrunner/UiAutomatorTestCase.java new file mode 100644 index 0000000..e7d961b --- /dev/null +++ b/library/testrunner-src/com/android/uiautomator/testrunner/UiAutomatorTestCase.java @@ -0,0 +1,147 @@ +/* + * 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.testrunner; + +import android.content.Context; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemClock; +import android.view.inputmethod.InputMethodInfo; + +import com.android.internal.view.IInputMethodManager; +import com.android.uiautomator.core.UiDevice; + +import junit.framework.TestCase; + +import java.util.List; + +/** + * UI automation test should extend this class. This class provides access + * to the following: + * {@link UiDevice} instance + * {@link Bundle} for command line parameters. + * @since API Level 16 + */ +public class UiAutomatorTestCase extends TestCase { + + private static final String DISABLE_IME = "disable_ime"; + private static final String DUMMY_IME_PACKAGE = "com.android.testing.dummyime"; + private UiDevice mUiDevice; + private Bundle mParams; + private IAutomationSupport mAutomationSupport; + private boolean mShouldDisableIme = false; + + @Override + protected void setUp() throws Exception { + super.setUp(); + mShouldDisableIme = "true".equals(mParams.getString(DISABLE_IME)); + if (mShouldDisableIme) { + setDummyIme(); + } + } + + @Override + protected void tearDown() throws Exception { + if (mShouldDisableIme) { + restoreActiveIme(); + } + super.tearDown(); + } + + /** + * Get current instance of {@link UiDevice}. Works similar to calling the static + * {@link UiDevice#getInstance()} from anywhere in the test classes. + * @since API Level 16 + */ + public UiDevice getUiDevice() { + return mUiDevice; + } + + /** + * Get command line parameters. On the command line when passing <code>-e key value</code> + * pairs, the {@link Bundle} will have the key value pairs conveniently available to the + * tests. + * @since API Level 16 + */ + public Bundle getParams() { + return mParams; + } + + /** + * Provides support for running tests to report interim status + * + * @return IAutomationSupport + * @since API Level 16 + */ + public IAutomationSupport getAutomationSupport() { + return mAutomationSupport; + } + + /** + * package private + * @param uiDevice + */ + void setUiDevice(UiDevice uiDevice) { + mUiDevice = uiDevice; + } + + /** + * package private + * @param params + */ + void setParams(Bundle params) { + mParams = params; + } + + void setAutomationSupport(IAutomationSupport automationSupport) { + mAutomationSupport = automationSupport; + } + + /** + * Calls {@link SystemClock#sleep(long)} to sleep + * @param ms is in milliseconds. + * @since API Level 16 + */ + public void sleep(long ms) { + SystemClock.sleep(ms); + } + + private void setDummyIme() throws RemoteException { + IInputMethodManager im = IInputMethodManager.Stub.asInterface(ServiceManager + .getService(Context.INPUT_METHOD_SERVICE)); + List<InputMethodInfo> infos = im.getInputMethodList(); + String id = null; + for (InputMethodInfo info : infos) { + if (DUMMY_IME_PACKAGE.equals(info.getComponent().getPackageName())) { + id = info.getId(); + } + } + if (id == null) { + throw new RuntimeException(String.format( + "Required testing fixture missing: IME package (%s)", DUMMY_IME_PACKAGE)); + } + im.setInputMethod(null, id); + } + + private void restoreActiveIme() throws RemoteException { + // TODO: figure out a way to restore active IME + // Currently retrieving active IME requires querying secure settings provider, which is hard + // to do without a Context; so the caveat here is that to make the post test device usable, + // the active IME needs to be manually switched. + } +} diff --git a/library/testrunner-src/com/android/uiautomator/testrunner/UiAutomatorTestCaseFilter.java b/library/testrunner-src/com/android/uiautomator/testrunner/UiAutomatorTestCaseFilter.java new file mode 100644 index 0000000..1de5a4d --- /dev/null +++ b/library/testrunner-src/com/android/uiautomator/testrunner/UiAutomatorTestCaseFilter.java @@ -0,0 +1,42 @@ +/* + * 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.testrunner; + +import com.android.uiautomator.testrunner.TestCaseCollector.TestCaseFilter; + +import java.lang.reflect.Method; + +/** + * A {@link TestCaseFilter} that accepts testFoo methods and {@link UiAutomatorTestCase} classes + * + * @hide + */ +public class UiAutomatorTestCaseFilter implements TestCaseFilter { + + @Override + public boolean accept(Method method) { + return ((method.getParameterTypes().length == 0) && + (method.getName().startsWith("test")) && + (method.getReturnType().getSimpleName().equals("void"))); + } + + @Override + public boolean accept(Class<?> clazz) { + return UiAutomatorTestCase.class.isAssignableFrom(clazz); + } + +} diff --git a/library/testrunner-src/com/android/uiautomator/testrunner/UiAutomatorTestRunner.java b/library/testrunner-src/com/android/uiautomator/testrunner/UiAutomatorTestRunner.java new file mode 100644 index 0000000..ef167f9 --- /dev/null +++ b/library/testrunner-src/com/android/uiautomator/testrunner/UiAutomatorTestRunner.java @@ -0,0 +1,431 @@ +/* + * 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.testrunner; + +import android.app.Activity; +import android.app.IInstrumentationWatcher; +import android.app.Instrumentation; +import android.content.ComponentName; +import android.os.Bundle; +import android.os.Debug; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.SystemClock; +import android.test.RepetitiveTest; +import android.util.Log; + +import com.android.uiautomator.core.ShellUiAutomatorBridge; +import com.android.uiautomator.core.Tracer; +import com.android.uiautomator.core.UiAutomationShellWrapper; +import com.android.uiautomator.core.UiDevice; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.lang.Thread.UncaughtExceptionHandler; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import junit.framework.AssertionFailedError; +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestListener; +import junit.framework.TestResult; +import junit.runner.BaseTestRunner; +import junit.textui.ResultPrinter; + +/** + * @hide + */ +public class UiAutomatorTestRunner { + + private static final String LOGTAG = UiAutomatorTestRunner.class.getSimpleName(); + private static final int EXIT_OK = 0; + private static final int EXIT_EXCEPTION = -1; + + private static final String HANDLER_THREAD_NAME = "UiAutomatorHandlerThread"; + + private boolean mDebug; + private boolean mMonkey; + private Bundle mParams = null; + private UiDevice mUiDevice; + private List<String> mTestClasses = null; + private final FakeInstrumentationWatcher mWatcher = new FakeInstrumentationWatcher(); + private final IAutomationSupport mAutomationSupport = new IAutomationSupport() { + @Override + public void sendStatus(int resultCode, Bundle status) { + mWatcher.instrumentationStatus(null, resultCode, status); + } + }; + private final List<TestListener> mTestListeners = new ArrayList<TestListener>(); + + private HandlerThread mHandlerThread; + + public void run(List<String> testClasses, Bundle params, boolean debug, boolean monkey) { + Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() { + @Override + public void uncaughtException(Thread thread, Throwable ex) { + Log.e(LOGTAG, "uncaught exception", ex); + Bundle results = new Bundle(); + results.putString("shortMsg", ex.getClass().getName()); + results.putString("longMsg", ex.getMessage()); + mWatcher.instrumentationFinished(null, 0, results); + // bailing on uncaught exception + System.exit(EXIT_EXCEPTION); + } + }); + + mTestClasses = testClasses; + mParams = params; + mDebug = debug; + mMonkey = monkey; + start(); + System.exit(EXIT_OK); + } + + /** + * Called after all test classes are in place, ready to test + */ + protected void start() { + TestCaseCollector collector = getTestCaseCollector(this.getClass().getClassLoader()); + try { + collector.addTestClasses(mTestClasses); + } catch (ClassNotFoundException e) { + // will be caught by uncaught handler + throw new RuntimeException(e.getMessage(), e); + } + if (mDebug) { + Debug.waitForDebugger(); + } + mHandlerThread = new HandlerThread(HANDLER_THREAD_NAME); + mHandlerThread.setDaemon(true); + mHandlerThread.start(); + UiAutomationShellWrapper automationWrapper = new UiAutomationShellWrapper(); + automationWrapper.connect(); + + long startTime = SystemClock.uptimeMillis(); + TestResult testRunResult = new TestResult(); + ResultReporter resultPrinter; + String outputFormat = mParams.getString("outputFormat"); + List<TestCase> testCases = collector.getTestCases(); + Bundle testRunOutput = new Bundle(); + if ("simple".equals(outputFormat)) { + resultPrinter = new SimpleResultPrinter(System.out, true); + } else { + resultPrinter = new WatcherResultPrinter(testCases.size()); + } + try { + automationWrapper.setRunAsMonkey(mMonkey); + mUiDevice = UiDevice.getInstance(); + mUiDevice.initialize(new ShellUiAutomatorBridge(automationWrapper.getUiAutomation())); + + String traceType = mParams.getString("traceOutputMode"); + if(traceType != null) { + Tracer.Mode mode = Tracer.Mode.valueOf(Tracer.Mode.class, traceType); + if (mode == Tracer.Mode.FILE || mode == Tracer.Mode.ALL) { + String filename = mParams.getString("traceLogFilename"); + if (filename == null) { + throw new RuntimeException("Name of log file not specified. " + + "Please specify it using traceLogFilename parameter"); + } + Tracer.getInstance().setOutputFilename(filename); + } + Tracer.getInstance().setOutputMode(mode); + } + + // add test listeners + testRunResult.addListener(resultPrinter); + // add all custom listeners + for (TestListener listener : mTestListeners) { + testRunResult.addListener(listener); + } + + // run tests for realz! + for (TestCase testCase : testCases) { + prepareTestCase(testCase); + testCase.run(testRunResult); + } + } catch (Throwable t) { + // catch all exceptions so a more verbose error message can be outputted + resultPrinter.printUnexpectedError(t); + testRunOutput.putString("shortMsg", t.getMessage()); + } finally { + long runTime = SystemClock.uptimeMillis() - startTime; + resultPrinter.print(testRunResult, runTime, testRunOutput); + automationWrapper.disconnect(); + automationWrapper.setRunAsMonkey(false); + mHandlerThread.quit(); + } + } + + // copy & pasted from com.android.commands.am.Am + private class FakeInstrumentationWatcher implements IInstrumentationWatcher { + + private final boolean mRawMode = true; + + @Override + public IBinder asBinder() { + throw new UnsupportedOperationException("I'm just a fake!"); + } + + @Override + public void instrumentationStatus(ComponentName name, int resultCode, Bundle results) { + synchronized (this) { + // pretty printer mode? + String pretty = null; + if (!mRawMode && results != null) { + pretty = results.getString(Instrumentation.REPORT_KEY_STREAMRESULT); + } + if (pretty != null) { + System.out.print(pretty); + } else { + if (results != null) { + for (String key : results.keySet()) { + System.out.println("INSTRUMENTATION_STATUS: " + key + "=" + + results.get(key)); + } + } + System.out.println("INSTRUMENTATION_STATUS_CODE: " + resultCode); + } + notifyAll(); + } + } + + @Override + public void instrumentationFinished(ComponentName name, int resultCode, Bundle results) { + synchronized (this) { + // pretty printer mode? + String pretty = null; + if (!mRawMode && results != null) { + pretty = results.getString(Instrumentation.REPORT_KEY_STREAMRESULT); + } + if (pretty != null) { + System.out.println(pretty); + } else { + if (results != null) { + for (String key : results.keySet()) { + System.out.println("INSTRUMENTATION_RESULT: " + key + "=" + + results.get(key)); + } + } + System.out.println("INSTRUMENTATION_CODE: " + resultCode); + } + notifyAll(); + } + } + } + + private interface ResultReporter extends TestListener { + public void print(TestResult result, long runTime, Bundle testOutput); + public void printUnexpectedError(Throwable t); + } + + // Copy & pasted from InstrumentationTestRunner.WatcherResultPrinter + private class WatcherResultPrinter implements ResultReporter { + + private static final String REPORT_KEY_NUM_TOTAL = "numtests"; + private static final String REPORT_KEY_NAME_CLASS = "class"; + private static final String REPORT_KEY_NUM_CURRENT = "current"; + private static final String REPORT_KEY_NAME_TEST = "test"; + private static final String REPORT_KEY_NUM_ITERATIONS = "numiterations"; + private static final String REPORT_VALUE_ID = "UiAutomatorTestRunner"; + private static final String REPORT_KEY_STACK = "stack"; + + private static final int REPORT_VALUE_RESULT_START = 1; + private static final int REPORT_VALUE_RESULT_ERROR = -1; + private static final int REPORT_VALUE_RESULT_FAILURE = -2; + + private final Bundle mResultTemplate; + Bundle mTestResult; + int mTestNum = 0; + int mTestResultCode = 0; + String mTestClass = null; + + private final SimpleResultPrinter mPrinter; + private final ByteArrayOutputStream mStream; + private final PrintStream mWriter; + + public WatcherResultPrinter(int numTests) { + mResultTemplate = new Bundle(); + mResultTemplate.putString(Instrumentation.REPORT_KEY_IDENTIFIER, REPORT_VALUE_ID); + mResultTemplate.putInt(REPORT_KEY_NUM_TOTAL, numTests); + + mStream = new ByteArrayOutputStream(); + mWriter = new PrintStream(mStream); + mPrinter = new SimpleResultPrinter(mWriter, false); + } + + /** + * send a status for the start of a each test, so long tests can be seen + * as "running" + */ + @Override + public void startTest(Test test) { + String testClass = test.getClass().getName(); + String testName = ((TestCase) test).getName(); + mTestResult = new Bundle(mResultTemplate); + mTestResult.putString(REPORT_KEY_NAME_CLASS, testClass); + mTestResult.putString(REPORT_KEY_NAME_TEST, testName); + mTestResult.putInt(REPORT_KEY_NUM_CURRENT, ++mTestNum); + // pretty printing + if (testClass != null && !testClass.equals(mTestClass)) { + mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT, + String.format("\n%s:", testClass)); + mTestClass = testClass; + } else { + mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT, ""); + } + + Method testMethod = null; + try { + testMethod = test.getClass().getMethod(testName); + // Report total number of iterations, if test is repetitive + if (testMethod.isAnnotationPresent(RepetitiveTest.class)) { + int numIterations = testMethod.getAnnotation(RepetitiveTest.class) + .numIterations(); + mTestResult.putInt(REPORT_KEY_NUM_ITERATIONS, numIterations); + } + } catch (NoSuchMethodException e) { + // ignore- the test with given name does not exist. Will be + // handled during test + // execution + } + + mAutomationSupport.sendStatus(REPORT_VALUE_RESULT_START, mTestResult); + mTestResultCode = 0; + + mPrinter.startTest(test); + } + + @Override + public void addError(Test test, Throwable t) { + mTestResult.putString(REPORT_KEY_STACK, BaseTestRunner.getFilteredTrace(t)); + mTestResultCode = REPORT_VALUE_RESULT_ERROR; + // pretty printing + mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT, + String.format("\nError in %s:\n%s", + ((TestCase)test).getName(), BaseTestRunner.getFilteredTrace(t))); + + mPrinter.addError(test, t); + } + + @Override + public void addFailure(Test test, AssertionFailedError t) { + mTestResult.putString(REPORT_KEY_STACK, BaseTestRunner.getFilteredTrace(t)); + mTestResultCode = REPORT_VALUE_RESULT_FAILURE; + // pretty printing + mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT, + String.format("\nFailure in %s:\n%s", + ((TestCase)test).getName(), BaseTestRunner.getFilteredTrace(t))); + + mPrinter.addFailure(test, t); + } + + @Override + public void endTest(Test test) { + if (mTestResultCode == 0) { + mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT, "."); + } + mAutomationSupport.sendStatus(mTestResultCode, mTestResult); + + mPrinter.endTest(test); + } + + @Override + public void print(TestResult result, long runTime, Bundle testOutput) { + mPrinter.print(result, runTime, testOutput); + testOutput.putString(Instrumentation.REPORT_KEY_STREAMRESULT, + String.format("\nTest results for %s=%s", + getClass().getSimpleName(), + mStream.toString())); + mWriter.close(); + mAutomationSupport.sendStatus(Activity.RESULT_OK, testOutput); + } + + @Override + public void printUnexpectedError(Throwable t) { + mWriter.println(String.format("Test run aborted due to unexpected exception: %s", + t.getMessage())); + t.printStackTrace(mWriter); + } + } + + /** + * Class that produces the same output as JUnit when running from command line. Can be + * used when default UiAutomator output is too verbose. + */ + private class SimpleResultPrinter extends ResultPrinter implements ResultReporter { + private final boolean mFullOutput; + public SimpleResultPrinter(PrintStream writer, boolean fullOutput) { + super(writer); + mFullOutput = fullOutput; + } + + @Override + public void print(TestResult result, long runTime, Bundle testOutput) { + printHeader(runTime); + if (mFullOutput) { + printErrors(result); + printFailures(result); + } + printFooter(result); + } + + @Override + public void printUnexpectedError(Throwable t) { + if (mFullOutput) { + getWriter().printf("Test run aborted due to unexpected exeption: %s", + t.getMessage()); + t.printStackTrace(getWriter()); + } + } + } + + protected TestCaseCollector getTestCaseCollector(ClassLoader classLoader) { + return new TestCaseCollector(classLoader, getTestCaseFilter()); + } + + /** + * Returns an object which determines if the class and its methods should be + * accepted into the test suite. + * @return + */ + public UiAutomatorTestCaseFilter getTestCaseFilter() { + return new UiAutomatorTestCaseFilter(); + } + + protected void addTestListener(TestListener listener) { + if (!mTestListeners.contains(listener)) { + mTestListeners.add(listener); + } + } + + protected void removeTestListener(TestListener listener) { + mTestListeners.remove(listener); + } + + /** + * subclass may override this method to perform further preparation + * + * @param testCase + */ + protected void prepareTestCase(TestCase testCase) { + ((UiAutomatorTestCase)testCase).setAutomationSupport(mAutomationSupport); + ((UiAutomatorTestCase)testCase).setUiDevice(mUiDevice); + ((UiAutomatorTestCase)testCase).setParams(mParams); + } +} diff --git a/samples/SkeletonTest/Android.mk b/samples/SkeletonTest/Android.mk new file mode 100644 index 0000000..a4a776f --- /dev/null +++ b/samples/SkeletonTest/Android.mk @@ -0,0 +1,30 @@ +#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. + +local_target_dir := $(TARGET_OUT_DATA)/local/tmp +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-java-files-under, src) + +LOCAL_MODULE_TAGS := optional + +LOCAL_MODULE := uiautomator.skeletontest + +LOCAL_JAVA_LIBRARIES := uiautomator.core + +LOCAL_MODULE_PATH := $(local_target_dir) + +include $(BUILD_JAVA_LIBRARY) diff --git a/samples/SkeletonTest/README b/samples/SkeletonTest/README new file mode 100644 index 0000000..b402039 --- /dev/null +++ b/samples/SkeletonTest/README @@ -0,0 +1,19 @@ +This is an (almost) empty test, it serves as a prototype to create new UI +Automator tests that can be compiled inside the Android build tree. The single +test case included performs a key press, and send out some information about +the test device. + +Steps to run this test: +* have a fully built Android source tree +* build the test: + mmm frameworks/testing/uiautomator/samples/SkeletonTest +* deploy the test: + adb push ${OUT}/data/local/tmp/uiautomator.skeletontest.jar /data/local/tmp/ +* run the test: + adb shell uiautomator runtest uiautomator.skeletontest.jar \ + -e class com.android.uiautomator.samples.skeleton.DemoTestCase + +Steps to create new tests off it: +* cp -r frameworks/testing/uiautomator/samples/SkeletonTest /new/location +* modify Android.mk, replace LOCAL_MODULE_NAME, change LOCAL_MODULE_TAGS if + necessary, add new dependecies if needed diff --git a/samples/SkeletonTest/src/com/android/uiautomator/samples/skeleton/DemoTestCase.java b/samples/SkeletonTest/src/com/android/uiautomator/samples/skeleton/DemoTestCase.java new file mode 100644 index 0000000..1abc25a --- /dev/null +++ b/samples/SkeletonTest/src/com/android/uiautomator/samples/skeleton/DemoTestCase.java @@ -0,0 +1,37 @@ +/* + * 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.samples.skeleton; + +import android.app.Activity; +import android.graphics.Point; +import android.os.Bundle; + +import com.android.uiautomator.testrunner.UiAutomatorTestCase; + +public class DemoTestCase extends UiAutomatorTestCase { + + public void testDemo() { + assertTrue(getUiDevice().pressHome()); + Bundle status = new Bundle(); + status.putString("msg", "This is a demo test and I just pressed HOME"); + status.putString("product", getUiDevice().getProductName()); + Point p = getUiDevice().getDisplaySizeDp(); + status.putInt("dp-width", p.x); + status.putInt("dp-height", p.y); + getAutomationSupport().sendStatus(Activity.RESULT_OK, status); + } +} diff --git a/uiautomator_test_libraries/Android.mk b/uiautomator_test_libraries/Android.mk new file mode 100644 index 0000000..f975016 --- /dev/null +++ b/uiautomator_test_libraries/Android.mk @@ -0,0 +1,33 @@ +# +# 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. +# +local_target_dir := $(TARGET_OUT_DATA)/local/tmp +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-java-files-under, src) + +LOCAL_MODULE := com.android.uiautomator.platform.common + +LOCAL_SDK_VERSION := 16 + +LOCAL_JAVA_LIBRARIES := uiautomator_sdk_v$(LOCAL_SDK_VERSION) + +LOCAL_MODULE_TAGS := optional + +LOCAL_MODULE_PATH := $(local_target_dir) + +include $(BUILD_STATIC_JAVA_LIBRARY) diff --git a/uiautomator_test_libraries/src/com/android/uiautomator/common/UiWatchers.java b/uiautomator_test_libraries/src/com/android/uiautomator/common/UiWatchers.java new file mode 100644 index 0000000..dd1bb5b --- /dev/null +++ b/uiautomator_test_libraries/src/com/android/uiautomator/common/UiWatchers.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2013 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.common; + +import android.util.Log; + +import com.android.uiautomator.core.UiDevice; +import com.android.uiautomator.core.UiObject; +import com.android.uiautomator.core.UiObjectNotFoundException; +import com.android.uiautomator.core.UiSelector; +import com.android.uiautomator.core.UiWatcher; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class UiWatchers { + private static final String LOG_TAG = UiWatchers.class.getSimpleName(); + private final List<String> mErrors = new ArrayList<String>(); + + /** + * We can use the UiDevice registerWatcher to register a small script to be executed when the + * framework is waiting for a control to appear. Waiting may be the cause of an unexpected + * dialog on the screen and it is the time when the framework runs the registered watchers. + * This is a sample watcher looking for ANR and crashes. it closes it and moves on. You should + * create your own watchers and handle error logging properly for your type of tests. + */ + public void registerAnrAndCrashWatchers() { + + UiDevice.getInstance().registerWatcher("ANR", new UiWatcher() { + @Override + public boolean checkForCondition() { + UiObject window = new UiObject(new UiSelector().className( + "com.android.server.am.AppNotRespondingDialog")); + String errorText = null; + if (window.exists()) { + try { + errorText = window.getText(); + } catch (UiObjectNotFoundException e) { + Log.e(LOG_TAG, "dialog gone?", e); + } + onAnrDetected(errorText); + postHandler(); + return true; // triggered + } + return false; // no trigger + } + }); + + // class names may have changed + UiDevice.getInstance().registerWatcher("ANR2", new UiWatcher() { + @Override + public boolean checkForCondition() { + UiObject window = new UiObject(new UiSelector().packageName("android") + .textContains("isn't responding.")); + if (window.exists()) { + String errorText = null; + try { + errorText = window.getText(); + } catch (UiObjectNotFoundException e) { + Log.e(LOG_TAG, "dialog gone?", e); + } + onAnrDetected(errorText); + postHandler(); + return true; // triggered + } + return false; // no trigger + } + }); + + UiDevice.getInstance().registerWatcher("CRASH", new UiWatcher() { + @Override + public boolean checkForCondition() { + UiObject window = new UiObject(new UiSelector().className( + "com.android.server.am.AppErrorDialog")); + if (window.exists()) { + String errorText = null; + try { + errorText = window.getText(); + } catch (UiObjectNotFoundException e) { + Log.e(LOG_TAG, "dialog gone?", e); + } + onCrashDetected(errorText); + postHandler(); + return true; // triggered + } + return false; // no trigger + } + }); + + UiDevice.getInstance().registerWatcher("CRASH2", new UiWatcher() { + @Override + public boolean checkForCondition() { + UiObject window = new UiObject(new UiSelector().packageName("android") + .textContains("has stopped")); + if (window.exists()) { + String errorText = null; + try { + errorText = window.getText(); + } catch (UiObjectNotFoundException e) { + Log.e(LOG_TAG, "dialog gone?", e); + } + onCrashDetected(errorText); + postHandler(); + return true; // triggered + } + return false; // no trigger + } + }); + + Log.i(LOG_TAG, "Registed GUI Exception watchers"); + } + + public void onAnrDetected(String errorText) { + mErrors.add(errorText); + } + + public void onCrashDetected(String errorText) { + mErrors.add(errorText); + } + + public void reset() { + mErrors.clear(); + } + + public List<String> getErrors() { + return Collections.unmodifiableList(mErrors); + } + + /** + * Current implementation ignores the exception and continues. + */ + public void postHandler() { + // TODO: Add custom error logging here + + String formatedOutput = String.format("UI Exception Message: %-20s\n", UiDevice + .getInstance().getCurrentPackageName()); + Log.e(LOG_TAG, formatedOutput); + + UiObject buttonOK = new UiObject(new UiSelector().text("OK").enabled(true)); + // sometimes it takes a while for the OK button to become enabled + buttonOK.waitForExists(5000); + try { + buttonOK.click(); + } catch (UiObjectNotFoundException e) { + Log.e(LOG_TAG, "Exception", e); + } + } +} diff --git a/uiautomator_test_libraries/src/com/android/uiautomator/common/helpers/AppHelperBase.java b/uiautomator_test_libraries/src/com/android/uiautomator/common/helpers/AppHelperBase.java new file mode 100644 index 0000000..e3d9b61 --- /dev/null +++ b/uiautomator_test_libraries/src/com/android/uiautomator/common/helpers/AppHelperBase.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2013 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.common.helpers; + +import java.io.BufferedReader; +import java.io.IOException; + +/** + * Base app helper class intended for all app helper to extend. + * This class provides common APIs that are expected to be present across + * all app helpers. + */ +public abstract class AppHelperBase { + + /* + * App helpers should provide methods for accessing various UI widgets. + * Assume the app has an Action Bar, the helper should provide something similar to + * SomeAppHelper.ActionBar.getRefreshButton(). Methods like this help the tests check the + * state of the targeted widget as well as clicking it if need be. These types of methods are + * referred to as object getters. If there are different components, consider creating internal + * name spaces as in the .ActionBar example for better context. + * + * Adding basic units of functionality APIs is also very helpful to test. + * Consider the Alarm clock application as an example. It would be helpful if its helper + * provided basic functionality such as, setAlarm(Date) and deleteAlarm(Date). Such basic + * and key functionality helper methods, will abstract the tests from the UI implementation and + * make tests more reliable. + */ + + /** + * Launches the application. + * + * This is typically performed by executing a shell command to launch the application + * via Intent. It is possible to launch the application by automating the Launcher + * views and finding the target app icon to launch, however, this is prone to failure if + * the Launcher UI implementation differ from one platform to another. + */ + abstract public void open(); + + /** + * Checks if the application is in foreground. + * + * This is typically performed by verifying the current package name of the foreground + * application. See UiDevice.getCurrentPackageName() + * @return true if open, else false. + */ + abstract public boolean isOpen(); + + + /** + * Helper to execute a shell command. + * @param command + */ + protected void runShellCommand(String command) { + Process p = null; + BufferedReader resultReader = null; + try { + p = Runtime.getRuntime().exec(command); + int status = p.waitFor(); + if (status != 0) { + System.err.println(String.format("Run shell command: %s, status: %s", command, + status)); + } + } catch (IOException e) { + System.err.println("// Exception from command " + command + ":"); + System.err.println(e.toString()); + } catch (InterruptedException e) { + System.err.println("// Interrupted while waiting for the command to finish. "); + System.err.println(e.toString()); + } finally { + try { + if (resultReader != null) { + resultReader.close(); + } + if (p != null) { + p.destroy(); + } + } catch (IOException e) { + System.err.println(e.toString()); + } + } + } +} diff --git a/uiautomator_test_libraries/src/com/android/uiautomator/common/helpers/DatePickerHelper.java b/uiautomator_test_libraries/src/com/android/uiautomator/common/helpers/DatePickerHelper.java new file mode 100644 index 0000000..6c7db3a --- /dev/null +++ b/uiautomator_test_libraries/src/com/android/uiautomator/common/helpers/DatePickerHelper.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2013 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.common.helpers; + +import com.android.uiautomator.core.UiObject; +import com.android.uiautomator.core.UiObjectNotFoundException; +import com.android.uiautomator.core.UiSelector; + +import java.util.Calendar; + +/** + * Use this helper anywhere there is a date picker to manage. This helper + * will set date specified in a Calendar object. + */ +public class DatePickerHelper { + + public static final int MONTH = 0; + public static final int DAY = 1; + public static final int YEAR = 2; + + public static String getCurrentMonth() throws UiObjectNotFoundException { + return getNumberPickerField(MONTH).getText(); + } + + public static String getCurrentDay() throws UiObjectNotFoundException { + return getNumberPickerField(DAY).getText(); + } + + public static String getCurrentYear() throws UiObjectNotFoundException { + return getNumberPickerField(YEAR).getText(); + } + + public static void incrementMonth() throws UiObjectNotFoundException { + incrementMonth(1); + } + + public static void incrementMonth(int count) throws UiObjectNotFoundException { + for (int x = 0; x < count; x++) + getNumberPickerIncrementButton(MONTH).click(); + } + + public static void decrementMonth() throws UiObjectNotFoundException { + decrementMonth(1); + } + + public static void decrementMonth(int count) throws UiObjectNotFoundException { + for (int x = 0; x < count; x++) + getNumberPickerDecrementButton(MONTH).click(); + } + + public static void incrementDay() throws UiObjectNotFoundException { + incrementDay(1); + } + + public static void incrementDay(int count) throws UiObjectNotFoundException { + for (int x = 0; x < count; x++) + getNumberPickerIncrementButton(DAY).click(); + } + + public static void decrementDay() throws UiObjectNotFoundException { + decrementDay(1); + } + + public static void decrementDay(int count) throws UiObjectNotFoundException { + for (int x = 0; x < count; x++) + getNumberPickerDecrementButton(DAY).click(); + } + + public static void incrementYear() throws UiObjectNotFoundException { + incrementYear(1); + } + + public static void incrementYear(int count) throws UiObjectNotFoundException { + for (int x = 0; x < count; x++) + getNumberPickerIncrementButton(YEAR).click(); + } + + public static void decrementYear() throws UiObjectNotFoundException { + decrementYear(1); + } + + public static void decrementYear(int count) throws UiObjectNotFoundException { + for (int x = 0; x < count; x++) + getNumberPickerDecrementButton(YEAR).click(); + } + + public static UiObject getNumberPicker(int instance) { + return new UiObject(new UiSelector().className( + android.widget.NumberPicker.class.getName()).instance(instance)); + } + + public static UiObject getNumberPickerField(int instance) + throws UiObjectNotFoundException { + return getNumberPicker(instance).getChild( + new UiSelector().className(android.widget.EditText.class.getName())); + } + + public static UiObject getNumberPickerDecrementButton(int instance) + throws UiObjectNotFoundException { + return getNumberPicker(instance).getChild( + new UiSelector().className(android.widget.Button.class.getName()).instance(0)); + } + + public static UiObject getNumberPickerIncrementButton(int instance) + throws UiObjectNotFoundException { + return getNumberPicker(instance).getChild( + new UiSelector().className(android.widget.Button.class.getName()).instance(1)); + } + + public static void clickDone() throws UiObjectNotFoundException { + new UiObject(new UiSelector().text("Done")).click(); + } + + public static void setDate(Calendar cal) throws UiObjectNotFoundException { + int calYear = cal.get(Calendar.YEAR); + int calMonth = cal.get(Calendar.MONTH); + int calDay = cal.get(Calendar.DAY_OF_MONTH); + + // Adjust day - increment or decrement using the shortest path + // while accounting for number of days in month and considering + // special case for Feb and leap years. + int dpDay = Integer.parseInt(getCurrentDay()); + if (calDay > dpDay) { + if (calDay - dpDay < getDaysInMonth(calYear, calMonth) / 2) + incrementDay(calDay - dpDay); + else + decrementDay(dpDay - calDay + getDaysInMonth(calYear, calMonth)); + } else if (dpDay > calDay) { + if (dpDay - calDay < getDaysInMonth(calYear, calMonth) / 2) + decrementDay(dpDay - calDay); + else + incrementDay(calDay - dpDay + getDaysInMonth(calYear, calMonth)); + } + + // Adjust month - increment or decrement using the shortest path + int dpMonth = toMonthNumber(getCurrentMonth()); + if (calMonth > dpMonth) { + if (calMonth - dpMonth < 6) + incrementMonth(calMonth - dpMonth); + else + decrementMonth(dpMonth - calMonth + 12); + } else if (dpMonth > calMonth) { + if (dpMonth - calMonth < 6) + decrementMonth(dpMonth - calMonth); + else + incrementMonth(calMonth - dpMonth + 12); + } + + // Adjust year + int dpYear = Integer.parseInt(getCurrentYear()); + if (calYear > dpYear) { + incrementYear(calYear - dpYear); + } else if (dpYear > calYear) { + decrementYear(dpYear - calYear); + } + } + + private static int toMonthNumber(String monthName) { + String months[] = new String[] {"January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December"}; + + for (int x = 0; x < months.length; x++) { + if (months[x].contains(monthName)) + return x; + } + + return 0; + } + + /** + * Get the number of days in the month + * @param year + * @param month + * @return + */ + private static int getDaysInMonth(int year, int month) { + Calendar cal = Calendar.getInstance(); + cal.set(Calendar.YEAR, year); + cal.set(Calendar.MONTH, month); + return cal.getActualMaximum(Calendar.DAY_OF_MONTH); + } +} diff --git a/uiautomator_test_libraries/src/com/android/uiautomator/common/helpers/TimePickerHelper.java b/uiautomator_test_libraries/src/com/android/uiautomator/common/helpers/TimePickerHelper.java new file mode 100644 index 0000000..c5e3add --- /dev/null +++ b/uiautomator_test_libraries/src/com/android/uiautomator/common/helpers/TimePickerHelper.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2013 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.common.helpers; + +import com.android.uiautomator.core.UiObject; +import com.android.uiautomator.core.UiObjectNotFoundException; +import com.android.uiautomator.core.UiSelector; + +import java.util.Calendar; +import java.util.Locale; + +/** + * Use this helper anywhere there is a time picker to manage. This helper + * will set time specified in a Calendar object. + */ +public class TimePickerHelper { + + public static final int HOUR = 0; + public static final int MINUTE = 1; + public static final int MERIDIEM = 2; + + public static String getCurrentHour() throws UiObjectNotFoundException { + return getNumberPickerField(HOUR).getText(); + } + + public static String getCurrentMinute() throws UiObjectNotFoundException { + return getNumberPickerField(MINUTE).getText(); + } + + public static String getCurrentMeridiem() throws UiObjectNotFoundException { + return getNumberPickerField(MERIDIEM).getText(); + } + + + public static void incrementHour() throws UiObjectNotFoundException { + incrementHour(1); + } + + public static void incrementHour(int count) throws UiObjectNotFoundException { + for (int x = 0; x < count; x++) + getNumberPickerIncrementButton(HOUR).click(); + } + + public static void decrementHour() throws UiObjectNotFoundException { + decrementHour(1); + } + + public static void decrementHour(int count) throws UiObjectNotFoundException { + for (int x = 0; x < count; x++) + getNumberPickerDecrementButton(HOUR).click(); + } + + public static void incrementMinute() throws UiObjectNotFoundException { + incrementMinute(1); + } + + public static void incrementMinute(int count) throws UiObjectNotFoundException { + for (int x = 0; x < count; x++) + getNumberPickerIncrementButton(MINUTE).click(); + } + + public static void decrementMinute() throws UiObjectNotFoundException { + decrementMinute(1); + } + + public static void decrementMinute(int count) throws UiObjectNotFoundException { + for (int x = 0; x < count; x++) + getNumberPickerDecrementButton(MINUTE).click(); + } + + public static void selectPM() throws UiObjectNotFoundException { + getNumberPicker(MERIDIEM).getChild(new UiSelector().text("PM")).click(); + } + + public static void selectAM() throws UiObjectNotFoundException { + getNumberPicker(MERIDIEM).getChild(new UiSelector().text("AM")).click(); + } + + public static UiObject getNumberPicker(int instance) { + return new UiObject(new UiSelector().className( + android.widget.NumberPicker.class.getName()).instance(instance)); + } + + public static UiObject getNumberPickerField(int instance) + throws UiObjectNotFoundException { + return getNumberPicker(instance).getChild( + new UiSelector().className(android.widget.EditText.class.getName())); + } + + public static UiObject getNumberPickerDecrementButton(int instance) + throws UiObjectNotFoundException { + return getNumberPicker(instance).getChild( + new UiSelector().className(android.widget.Button.class.getName()).instance(0)); + } + + public static UiObject getNumberPickerIncrementButton(int instance) + throws UiObjectNotFoundException { + return getNumberPicker(instance).getChild( + new UiSelector().className(android.widget.Button.class.getName()).instance(1)); + } + + public static void clickDone() throws UiObjectNotFoundException { + new UiObject(new UiSelector().text("Done")).click(); + } + + public static void setTime(Calendar cal) throws UiObjectNotFoundException { + // Adjust minutes - increment or decrement using the shortest path + int tpMinute = Integer.parseInt(getCurrentMinute()); + int calMinute = cal.get(Calendar.MINUTE); + if (calMinute > tpMinute) { + if (calMinute - tpMinute < 30) + incrementMinute(calMinute - tpMinute); + else + decrementMinute(tpMinute - calMinute + 60); + } else if (tpMinute > calMinute) { + if (tpMinute - calMinute < 30) + decrementMinute(tpMinute - calMinute); + else + incrementMinute(calMinute - tpMinute + 60); + } + + // Adjust hour - increment or decrement using the shortest path + int tpHour = Integer.parseInt(getCurrentHour()); + int calHour = cal.get(Calendar.HOUR); + if (calHour > tpHour) { + if (calHour - tpHour < 6) + incrementHour(calHour - tpHour); + else + decrementHour(tpHour - calHour + 12); + } else if (tpHour > calHour) { + if (tpHour - calHour < 6) + decrementHour(tpHour - calHour); + else + incrementHour(calHour - tpHour + 12); + } + + // Adjust meridiem + String calMer = cal.getDisplayName(Calendar.AM_PM, Calendar.SHORT, Locale.US); + String tpMer = getCurrentMeridiem(); + if (tpMer.equalsIgnoreCase(calMer)) + return; + + if (!calMer.equalsIgnoreCase("AM")) { + selectPM(); + } else { + selectAM(); + } + } +} diff --git a/uiautomator_test_libraries/src/com/android/uiautomator/platform/JankTestBase.java b/uiautomator_test_libraries/src/com/android/uiautomator/platform/JankTestBase.java new file mode 100644 index 0000000..6662f80 --- /dev/null +++ b/uiautomator_test_libraries/src/com/android/uiautomator/platform/JankTestBase.java @@ -0,0 +1,481 @@ +/* + * Copyright (C) 2013 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.platform; + +import android.os.Bundle; +import android.os.Environment; +import android.util.Log; + +import com.android.uiautomator.core.UiDevice; +import com.android.uiautomator.testrunner.UiAutomatorTestCase; + +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +/** + * Base class for jank test. + * All jank test needs to extend JankTestBase + */ +public class JankTestBase extends UiAutomatorTestCase { + private static final String TAG = JankTestBase.class.getSimpleName(); + + protected UiDevice mDevice; + protected TestWatchers mTestWatchers = null; + protected BufferedWriter mWriter = null; + protected BufferedWriter mStatusWriter = null; + protected int mIteration = 20; // default iteration is set 20 + /* can be used to enable/disable systrace in the test */ + protected int mTraceTime = 0; + protected Bundle mParams; + protected String mTestCaseName; + protected int mSuccessTestRuns = 0; + protected Thread mThread = null; + + // holds all params for the derived tests + private static final String PROPERTY_FILE_NAME = "UiJankinessTests.conf"; + private static final String PARAM_CONFIG = "conf"; + private static final String LOCAL_TMP_DIR = "/data/local/tmp/"; + // File that hold the test results + private static String OUTPUT_FILE_NAME = LOCAL_TMP_DIR + "UiJankinessTestsOutput.txt"; + // File that hold test status, e.g successful test iterations + private static String STATUS_FILE_NAME = LOCAL_TMP_DIR + "UiJankinessTestsStatus.txt"; + private static final String RAW_DATA_DIR = LOCAL_TMP_DIR + "UiJankinessRawData"; + + private static int SUCCESS_THRESHOLD = 80; + private static boolean DEBUG = false; + + /* default animation time is set to 2 seconds */ + protected static final long DEFAULT_ANIMATION_TIME = 2 * 1000; + /* default swipe steps for fling animation */ + protected static final int DEFAULT_FLING_STEPS = 8; + + /* Array to record jankiness data in each test iteration */ + private int[] jankinessArray; + /* Array to record frame rate in each test iteration */ + private double[] frameRateArray; + /* Array to save max accumulated frame number in each test iteration */ + private int[] maxDeltaVsyncArray; + /* Default file to store the systrace */ + private static final File SYSTRACE_DIR = new File(LOCAL_TMP_DIR, "systrace"); + /* Default trace file name */ + private static final String TRACE_FILE_NAME = "trace.txt"; + /* Default tracing time is 5 seconds */ + private static final int DEFAULT_TRACE_TIME = 5; // 5 seconds + // Command to dump compressed trace data + private static final String ATRACE_COMMAND = "atrace -z -t %d gfx input view sched freq"; + + /** + * Thread to capture systrace log from the test + */ + public class SystraceTracker implements Runnable { + File mFile = new File(SYSTRACE_DIR, TRACE_FILE_NAME); + int mTime = DEFAULT_TRACE_TIME; + + public SystraceTracker(int traceTime, String fileName) { + try { + if (!SYSTRACE_DIR.exists()) { + if (!SYSTRACE_DIR.mkdir()) { + log(String.format("create directory %s failed, you can manually create " + + "it and start the test again", SYSTRACE_DIR.getAbsolutePath())); + return; + } + } + } catch (SecurityException e) { + Log.e(TAG, "creating directory failed?", e); + } + + if (traceTime > 0) { + mTime = traceTime; + } + if (fileName != null) { + mFile = new File(SYSTRACE_DIR, fileName); + } + } + + @Override + public void run() { + String command = String.format(ATRACE_COMMAND, mTime); + Log.v(TAG, "command: " + command); + Process p = null; + InputStream in = null; + BufferedOutputStream out = null; + try { + p = Runtime.getRuntime().exec(command); + Log.v(TAG, "write systrace into file: " + mFile.getAbsolutePath()); + // read bytes from the process output stream as the output is compressed + byte[] buffer = new byte[1024]; + in = p.getInputStream(); + out = new BufferedOutputStream(new FileOutputStream(mFile)); + int n; + while ((n = in.read(buffer)) != -1) { + out.write(buffer, 0, n); + out.flush(); + } + in.close(); + out.close(); + // read error message + BufferedReader br = new BufferedReader(new InputStreamReader(p.getErrorStream())); + String line; + while ((line = br.readLine()) != null) { + Log.e(TAG, "Command return errors: " + line); + } + br.close(); + + // Due to limited buffer size for standard input and output stream, + // promptly reading from the input stream or output stream to avoid block + int status = p.waitFor(); + if (status != 0) { + Log.e(TAG, String.format("Run shell command: %s, status: %s", + command, status)); + } + } catch (InterruptedException e) { + Log.e(TAG, "Exception from command " + command + ":"); + Log.e(TAG, "Thread interrupted? ", e); + } catch (IOException e) { + Log.e(TAG, "Open file error: ", e); + } catch (IllegalThreadStateException e) { + Log.e(TAG, "the process has not exit yet ", e); + } + } + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + mDevice = UiDevice.getInstance(); + mTestWatchers = new TestWatchers(); // extends the common class UiWatchers + mTestWatchers.registerAnrAndCrashWatchers(); + + mWriter = new BufferedWriter(new FileWriter(new File(OUTPUT_FILE_NAME), true)); + mStatusWriter = new BufferedWriter(new FileWriter(new File(STATUS_FILE_NAME), true)); + + mParams = getParams(); + if (mParams != null && !mParams.isEmpty()) { + log("mParams is not empty, get properties."); + String mIterationStr = getPropertyString(mParams, "iteration"); + if (mIterationStr != null) { + mIteration = Integer.valueOf(mIterationStr); + } + String mTraceTimeStr = getPropertyString(mParams, "tracetime"); + if (mTraceTimeStr != null) { + mTraceTime = Integer.valueOf(mTraceTimeStr); + } + } + jankinessArray = new int[mIteration]; + frameRateArray = new double[mIteration]; + maxDeltaVsyncArray = new int[mIteration]; + mTestCaseName = this.getName(); + + mSuccessTestRuns = 0; + mDevice.pressHome(); + } + + /** + * Create a new thread for systrace and start the thread + * + * @param testCaseName + * @param iteration + */ + protected void startTrace(String testCaseName, int iteration) { + if (mTraceTime > 0) { + String outputFile = String.format("%s_%d_trace", mTestCaseName, iteration); + mThread = new Thread(new SystraceTracker(mTraceTime, outputFile)); + mThread.start(); + } + } + + /** + * Wait for the tracing thread to exit + */ + protected void endTrace() { + if (mThread != null) { + try { + mThread.join(); + } catch (InterruptedException e) { + Log.e(TAG, "wait for the trace thread to exit exception:", e); + } + } + } + + /** + * Expects a file from the command line via conf param or default following format each on its + * own line. <code> + * key=Value + * Browser_URL1=cnn.com + * Browser_URL2=google.com + * Camera_ShutterDelay=1000 + * etc... + * </code> + * @param Bundle params + * @param key + * @return the value of the property else defaultValue + * @throws FileNotFoundException + * @throws IOException + */ + protected String getPropertyString(Bundle params, String key) + throws FileNotFoundException, IOException { + Properties prop = new Properties(); + prop.load(new FileInputStream(new File(LOCAL_TMP_DIR, + params.getString(PARAM_CONFIG, PROPERTY_FILE_NAME)))); + String value = prop.getProperty(key); + if (value != null && !value.isEmpty()) + return value; + return null; + } + + /** + * Expects a file from the command line via conf param or default following format each on its + * own line. <code> + * key=Value + * Browser_URL1=cnn.com + * Browser_URL2=google.com + * Camera_ShutterDelay=1000 + * etc... + * </code> + * @param Bundle params + * @param key + * @return the value of the property else defaultValue + * @throws FileNotFoundException + * @throws IOException + */ + protected long getPropertyLong(Bundle params, String key) + throws FileNotFoundException, IOException { + Properties prop = new Properties(); + prop.load(new FileInputStream(new File(LOCAL_TMP_DIR, + params.getString(PARAM_CONFIG, PROPERTY_FILE_NAME)))); + String value = prop.getProperty(key); + if (value != null && !value.trim().isEmpty()) + return Long.valueOf(value.trim()); + return 0; + } + + /** + * Verify the test result by comparing data sample size with expected value + * @param expectedDataSize the expected data size + */ + protected boolean validateResults(int expectedDataSize) { + int receivedDataSize = SurfaceFlingerHelper.getDataSampleSize(); + return ((expectedDataSize > 0) && (receivedDataSize >= expectedDataSize)); + } + + /** + * Process the raw data, calculate jankiness, frame rate and max accumulated frames number + * @param testCaseName + * @param iteration + */ + protected void recordResults(String testCaseName, int iteration) { + long refreshPeriod = SurfaceFlingerHelper.getRefreshPeriod(); + // if the raw directory doesn't exit, create the directory + File rawDataDir = new File(RAW_DATA_DIR); + try { + if (!rawDataDir.exists()) { + if (!rawDataDir.mkdir()) { + log(String.format("create directory %s failed, you can manually create " + + "it and start the test again", rawDataDir)); + } + } + } catch (SecurityException e) { + Log.e(TAG, "create directory failed: ", e); + } + String rawFileName = String.format("%s/%s_%d.txt", RAW_DATA_DIR, testCaseName, iteration); + // write results into a file + BufferedWriter fw = null; + try { + fw = new BufferedWriter(new FileWriter(new File(rawFileName), false)); + fw.write(SurfaceFlingerHelper.getFrameBufferData()); + } catch (IOException e) { + Log.e(TAG, "failed to write to file", e); + return; + } finally { + try { + if (fw != null) { + fw.close(); + } + } + catch (IOException e) { + Log.e(TAG, "close file failed.", e); + } + } + + // get jankiness count + int jankinessCount = SurfaceFlingerHelper.getVsyncJankiness(); + // get frame rate + double frameRate = SurfaceFlingerHelper.getFrameRate(); + // get max accumulated frames + int maxDeltaVsync = SurfaceFlingerHelper.getMaxDeltaVsync(); + + // only record data when they are valid + if (jankinessCount >=0 && frameRate > 0) { + jankinessArray[iteration] = jankinessCount; + frameRateArray[iteration] = frameRate; + maxDeltaVsyncArray[iteration] = maxDeltaVsync; + mSuccessTestRuns++; + } + String msg = String.format("%s, iteration %d\n" + + "refresh period: %d\n" + + "jankiness count: %d\n" + + "frame rate: %f\n" + + "max accumulated frames: %d\n", + testCaseName, iteration, refreshPeriod, + jankinessCount, frameRate, maxDeltaVsync); + log(msg); + if (DEBUG) { + SurfaceFlingerHelper.printData(testCaseName, iteration); + } + } + + /** + * Process data from all test iterations, and save to disk + * @param testCaseName + */ + protected void saveResults(String testCaseName) { + // write test status into status file + try { + mStatusWriter.write(String.format("%s: %d success runs out of %d iterations\n", + testCaseName, mSuccessTestRuns, mIteration)); + } catch (IOException e) { + log("failed to write output for test case " + testCaseName); + } + + // if successful test runs is less than the threshold, no results will be saved. + if (mSuccessTestRuns * 100 / mIteration < SUCCESS_THRESHOLD) { + log(String.format("In %s, # of successful test runs out of %s iterations: %d ", + testCaseName, mIteration, mSuccessTestRuns)); + log(String.format("threshold is %d%%", SUCCESS_THRESHOLD)); + return; + } + + if (DEBUG) { + print(jankinessArray, "jankiness array"); + print(frameRateArray, "frame rate array"); + print(maxDeltaVsyncArray, "max delta vsync array"); + } + double avgJankinessCount = getAverage(jankinessArray); + int maxJankinessCount = getMaxValue(jankinessArray); + double avgFrameRate = getAverage(frameRateArray); + double avgMaxDeltaVsync = getAverage(maxDeltaVsyncArray); + + String avgMsg = String.format("%s\n" + + "average number of jankiness: %f\n" + + "max number of jankiness: %d\n" + + "average frame rate: %f\n" + + "average of max accumulated frames: %f\n", + testCaseName, avgJankinessCount, maxJankinessCount, avgFrameRate, avgMaxDeltaVsync); + log(avgMsg); + + try { + mWriter.write(avgMsg); + } catch (IOException e) { + log("failed to write output for test case " + testCaseName); + } + } + + // return the max value in an integer array + private int getMaxValue(int[] intArray) { + int index = 0; + int max = intArray[index]; + for (int i = 1; i < intArray.length; i++) { + if (max < intArray[i]) { + max = intArray[i]; + } + } + return max; + } + + private double getAverage(int[] intArray) { + int mean = 0; + int numberTests = 0; + for (int i = 0; i < intArray.length; i++) { + // in case in some iteration, test fails, no data points is collected + if (intArray[i] >= 0) { + mean += intArray[i]; + ++numberTests; + } + } + return (double)mean/numberTests; + } + + private double getAverage(double[] doubleArray) { + double mean = 0; + int numberTests = 0; + for (int i = 0; i < doubleArray.length; i++) { + // in case in some iteration, test fails, no data points is collected + if (doubleArray[i] >= 0) { + mean += doubleArray[i]; + ++numberTests; + } + } + return mean/numberTests; + } + + private void print(int[] intArray, String arrayName) { + log("start to print array for " + arrayName); + for (int i = 0; i < intArray.length; i++) { + log(String.format("%d: %d", i, intArray[i])); + } + } + + private void print(double[] doubleArray, String arrayName) { + log("start to print array for " + arrayName); + for (int i = 0; i < doubleArray.length; i++) { + log(String.format("%d: %f", i, doubleArray[i])); + } + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + if (mWriter != null) { + mWriter.close(); + } + if (mStatusWriter != null) { + mStatusWriter.close(); + } + } + + private void log(String message) { + Log.v(TAG, message); + } + + /** + * Set the total number of test iteration + * @param iteration + */ + protected void setIteration(int iteration){ + mIteration = iteration; + } + + /** + * Get the total number of test iteration + * @return iteration + */ + protected int getIteration(){ + return mIteration; + } +} diff --git a/uiautomator_test_libraries/src/com/android/uiautomator/platform/SurfaceFlingerHelper.java b/uiautomator_test_libraries/src/com/android/uiautomator/platform/SurfaceFlingerHelper.java new file mode 100644 index 0000000..748b433 --- /dev/null +++ b/uiautomator_test_libraries/src/com/android/uiautomator/platform/SurfaceFlingerHelper.java @@ -0,0 +1,421 @@ +/* + * Copyright (C) 2013 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.platform; + +import android.os.Environment; +import android.util.Log; + +import java.util.Arrays; +import java.util.List; +import java.util.ArrayList; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileWriter; +import java.io.BufferedWriter; +import java.io.InputStreamReader; +import java.io.IOException; +import java.lang.Math; + +/* + * Tools to measure jankiness through SurfaceFlinger + */ +public class SurfaceFlingerHelper { + private static String TAG = "SurfaceFlingerHelper"; + private static int BUFFER_SIZE = 128; + private static int BUFFER_NUMBER = 3; + private static String CLEAR_BUFFER_CMD = "dumpsys SurfaceFlinger --latency-clear"; + private static String FRAME_LATENCY_CMD = "dumpsys SurfaceFlinger --latency"; + private static final String RAW_DATA_DIR = "UiJankinessRawData"; + private static final String LOCAL_TMP_DIR = "/data/local/tmp/"; + /* If the latency between two frames is greater than this number, it it treated as a pause + * not a jankiness */ + private static final int PAUSE_LATENCY = 20; + + /* An array list which includes the raw buffer information from frame latency tool */ + private static List<List<String>> mFrameBufferData = new ArrayList<List<String>>(BUFFER_SIZE); + + /* Record the refresh period returned from driver */ + private static long mRefreshPeriod = -1; + + /* Record the size of frame latency */ + private static int mFrameLatencySampleSize = 0; + + /* An integer array which includes delta vsync */ + private static long[] mDeltaVsync = new long[BUFFER_SIZE]; + + /* Integer array for delta of delta vsync */ + private static long[] mDelta2Vsync = new long[BUFFER_SIZE]; + + /* the maximum delta vsync number */ + private static long mMaxDeltaVsync; + + /* Normalized data */ + private static double[] mNormalizedDelta2Vsync = new double[BUFFER_SIZE]; + private static int[] mRoundNormalizedDelta2Vsync = new int[BUFFER_SIZE]; + // Symbol of unfinished frame time */ + public static final String PENDING_FENCE_TIME = new Long(Long.MAX_VALUE).toString(); + + /** + * Run clear buffer command and clear the saved frame buffer results + * + * @param windowName the window name that the buffer will be cleared + */ + public static void clearBuffer(String windowName) { + // clear results + if (mFrameBufferData != null) { + mFrameBufferData.clear(); + } + Arrays.fill(mDeltaVsync, -1); + Arrays.fill(mDelta2Vsync, -1); + Arrays.fill(mNormalizedDelta2Vsync, -1.0); + Arrays.fill(mRoundNormalizedDelta2Vsync, -1); + mRefreshPeriod = -1; + mFrameLatencySampleSize = 0; + mMaxDeltaVsync = 0; + + Process p = null; + BufferedReader resultReader = null; + String command = CLEAR_BUFFER_CMD; + if (windowName != null) { + command = String.format("%s %s", CLEAR_BUFFER_CMD, windowName); + } + try { + p = Runtime.getRuntime().exec(command); + int status = p.waitFor(); + if (status != 0) { + Log.e(TAG, String.format("Run shell command: %s, status: %s", + command, status)); + } + } catch (IOException e) { + Log.e(TAG, "// Exception from command " + command + ":", e); + } catch (InterruptedException e) { + Log.e(TAG, "// Interrupted while waiting for the command to finish. ", e); + } finally { + try { + if (resultReader != null) { + resultReader.close(); + } + if (p != null) { + p.destroy(); + } + } catch (IOException e) { + Log.e(TAG, "exception " + e); + } + } + } + + /** + * Run frame latency command without ignoring pending fence time + * + * @param windowName the window name which SurfaceFlinger will acquire frame time for + */ + public static boolean dumpFrameLatency(String windowName) { + return dumpFrameLatency(windowName, false); + } + + /** + * Run frame latency command to get frame time + * + * @param windowName the window name which SurfaceFlinger will get frame time for + * @param ignorePendingFenceTime flag to process frames with pending fence time + * set true to ignore pending fence time + * set false to fail the test if pending fence time is not allowed + */ + public static boolean dumpFrameLatency(String windowName, boolean ignorePendingFenceTime) { + Process p = null; + BufferedReader resultReader = null; + String command = FRAME_LATENCY_CMD; + if (windowName != null) { + command = String.format("%s %s", FRAME_LATENCY_CMD, windowName); + } + log("dump frame latency command: " + command); + try { + p = Runtime.getRuntime().exec(command); + int status = p.waitFor(); + if (status != 0) { + Log.e(TAG, String.format("Run shell command: %s, status: %s",command, status)); + } + resultReader = new BufferedReader(new InputStreamReader(p.getInputStream())); + String line = resultReader.readLine(); + mRefreshPeriod = Long.parseLong(line.trim()); + log("reading refresh period: " + mRefreshPeriod); + if (mRefreshPeriod < 0) { + return false; + } + boolean dataInvalidFlag = false; + while((line = resultReader.readLine()) != null) { + // remove lines which are empty + if (line.trim().isEmpty()) { + break; + } + String[] bufferValues = line.split("\\s+"); + if (bufferValues[0].trim().compareTo("0") == 0) { + continue; + } else if (bufferValues[1].trim().compareTo(PENDING_FENCE_TIME) == 0 ) { + if (ignorePendingFenceTime) { + log("ignore pending fence time"); + dataInvalidFlag = true; + } else { + log("the data contains unfinished frame time, please allow the animation" + + " to finish in the test before calling dumpFrameLatency."); + return false; + } + } + // store raw data which could have both valid and invalid data + List<String> delayArray = Arrays.asList(bufferValues); + mFrameBufferData.add(delayArray); + if (!dataInvalidFlag) { + // only count frames which have valid time + ++mFrameLatencySampleSize; + } + } + log("frame latency sample size: " + mFrameLatencySampleSize); + } catch (InterruptedException e) { + Log.e(TAG, "// Exception from command " + command + ":", e); + } catch (IOException e) { + Log.e(TAG, "Open file error: ", e); + return false; + } + finally { + try { + if (resultReader != null) { + resultReader.close(); + } + if (p != null) { + p.destroy(); + } + } catch (IOException e) { + Log.e(TAG, "io exception: ", e); + } + } + return true; + } + + public static int getDataSampleSize() { + return mFrameLatencySampleSize; + } + + public static long getRefreshPeriod() { + if (mRefreshPeriod < 0) { + // Haven't dump the frame latency yet + log("Run command \"" + FRAME_LATENCY_CMD + " \" before retrieving the refresh period"); + } + return mRefreshPeriod; + } + + public static String getFrameBufferData() { + if (mFrameBufferData.get(0) == null) { + log("Run command \"" + FRAME_LATENCY_CMD + " \" before retrieving frame buffer data"); + return null; + } + String rawData = String.format("%d\n", mRefreshPeriod); + List<String> tempList = new ArrayList<String>(BUFFER_NUMBER); + for (int i = 0; i < mFrameBufferData.size(); i++) { + tempList = mFrameBufferData.get(i); + for (int j = 0; j < BUFFER_NUMBER; j++) { + rawData += String.format("%s", tempList.get(j)); + if (j < BUFFER_NUMBER - 1) { + rawData += "\t"; + } else { + rawData += "\n"; + } + } + } + return rawData; + } + + /** + * Calculate delta(vsync) + * @return + */ + public static long[] getDeltaVsync() { + if (mRefreshPeriod < 0) { + log("Run command \"" + FRAME_LATENCY_CMD + " \" before retrieving frame latency"); + return null; + } + if (mDeltaVsync[0] < 0 ) { + // keep a record of the max DeltaVsync + mMaxDeltaVsync = 0; + // get the first frame vsync time + long preVsyncTime = Long.parseLong(mFrameBufferData.get(0).get(1)); + for (int i = 0; i < mFrameLatencySampleSize - 1; i++) { + long curVsyncTime = Long.parseLong(mFrameBufferData.get(i + 1).get(1)); + mDeltaVsync[i] = curVsyncTime - preVsyncTime; + preVsyncTime = curVsyncTime; + if (mMaxDeltaVsync < mDeltaVsync[i]) { + mMaxDeltaVsync = mDeltaVsync[i]; + } + } + } + return mDeltaVsync; + } + + /** + * Calculate difference between delta vsync + * @return + */ + public static long[] getDelta2Vsync() { + if (mRefreshPeriod < 0) { + log("Run command \"" + FRAME_LATENCY_CMD + " \" before retrieving frame latency"); + return null; + } + if (mDeltaVsync[0] < 0) { + getDeltaVsync(); + } + if (mDelta2Vsync[0] < 0) { + int numDeltaVsync = mFrameLatencySampleSize - 1; + for (int i = 0; i < numDeltaVsync - 1; i++) { + mDelta2Vsync[i] = mDeltaVsync[i + 1] - mDeltaVsync[i]; + } + } + return mDelta2Vsync; + } + + /** + * normalized delta(delta(vsync)) by refresh period + * @return + */ + public static double[] getNormalizedDelta2Vsync() { + if (mRefreshPeriod < 0) { + log("Run command \"" + FRAME_LATENCY_CMD + " \" before retrieving frame latency"); + return null; + } + if (mDelta2Vsync[0] < 0) { + getDelta2Vsync(); + } + if (mNormalizedDelta2Vsync[0] < 0) { + for (int i = 0; i < mFrameLatencySampleSize - 2; i++) { + mNormalizedDelta2Vsync[i] = (double)mDelta2Vsync[i] /mRefreshPeriod; + } + } + return mNormalizedDelta2Vsync; + } + + public static int[] getRoundNormalizedDelta2Vsync() { + if (mRefreshPeriod < 0) { + log("Run command \"" + FRAME_LATENCY_CMD + " \" for number of jankiness."); + return null; + } + if (mNormalizedDelta2Vsync[0] < 0) { + getNormalizedDelta2Vsync(); + } + + for (int i = 0; i < mFrameLatencySampleSize - 2; i++) { + int value = (int)Math.round(Math.max(mNormalizedDelta2Vsync[i], 0.0)); + mRoundNormalizedDelta2Vsync[i] = value; + } + return mRoundNormalizedDelta2Vsync; + } + + /* + * Get number of jankiness using Vsync time difference + */ + public static int getVsyncJankiness() { + if (mRefreshPeriod < 0) { + log("Run command \"" + FRAME_LATENCY_CMD + " \" for number of jankiness."); + return -1; + } + if (mRoundNormalizedDelta2Vsync[0] < 0) { + getRoundNormalizedDelta2Vsync(); + } + int numberJankiness = 0; + for (int i = 0; i < mFrameLatencySampleSize - 2; i++) { + int value = mRoundNormalizedDelta2Vsync[i]; + // ignore the latency which is too long + if (value > 0 && value < PAUSE_LATENCY) { + numberJankiness++; + } + } + return numberJankiness; + } + + /* Track the maximum delta which shows the accumulating time + * before animation starts */ + public static int getMaxDeltaVsync() { + return Math.round((float)mMaxDeltaVsync /mRefreshPeriod); + } + + /** + * Calculate frame rate + * @return + */ + public static double getFrameRate() { + if (mRefreshPeriod < 0) { + log("Run command \"" + FRAME_LATENCY_CMD + " \" before calcuating average frame rate"); + return -1.0; + } + if (mFrameBufferData.get(0) == null) { + log("Run command \"" + FRAME_LATENCY_CMD + " \" before retrieving frame buffer data"); + return -1.0; + } + long startTime = Long.parseLong(mFrameBufferData.get(0).get(1)); + long endTime = Long.parseLong(mFrameBufferData.get(mFrameLatencySampleSize - 1).get(1)); + long totalDuration = endTime - startTime; + return (double)((mFrameLatencySampleSize - 1) * Math.pow(10, 9))/totalDuration; + } + + /** + * Print raw data and processed results into file <testcasename_[iteration]_processed.txt> + * @param fileName + * @param index + */ + public static void printData(String fileName, int index) { + String rawAndProcDataFileName = String.format("%s/%s_%d_processed.txt", RAW_DATA_DIR, + fileName, index); + log("write raw data and process data into file: " + rawAndProcDataFileName); + BufferedWriter fw = null; + try { + fw = new BufferedWriter(new FileWriter(new File(rawAndProcDataFileName), false)); + // Show the number of jankiness first: + fw.write(String.format("Jankiness count: %d\n", getVsyncJankiness())); + fw.write(String.format("Max accumulated frames: %d\n", getMaxDeltaVsync())); + fw.write(String.format("Frame rate is: %f\n", getFrameRate())); + + // refresh period + fw.write(String.valueOf(mRefreshPeriod)); + fw.write("\n"); + fw.write("app\tvsync\tset\tdelta(vsync)\tdelta^2(vsync)\t" + + "delta^2(vsync)/refreshPeriod\t normalized delta^2(vsync)\n"); + + for (int i = 0; i < mFrameLatencySampleSize; i++) { + // write raw data + List<String> rawData = mFrameBufferData.get(i); + String line = String.format("%s\t%s\t%s\t%d\t%d\t%f\t%d\n", + rawData.get(0), rawData.get(1), rawData.get(2), + mDeltaVsync[i], mDelta2Vsync[i], + mNormalizedDelta2Vsync[i], mRoundNormalizedDelta2Vsync[i]); + fw.write(line); + } + } catch (IOException e) { + log("Open file error: " + e.toString()); + } finally { + try { + if (fw != null) { + fw.flush(); + fw.close(); + } + } + catch (IOException e) { + Log.e(TAG, "close file exception: ", e); + } + } + } + + private static void log(String msg) { + Log.v(TAG, msg); + } +} diff --git a/uiautomator_test_libraries/src/com/android/uiautomator/platform/TestWatchers.java b/uiautomator_test_libraries/src/com/android/uiautomator/platform/TestWatchers.java new file mode 100644 index 0000000..5cab83a --- /dev/null +++ b/uiautomator_test_libraries/src/com/android/uiautomator/platform/TestWatchers.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2013 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.platform; + +import com.android.uiautomator.common.UiWatchers; + +public class TestWatchers extends UiWatchers { + private String TAG = "TestWatchers"; + + @Override + public void onAnrDetected(String errorText) { + // The ANR dialog is still open now and upon returning from here + // it will automatically get closed. See UiWatchers or implement + // your handlers directly. + super.onAnrDetected("ANR:" + errorText); + } + + @Override + public void onCrashDetected(String errorText) { + // what do we need to do here? + // The Crash dialog is still open now and upon returning from here + // it will automatically get closed. See UiWatchers or implement + // your handlers directly. + super.onCrashDetected("CRASH:" + errorText); + } +} diff --git a/utils/Android.mk b/utils/Android.mk new file mode 100644 index 0000000..c141484 --- /dev/null +++ b/utils/Android.mk @@ -0,0 +1,17 @@ +# +# 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. +# + +include $(call all-subdir-makefiles) diff --git a/utils/DummyIME/Android.mk b/utils/DummyIME/Android.mk new file mode 100644 index 0000000..c8d9f87 --- /dev/null +++ b/utils/DummyIME/Android.mk @@ -0,0 +1,26 @@ +# +# 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. +# + +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := tests + +LOCAL_SRC_FILES := $(call all-java-files-under, src) + +LOCAL_PACKAGE_NAME := DummyIME + +include $(BUILD_PACKAGE) diff --git a/utils/DummyIME/AndroidManifest.xml b/utils/DummyIME/AndroidManifest.xml new file mode 100644 index 0000000..fd17a52 --- /dev/null +++ b/utils/DummyIME/AndroidManifest.xml @@ -0,0 +1,35 @@ +<!-- +/* + * Copyright 2006, 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. + */ +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.testing.dummyime"> + <application android:label="Dummy IME"> + <service android:name="DummyIme" + android:permission="android.permission.BIND_INPUT_METHOD"> + <intent-filter> + <action android:name="android.view.InputMethod" /> + </intent-filter> + <meta-data android:name="android.view.im" android:resource="@xml/method" /> + </service> + <activity android:name=".ImePreferences" android:label="Dummy IME Settings"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/utils/DummyIME/res/xml/method.xml b/utils/DummyIME/res/xml/method.xml new file mode 100644 index 0000000..43a330e --- /dev/null +++ b/utils/DummyIME/res/xml/method.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/** + * 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. + */ +--> + +<!-- The attributes in this XML file provide configuration information --> +<!-- for the Search Manager. --> + +<input-method xmlns:android="http://schemas.android.com/apk/res/android" + android:settingsActivity="com.android.testing.dummyime.ImePreferences"> + <subtype + android:label="Generic" + android:imeSubtypeLocale="en_US" + android:imeSubtypeMode="keyboard" /> +</input-method>
\ No newline at end of file diff --git a/utils/DummyIME/src/com/android/testing/dummyime/DummyIme.java b/utils/DummyIME/src/com/android/testing/dummyime/DummyIme.java new file mode 100644 index 0000000..7b7a39a --- /dev/null +++ b/utils/DummyIME/src/com/android/testing/dummyime/DummyIme.java @@ -0,0 +1,35 @@ +/* + * 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.testing.dummyime; + +import android.inputmethodservice.InputMethodService; + +/** + * Dummy IME implementation that basically does nothing + */ +public class DummyIme extends InputMethodService { + + @Override + public boolean onEvaluateFullscreenMode() { + return false; + } + + @Override + public boolean onEvaluateInputViewShown() { + return false; + } +} diff --git a/utils/DummyIME/src/com/android/testing/dummyime/ImePreferences.java b/utils/DummyIME/src/com/android/testing/dummyime/ImePreferences.java new file mode 100644 index 0000000..41036ab --- /dev/null +++ b/utils/DummyIME/src/com/android/testing/dummyime/ImePreferences.java @@ -0,0 +1,26 @@ +/* + * 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.testing.dummyime; + +import android.preference.PreferenceActivity; + +/** + * Dummy IME preference activity + */ +public class ImePreferences extends PreferenceActivity { + +} |