summaryrefslogtreecommitdiff
path: root/uiautomator
diff options
context:
space:
mode:
authorSvetoslav Ganov <svetoslavganov@google.com>2013-01-02 10:24:19 -0800
committerAndroid (Google) Code Review <android-gerrit@google.com>2013-01-23 00:19:48 +0000
commit89f6117cb1fbeab3770106cf54e05af1f597be81 (patch)
tree77554dd4e08ee6a0a37d984788199c8bfb380382 /uiautomator
parenta1c9eab767cbfad143c024d171cf9cff30acac42 (diff)
downloadtesting-89f6117cb1fbeab3770106cf54e05af1f597be81.tar.gz
Refactoring of UiAutomator to use the new UI test APIs.
Change-Id: If3445b0b4fd3aad66a5cd661e81f3639bff90dba
Diffstat (limited to 'uiautomator')
-rw-r--r--uiautomator/api/current.txt13
-rw-r--r--uiautomator/cmds/uiautomator/src/com/android/commands/uiautomator/DumpCommand.java28
-rw-r--r--uiautomator/cmds/uiautomator/src/com/android/commands/uiautomator/EventsCommand.java12
-rw-r--r--uiautomator/library/src/com/android/uiautomator/core/InteractionController.java99
-rw-r--r--uiautomator/library/src/com/android/uiautomator/core/OnDeviceUiAutomatorBridge.java44
-rw-r--r--uiautomator/library/src/com/android/uiautomator/core/QueryController.java8
-rw-r--r--uiautomator/library/src/com/android/uiautomator/core/ShellUiAutomatorBridge.java87
-rw-r--r--uiautomator/library/src/com/android/uiautomator/core/UiAutomationShellWrapper.java36
-rw-r--r--uiautomator/library/src/com/android/uiautomator/core/UiAutomatorBridge.java220
-rw-r--r--uiautomator/library/src/com/android/uiautomator/core/UiDevice.java30
-rw-r--r--uiautomator/library/src/com/android/uiautomator/core/UiSelector.java20
-rw-r--r--uiautomator/library/src/com/android/uiautomator/testrunner/OnDeviceUiTestCase.java141
-rw-r--r--uiautomator/library/src/com/android/uiautomator/testrunner/OnDeviceUiTestRunner.java52
-rw-r--r--uiautomator/library/src/com/android/uiautomator/testrunner/UiAutomatorTestRunner.java14
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();
}
}