diff options
author | Svetoslav Ganov <svetoslavganov@google.com> | 2013-01-02 10:24:19 -0800 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2013-01-23 00:19:48 +0000 |
commit | 89f6117cb1fbeab3770106cf54e05af1f597be81 (patch) | |
tree | 77554dd4e08ee6a0a37d984788199c8bfb380382 /uiautomator | |
parent | a1c9eab767cbfad143c024d171cf9cff30acac42 (diff) | |
download | testing-89f6117cb1fbeab3770106cf54e05af1f597be81.tar.gz |
Refactoring of UiAutomator to use the new UI test APIs.
Change-Id: If3445b0b4fd3aad66a5cd661e81f3639bff90dba
Diffstat (limited to 'uiautomator')
14 files changed, 549 insertions, 255 deletions
diff --git a/uiautomator/api/current.txt b/uiautomator/api/current.txt index a1d80c4..cf78296 100644 --- a/uiautomator/api/current.txt +++ b/uiautomator/api/current.txt @@ -166,6 +166,7 @@ package com.android.uiautomator.core { 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); + method public com.android.uiautomator.core.UiSelector viewId(java.lang.String); } public abstract interface UiWatcher { @@ -180,6 +181,18 @@ package com.android.uiautomator.testrunner { method public abstract void sendStatus(int, android.os.Bundle); } + public class OnDeviceUiTestCase extends android.test.UiTestCase { + ctor public OnDeviceUiTestCase(); + 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); + } + + public class OnDeviceUiTestRunner extends android.test.InstrumentationTestRunner { + ctor public OnDeviceUiTestRunner(); + } + public class UiAutomatorTestCase extends junit.framework.TestCase { ctor public UiAutomatorTestCase(); method public com.android.uiautomator.testrunner.IAutomationSupport getAutomationSupport(); diff --git a/uiautomator/cmds/uiautomator/src/com/android/commands/uiautomator/DumpCommand.java b/uiautomator/cmds/uiautomator/src/com/android/commands/uiautomator/DumpCommand.java index cceef88..6f5ac1c 100644 --- a/uiautomator/cmds/uiautomator/src/com/android/commands/uiautomator/DumpCommand.java +++ b/uiautomator/cmds/uiautomator/src/com/android/commands/uiautomator/DumpCommand.java @@ -16,14 +16,16 @@ package com.android.commands.uiautomator; -import android.accessibilityservice.UiTestAutomationBridge; +import android.app.UiAutomation; import android.os.Environment; 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 @@ -57,21 +59,27 @@ public class DumpCommand extends Command { if (args.length > 0) { dumpFile = new File(args[0]); } - UiTestAutomationBridge bridge = new UiTestAutomationBridge(); - bridge.connect(); + UiAutomationShellWrapper automationWrapper = new UiAutomationShellWrapper(); + automationWrapper.connect(); // 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. - bridge.waitForIdle(1000, 1000 * 10); - AccessibilityNodeInfo info = bridge.getRootAccessibilityNodeInfoInActiveWindow(); - if (info == null) { - System.err.println("ERROR: null root node returned by UiTestAutomationBridge."); + 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; + } + AccessibilityNodeInfoDumper.dumpWindowToFile(info, dumpFile); + } catch (TimeoutException re) { + System.err.println("ERROR: could not get idle state."); return; + } finally { + automationWrapper.disconnect(); } - AccessibilityNodeInfoDumper.dumpWindowToFile(info, dumpFile); - bridge.disconnect(); System.out.println( String.format("UI hierchary dumped to: %s", dumpFile.getAbsolutePath())); } - } diff --git a/uiautomator/cmds/uiautomator/src/com/android/commands/uiautomator/EventsCommand.java b/uiautomator/cmds/uiautomator/src/com/android/commands/uiautomator/EventsCommand.java index 79428e9..ce55f18 100644 --- a/uiautomator/cmds/uiautomator/src/com/android/commands/uiautomator/EventsCommand.java +++ b/uiautomator/cmds/uiautomator/src/com/android/commands/uiautomator/EventsCommand.java @@ -16,10 +16,11 @@ package com.android.commands.uiautomator; -import android.accessibilityservice.UiTestAutomationBridge; +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; @@ -49,15 +50,17 @@ public class EventsCommand extends Command { @Override public void run(String[] args) { - final UiTestAutomationBridge bridge = new UiTestAutomationBridge() { + 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())); } - }; - bridge.connect(); + }); // there's really no way to stop, essentially we just block indefinitely here and wait // for user to press Ctrl+C synchronized (mQuitLock) { @@ -67,5 +70,6 @@ public class EventsCommand extends Command { e.printStackTrace(); } } + automationWrapper.disconnect(); } } diff --git a/uiautomator/library/src/com/android/uiautomator/core/InteractionController.java b/uiautomator/library/src/com/android/uiautomator/core/InteractionController.java index 93a162e..e712559 100644 --- a/uiautomator/library/src/com/android/uiautomator/core/InteractionController.java +++ b/uiautomator/library/src/com/android/uiautomator/core/InteractionController.java @@ -16,22 +16,13 @@ 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.graphics.Point; -import android.hardware.input.InputManager; -import android.os.Binder; -import android.os.IBinder; import android.os.IPowerManager; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; -import android.os.UserHandle; -import android.provider.Settings; import android.util.Log; import android.view.IWindowManager; import android.view.InputDevice; @@ -39,7 +30,6 @@ import android.view.InputEvent; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.MotionEvent; -import android.view.Surface; import android.view.accessibility.AccessibilityEvent; import com.android.internal.util.Predicate; @@ -69,8 +59,6 @@ class InteractionController { private final IWindowManager mWindowManager; - private final long mLongPressTimeout; - private static final long REGULAR_CLICK_LENGTH = 100; private long mDownTime; @@ -85,54 +73,6 @@ class InteractionController { throw new RuntimeException("Unable to connect to WindowManager, " + "is the system running?"); } - - // the value returned is on the border of going undetected as used - // by this framework during long presses. Adding few extra 100ms - // of long press time helps ensure long enough time for a valid - // longClick detection. - mLongPressTimeout = getSystemLongPressTime() * 2 + 100; - } - - /** - * Get the system long press time - * @return milliseconds - */ - private 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; } /** @@ -170,7 +110,6 @@ class InteractionController { Boolean.toString(waitForAll), eventTypes); Log.d(LOG_TAG, logString); - mUiAutomatorBridge.setOperationTime(); Runnable command = new Runnable() { @Override public void run() { @@ -266,7 +205,6 @@ class InteractionController { */ public boolean sendKeyAndWaitForEvent(final int keyCode, final int metaState, final int eventType, long timeout) { - mUiAutomatorBridge.setOperationTime(); Runnable command = new Runnable() { @Override public void run() { @@ -295,7 +233,6 @@ class InteractionController { */ public boolean click(int x, int y) { Log.d(LOG_TAG, "click (" + x + ", " + y + ")"); - mUiAutomatorBridge.setOperationTime(); if (touchDown(x, y)) { SystemClock.sleep(REGULAR_CLICK_LENGTH); @@ -325,9 +262,8 @@ class InteractionController { Log.d(LOG_TAG, "longTap (" + x + ", " + y + ")"); } - mUiAutomatorBridge.setOperationTime(); if (touchDown(x, y)) { - SystemClock.sleep(mLongPressTimeout); + SystemClock.sleep(mUiAutomatorBridge.getSystemLongPressTime()); if(touchUp(x, y)) { return true; } @@ -510,7 +446,6 @@ class InteractionController { Log.d(LOG_TAG, "sendText (" + text + ")"); } - mUiAutomatorBridge.setOperationTime(); KeyEvent[] events = mKeyCharacterMap.getEvents(text.toCharArray()); if (events != null) { for (KeyEvent event2 : events) { @@ -534,7 +469,6 @@ class InteractionController { Log.d(LOG_TAG, "sendKey (" + keyCode + ", " + metaState + ")"); } - mUiAutomatorBridge.setOperationTime(); final long eventTime = SystemClock.uptimeMillis(); KeyEvent downEvent = KeyEvent.obtain(eventTime, eventTime, KeyEvent.ACTION_DOWN, keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, @@ -557,8 +491,8 @@ class InteractionController { * @throws RemoteException */ public boolean isNaturalRotation() throws RemoteException { - return mWindowManager.getRotation() == Surface.ROTATION_0 - || mWindowManager.getRotation() == Surface.ROTATION_180; + return mWindowManager.getRotation() == UiAutomation.ROTATION_FREEZE_0 + || mWindowManager.getRotation() == UiAutomation.ROTATION_FREEZE_180; } /** @@ -569,8 +503,8 @@ class InteractionController { * depending on the current physical position of the test device. * @throws RemoteException */ - public void setRotationRight() throws RemoteException { - mWindowManager.freezeRotation(Surface.ROTATION_270); + public void setRotationRight() { + mUiAutomatorBridge.setRotation(UiAutomation.ROTATION_FREEZE_270); } /** @@ -581,8 +515,8 @@ class InteractionController { * depending on the current physical position of the test device. * @throws RemoteException */ - public void setRotationLeft() throws RemoteException { - mWindowManager.freezeRotation(Surface.ROTATION_90); + public void setRotationLeft() { + mUiAutomatorBridge.setRotation(UiAutomation.ROTATION_FREEZE_90); } /** @@ -593,8 +527,8 @@ class InteractionController { * depending on the current physical position of the test device. * @throws RemoteException */ - public void setRotationNatural() throws RemoteException { - mWindowManager.freezeRotation(Surface.ROTATION_0); + public void setRotationNatural() { + mUiAutomatorBridge.setRotation(UiAutomation.ROTATION_FREEZE_0); } /** @@ -602,8 +536,8 @@ class InteractionController { * current rotation state. * @throws RemoteException */ - public void freezeRotation() throws RemoteException { - mWindowManager.freezeRotation(-1); + public void freezeRotation() { + mUiAutomatorBridge.setRotation(UiAutomation.ROTATION_FREEZE_CURRENT); } /** @@ -611,8 +545,8 @@ class InteractionController { * allowing its contents to rotate with the device physical rotation. * @throws RemoteException */ - public void unfreezeRotation() throws RemoteException { - mWindowManager.thawRotation(); + public void unfreezeRotation() { + mUiAutomatorBridge.setRotation(UiAutomation.ROTATION_UNFREEZE); } /** @@ -654,8 +588,7 @@ class InteractionController { return pm.isScreenOn(); } - private static boolean injectEventSync(InputEvent event) { - return InputManager.getInstance().injectInputEvent(event, - InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH); + private boolean injectEventSync(InputEvent event) { + return mUiAutomatorBridge.injectInputEvent(event, true); } } diff --git a/uiautomator/library/src/com/android/uiautomator/core/OnDeviceUiAutomatorBridge.java b/uiautomator/library/src/com/android/uiautomator/core/OnDeviceUiAutomatorBridge.java new file mode 100644 index 0000000..a668cf5 --- /dev/null +++ b/uiautomator/library/src/com/android/uiautomator/core/OnDeviceUiAutomatorBridge.java @@ -0,0 +1,44 @@ +/* + * 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.Service; +import android.app.UiAutomation; +import android.content.Context; +import android.view.Display; +import android.view.ViewConfiguration; +import android.view.WindowManager; + +class OnDeviceUiAutomatorBridge extends UiAutomatorBridge { + + private final Context mContext; + + public OnDeviceUiAutomatorBridge(Context context, UiAutomation uiAutomation) { + super(uiAutomation); + mContext = context; + } + + public Display getDefaultDisplay() { + WindowManager windowManager = (WindowManager) + mContext.getSystemService(Service.WINDOW_SERVICE); + return windowManager.getDefaultDisplay(); + } + + public long getSystemLongPressTime() { + return ViewConfiguration.getLongPressTimeout(); + } +} diff --git a/uiautomator/library/src/com/android/uiautomator/core/QueryController.java b/uiautomator/library/src/com/android/uiautomator/core/QueryController.java index 0af603a..6931528 100644 --- a/uiautomator/library/src/com/android/uiautomator/core/QueryController.java +++ b/uiautomator/library/src/com/android/uiautomator/core/QueryController.java @@ -15,12 +15,12 @@ */ 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; -import com.android.uiautomator.core.UiAutomatorBridge.AccessibilityEventListener; /** * The QueryController main purpose is to translate a {@link UiSelector} selectors to @@ -55,7 +55,7 @@ class QueryController { public QueryController(UiAutomatorBridge bridge) { mUiAutomatorBridge = bridge; - bridge.addAccessibilityEventListener(new AccessibilityEventListener() { + bridge.setOnAccessibilityEventListener(new OnAccessibilityEventListener() { @Override public void onAccessibilityEvent(AccessibilityEvent event) { synchronized (mLock) { @@ -169,7 +169,7 @@ class QueryController { final long waitInterval = 250; AccessibilityNodeInfo rootNode = null; for(int x = 0; x < maxRetry; x++) { - rootNode = mUiAutomatorBridge.getRootAccessibilityNodeInfoInActiveWindow(); + rootNode = mUiAutomatorBridge.getRootInActiveWindow(); if (rootNode != null) { return rootNode; } @@ -480,7 +480,7 @@ class QueryController { } public AccessibilityNodeInfo getAccessibilityRootNode() { - return mUiAutomatorBridge.getRootAccessibilityNodeInfoInActiveWindow(); + return mUiAutomatorBridge.getRootInActiveWindow(); } /** diff --git a/uiautomator/library/src/com/android/uiautomator/core/ShellUiAutomatorBridge.java b/uiautomator/library/src/com/android/uiautomator/core/ShellUiAutomatorBridge.java new file mode 100644 index 0000000..ebf99f2 --- /dev/null +++ b/uiautomator/library/src/com/android/uiautomator/core/ShellUiAutomatorBridge.java @@ -0,0 +1,87 @@ +/* + * 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.UiAutomation; +import android.app.IActivityManager.ContentProviderHolder; +import android.content.IContentProvider; +import android.database.Cursor; +import android.hardware.display.DisplayManagerGlobal; +import android.os.Binder; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.Log; +import android.view.Display; + +class ShellUiAutomatorBridge extends UiAutomatorBridge { + + private static final String LOG_TAG = ShellUiAutomatorBridge.class.getSimpleName(); + + 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; + } +} diff --git a/uiautomator/library/src/com/android/uiautomator/core/UiAutomationShellWrapper.java b/uiautomator/library/src/com/android/uiautomator/core/UiAutomationShellWrapper.java new file mode 100644 index 0000000..eb43cb5 --- /dev/null +++ b/uiautomator/library/src/com/android/uiautomator/core/UiAutomationShellWrapper.java @@ -0,0 +1,36 @@ +package com.android.uiautomator.core; + +import android.app.UiAutomation; +import android.app.UiAutomationConnection; +import android.os.HandlerThread; + +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(); + } + + public void disconnect() { + if (!mHandlerThread.isAlive()) { + throw new IllegalStateException("Already disconnected!"); + } + mUiAutomation.disconnect(); + mHandlerThread.quit(); + } + + public UiAutomation getUiAutomation() { + return mUiAutomation; + } +} diff --git a/uiautomator/library/src/com/android/uiautomator/core/UiAutomatorBridge.java b/uiautomator/library/src/com/android/uiautomator/core/UiAutomatorBridge.java index 90aa4df..0387a62 100644 --- a/uiautomator/library/src/com/android/uiautomator/core/UiAutomatorBridge.java +++ b/uiautomator/library/src/com/android/uiautomator/core/UiAutomatorBridge.java @@ -1,82 +1,50 @@ -/* - * 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 com.android.internal.util.Predicate; - -import android.accessibilityservice.UiTestAutomationBridge; -import android.os.SystemClock; +import android.app.UiAutomation; +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 com.android.internal.util.Predicate; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; import java.util.concurrent.TimeoutException; -class UiAutomatorBridge extends UiTestAutomationBridge { +abstract class UiAutomatorBridge { - private static final String LOGTAG = UiAutomatorBridge.class.getSimpleName(); + 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. + /** + * 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 value is used to wait for the UI to go busy after an action. This has little - // bearing on the appearance of test execution speeds. This value is used as a maximum - // time to wait for busy state where it is possible to occur much sooner. - private static final long WAIT_TIME_FROM_IDLE_TO_BUSY_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... + /** + * 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 - // Poll time used to check for last accessibility event time - private static final long BUSY_STATE_POLL_TIME = 50; //ms - - private final CopyOnWriteArrayList<AccessibilityEventListener> mListeners = - new CopyOnWriteArrayList<AccessibilityEventListener>(); - - private final Object mLock = new Object(); + private final UiAutomation mUiAutomation; private final InteractionController mInteractionController; private final QueryController mQueryController; - private long mLastEventTime = 0; - private long mLastOperationTime = 0; - - private volatile boolean mWaitingForEventDelivery; - - public static final long TIMEOUT_ASYNC_PROCESSING = 5000; - - private final LinkedBlockingQueue<AccessibilityEvent> mEventQueue = - new LinkedBlockingQueue<AccessibilityEvent>(10); - - public interface AccessibilityEventListener { - public void onAccessibilityEvent(AccessibilityEvent event); - } - - UiAutomatorBridge() { + UiAutomatorBridge(UiAutomation uiAutomation) { + mUiAutomation = uiAutomation; mInteractionController = new InteractionController(this); mQueryController = new QueryController(this); - connect(); } InteractionController getInteractionController() { @@ -87,115 +55,77 @@ class UiAutomatorBridge extends UiTestAutomationBridge { return mQueryController; } - @Override - public void onAccessibilityEvent(AccessibilityEvent event) { - super.onAccessibilityEvent(event); - Log.d(LOGTAG, event.toString()); - if (mWaitingForEventDelivery) { - try { - AccessibilityEvent clone = AccessibilityEvent.obtain(event); - mEventQueue.offer(clone, TIMEOUT_ASYNC_PROCESSING, TimeUnit.MILLISECONDS); - } catch (InterruptedException ie) { - /* ignore */ - } - if (!mWaitingForEventDelivery) { - mEventQueue.clear(); - } - } - mLastEventTime = SystemClock.uptimeMillis(); - notifyListeners(event); + public void connect() { + mUiAutomation.connect(); } - - void addAccessibilityEventListener(AccessibilityEventListener listener) { - mListeners.add(listener); + public void disconnect() { + mUiAutomation.disconnect(); } - private void notifyListeners(AccessibilityEvent event) { - for (AccessibilityEventListener listener : mListeners) { - listener.onAccessibilityEvent(event); - } + public void setOnAccessibilityEventListener(OnAccessibilityEventListener listener) { + mUiAutomation.setOnAccessibilityEventListener(listener); } - @Override - public void waitForIdle(long idleTimeout, long globalTimeout) { - long start = SystemClock.uptimeMillis(); - while ((SystemClock.uptimeMillis() - start) < WAIT_TIME_FROM_IDLE_TO_BUSY_STATE) { - if (getLastOperationTime() > getLastEventTime()) { - SystemClock.sleep(BUSY_STATE_POLL_TIME); - } else { - break; - } - } - super.waitForIdle(idleTimeout, globalTimeout); + public AccessibilityNodeInfo getRootInActiveWindow() { + return mUiAutomation.getRootInActiveWindow(); } - public void waitForIdle() { - waitForIdle(TOTAL_TIME_TO_WAIT_FOR_IDLE_STATE); + public boolean injectInputEvent(InputEvent event, boolean sync) { + return mUiAutomation.injectInputEvent(event, sync); } - public void waitForIdle(long timeout) { - waitForIdle(QUIET_TIME_TO_BE_CONSIDERD_IDLE_STATE, timeout); + public boolean setRotation(int rotation) { + return mUiAutomation.setRotation(rotation); } - private long getLastEventTime() { - synchronized (mLock) { - return mLastEventTime; - } + public void waitForIdle() { + waitForIdle(TOTAL_TIME_TO_WAIT_FOR_IDLE_STATE); } - private long getLastOperationTime() { - synchronized (mLock) { - return mLastOperationTime; + 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); } } - void setOperationTime() { - synchronized (mLock) { - mLastOperationTime = SystemClock.uptimeMillis(); - } + public AccessibilityEvent executeCommandAndWaitForAccessibilityEvent(Runnable command, + Predicate<AccessibilityEvent> filter, long timeoutMillis) throws TimeoutException { + return mUiAutomation.executeAndWaitForEvent(command, + filter, timeoutMillis); } - void updateEventTime() { - synchronized (mLock) { - mLastEventTime = SystemClock.uptimeMillis(); + public boolean takeScreenshot(File storePath, int quality) { + Bitmap screenshot = mUiAutomation.takeScreenshot(); + if (screenshot == null) { + return false; } - } - - public AccessibilityEvent executeCommandAndWaitForAccessibilityEvent(Runnable command, - Predicate<AccessibilityEvent> predicate, long timeoutMillis) - throws TimeoutException, Exception { - // Prepare to wait for an event. - mWaitingForEventDelivery = true; - // Execute the command. - command.run(); - // Wait for the event. - final long startTimeMillis = SystemClock.uptimeMillis(); - while (true) { - // Check if timed out and if not wait. - final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis; - final long remainingTimeMillis = timeoutMillis - elapsedTimeMillis; - if (remainingTimeMillis <= 0) { - mWaitingForEventDelivery = false; - mEventQueue.clear(); - throw new TimeoutException("Expected event not received within: " - + timeoutMillis + " ms."); - } - AccessibilityEvent event = null; - try { - event = mEventQueue.poll(remainingTimeMillis, TimeUnit.MILLISECONDS); - } catch (InterruptedException ie) { - /* ignore */ + BufferedOutputStream bos = null; + try { + bos = new BufferedOutputStream(new FileOutputStream(storePath)); + if (bos != null) { + screenshot.compress(Bitmap.CompressFormat.PNG, quality, bos); + bos.flush(); } - if (event != null) { - if (predicate.apply(event)) { - mWaitingForEventDelivery = false; - mEventQueue.clear(); - return event; - } else { - event.recycle(); + } 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 abstract Display getDefaultDisplay(); + + public abstract long getSystemLongPressTime(); } diff --git a/uiautomator/library/src/com/android/uiautomator/core/UiDevice.java b/uiautomator/library/src/com/android/uiautomator/core/UiDevice.java index b668bea..3605e40 100644 --- a/uiautomator/library/src/com/android/uiautomator/core/UiDevice.java +++ b/uiautomator/library/src/com/android/uiautomator/core/UiDevice.java @@ -16,6 +16,8 @@ package com.android.uiautomator.core; +import android.app.UiAutomation; +import android.app.UiAutomationConnection; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -27,7 +29,6 @@ import android.os.Environment; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; -import android.os.Trace; import android.util.DisplayMetrics; import android.util.Log; import android.view.Display; @@ -70,27 +71,38 @@ public class UiDevice { private boolean mInWatcherContext = false; // provides access the {@link QueryController} and {@link InteractionController} - private final UiAutomatorBridge mUiAutomationBridge; + private UiAutomatorBridge mUiAutomationBridge; // reference to self - private static UiDevice mDevice; + private static UiDevice sDevice; private UiDevice() { - mUiAutomationBridge = new UiAutomatorBridge(); - mDevice = this; + /* hide constructor */ } + /** + * @hide + */ + public void initialize(Context context, UiAutomation uiAutomation) { + if (context == null) { + mUiAutomationBridge = new ShellUiAutomatorBridge(uiAutomation); + } else { + mUiAutomationBridge = new OnDeviceUiAutomatorBridge(context, uiAutomation); + } + } + boolean isInWatcherContext() { return mInWatcherContext; } /** * Provides access the {@link QueryController} and {@link InteractionController} - * @return {@link UiAutomatorBridge} + * @return {@link ShellUiAutomatorBridge} */ UiAutomatorBridge getAutomatorBridge() { return mUiAutomationBridge; } + /** * Retrieves a singleton instance of UiDevice * @@ -98,10 +110,10 @@ public class UiDevice { * @since API Level 16 */ public static UiDevice getInstance() { - if (mDevice == null) { - mDevice = new UiDevice(); + if (sDevice == null) { + sDevice = new UiDevice(); } - return mDevice; + return sDevice; } /** diff --git a/uiautomator/library/src/com/android/uiautomator/core/UiSelector.java b/uiautomator/library/src/com/android/uiautomator/core/UiSelector.java index 8963c38..cb4e9ad 100644 --- a/uiautomator/library/src/com/android/uiautomator/core/UiSelector.java +++ b/uiautomator/library/src/com/android/uiautomator/core/UiSelector.java @@ -58,6 +58,7 @@ public class UiSelector { 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_VIEW_ID = 29; private SparseArray<Object> mSelectorAttributes = new SparseArray<Object>(); @@ -279,6 +280,17 @@ public class UiSelector { } /** + * Set the search criteria to match the given view id. + * + * @param desc Value to match + * @return UiSelector with the specified search criteria + * @since API Level 18 + */ + public UiSelector viewId(String viewId) { + return buildSelector(SELECTOR_VIEW_ID, viewId); + } + + /** * Set the search criteria to match the widget by its node * index in the layout hierarchy. * @@ -734,6 +746,11 @@ public class UiSelector { return false; } break; + case UiSelector.SELECTOR_VIEW_ID: + if (node.getViewId() != getString(criterion)) { + return false; + } + break; } } return matchOrUpdateInstance(); @@ -941,6 +958,9 @@ public class UiSelector { case SELECTOR_PACKAGE_NAME_REGEX: builder.append("PACKAGE_NAME_REGEX=").append(mSelectorAttributes.valueAt(i)); break; + case SELECTOR_VIEW_ID: + builder.append("VIEW_ID=").append(mSelectorAttributes.valueAt(i)); + break; default: builder.append("UNDEFINED="+criterion+" ").append(mSelectorAttributes.valueAt(i)); } diff --git a/uiautomator/library/src/com/android/uiautomator/testrunner/OnDeviceUiTestCase.java b/uiautomator/library/src/com/android/uiautomator/testrunner/OnDeviceUiTestCase.java new file mode 100644 index 0000000..dc38d25 --- /dev/null +++ b/uiautomator/library/src/com/android/uiautomator/testrunner/OnDeviceUiTestCase.java @@ -0,0 +1,141 @@ +/* + * 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.UiAutomation; +import android.content.Context; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemClock; +import android.test.AndroidTestCase; +import android.view.inputmethod.InputMethodInfo; + +import com.android.internal.view.IInputMethodManager; +import com.android.uiautomator.core.UiDevice; + +import java.util.List; + +/** + * UI automator test case that is executed on the device. + */ +public class OnDeviceUiTestCase extends AndroidTestCase { + private static final String DISABLE_IME = "disable_ime"; + private static final String DUMMY_IME_PACKAGE = "com.android.testing.dummyime"; + private boolean mShouldDisableIme; + + private UiAutomation mUiAutomation; + private Bundle mParams; + + private final IAutomationSupport mAutomationSupport = new IAutomationSupport() { + @Override + public void sendStatus(int resultCode, Bundle status) { + sendStatus(resultCode, status); + } + }; + + @Override + protected void setUp() throws Exception { + super.setUp(); + UiDevice.getInstance().initialize(getContext(), mUiAutomation); + mShouldDisableIme = "true".equals(getParams().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 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; + } + + /** + * Initializes this test case. + * + * @param uiAutomation An {@link UiAutomation} instance. + * @param params Instrumentation arguments. + */ + void initialize(UiAutomation uiAutomation, Bundle params) { + mUiAutomation = uiAutomation; + mParams = params; + } + + /** + * Provides support for running tests to report interim status + * + * @return IAutomationSupport + * @since API Level 16 + */ + public IAutomationSupport getAutomationSupport() { + return mAutomationSupport; + } + + /** + * 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/uiautomator/library/src/com/android/uiautomator/testrunner/OnDeviceUiTestRunner.java b/uiautomator/library/src/com/android/uiautomator/testrunner/OnDeviceUiTestRunner.java new file mode 100644 index 0000000..857d7e9 --- /dev/null +++ b/uiautomator/library/src/com/android/uiautomator/testrunner/OnDeviceUiTestRunner.java @@ -0,0 +1,52 @@ +/* + * 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 junit.framework.TestCase; +import junit.framework.TestResult; + +import java.util.List; + +/** + * Test runner for {@link OnDeviceUiTestCase}s. Such tests are executed + * on the device and have access to an applications context. + */ +public class OnDeviceUiTestRunner extends InstrumentationTestRunner { + + @Override + protected AndroidTestRunner getAndroidTestRunner() { + return new AndroidTestRunner() { + @Override + public void runTest(TestResult testResult) { + List<TestCase> testCases = getTestCases(); + final int testCaseCount = testCases.size(); + for (int i = 0; i < testCaseCount; i++) { + TestCase testCase = testCases.get(i); + if (testCase instanceof OnDeviceUiTestCase) { + OnDeviceUiTestCase uiTestCase = (OnDeviceUiTestCase) testCase; + uiTestCase.initialize(OnDeviceUiTestRunner.this.getUiAutomation(), + getArguments()); + } + } + super.runTest(testResult); + } + }; + } +} diff --git a/uiautomator/library/src/com/android/uiautomator/testrunner/UiAutomatorTestRunner.java b/uiautomator/library/src/com/android/uiautomator/testrunner/UiAutomatorTestRunner.java index 4f41a5c..4405927 100644 --- a/uiautomator/library/src/com/android/uiautomator/testrunner/UiAutomatorTestRunner.java +++ b/uiautomator/library/src/com/android/uiautomator/testrunner/UiAutomatorTestRunner.java @@ -22,12 +22,14 @@ 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.Tracer; +import com.android.uiautomator.core.UiAutomationShellWrapper; import com.android.uiautomator.core.UiDevice; import junit.framework.AssertionFailedError; @@ -54,6 +56,8 @@ public class UiAutomatorTestRunner { 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 Bundle mParams = null; private UiDevice mUiDevice; @@ -67,6 +71,8 @@ public class UiAutomatorTestRunner { }; private List<TestListener> mTestListeners = new ArrayList<TestListener>(); + private HandlerThread mHandlerThread; + public void run(List<String> testClasses, Bundle params, boolean debug) { Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() { @Override @@ -102,6 +108,12 @@ public class UiAutomatorTestRunner { if (mDebug) { Debug.waitForDebugger(); } + mHandlerThread = new HandlerThread(HANDLER_THREAD_NAME); + mHandlerThread.setDaemon(true); + mHandlerThread.start(); + UiAutomationShellWrapper automationWrapper = new UiAutomationShellWrapper(); + automationWrapper.connect(); + UiDevice.getInstance().initialize(null, automationWrapper.getUiAutomation()); mUiDevice = UiDevice.getInstance(); List<TestCase> testCases = collector.getTestCases(); Bundle testRunOutput = new Bundle(); @@ -149,6 +161,8 @@ public class UiAutomatorTestRunner { } finally { long runTime = SystemClock.uptimeMillis() - startTime; resultPrinter.print(testRunResult, runTime, testRunOutput); + automationWrapper.disconnect(); + mHandlerThread.quit(); } } |