summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAllen Hair <allenhair@google.com>2015-05-26 19:03:29 +0000
committerAllen Hair <allenhair@google.com>2015-05-26 19:03:29 +0000
commit55de088f338baf200fdae9bcb00a6dd47e3e3b40 (patch)
treeef073685d0c8e2927e38f5308bd56ae33b433a73
parent5304893badddc79409f2a4354652660424498dce (diff)
downloaduiautomator-55de088f338baf200fdae9bcb00a6dd47e3e3b40.tar.gz
Revert "Remove multi-window changes."
This reverts commit 5304893badddc79409f2a4354652660424498dce. Change-Id: I5004ddb97e6f0f4f4033e349705254c70b11cbf6
-rw-r--r--src/main/java/android/support/test/uiautomator/AccessibilityNodeInfoDumper.java5
-rw-r--r--src/main/java/android/support/test/uiautomator/ByMatcher.java44
-rw-r--r--src/main/java/android/support/test/uiautomator/UiDevice.java53
-rw-r--r--src/main/java/android/support/test/uiautomator/UiObject2.java7
-rw-r--r--tests/src/androidTest/java/android/support/test/uiautomator/tests/MultiWindowTests.java67
-rw-r--r--util/build.gradle11
-rw-r--r--util/src/main/AndroidManifest.xml32
-rw-r--r--util/src/main/java/android/support/test/uiautomator/util/Dump.java79
-rw-r--r--util/src/main/java/android/support/test/uiautomator/util/Events.java51
9 files changed, 314 insertions, 35 deletions
diff --git a/src/main/java/android/support/test/uiautomator/AccessibilityNodeInfoDumper.java b/src/main/java/android/support/test/uiautomator/AccessibilityNodeInfoDumper.java
index 589a780..7cc777d 100644
--- a/src/main/java/android/support/test/uiautomator/AccessibilityNodeInfoDumper.java
+++ b/src/main/java/android/support/test/uiautomator/AccessibilityNodeInfoDumper.java
@@ -46,8 +46,9 @@ class AccessibilityNodeInfoDumper {
serializer.startTag("", "hierarchy"); // TODO(allenhair): Should we use a namespace?
serializer.attribute("", "rotation", Integer.toString(device.getDisplayRotation()));
- dumpNodeRec(device.getActiveWindowRoot(), serializer, 0, device.getDisplayWidth(),
- device.getDisplayHeight());
+ for (AccessibilityNodeInfo root : device.getWindowRoots()) {
+ dumpNodeRec(root, serializer, 0, device.getDisplayWidth(), device.getDisplayHeight());
+ }
serializer.endTag("", "hierarchy");
serializer.endDocument();
diff --git a/src/main/java/android/support/test/uiautomator/ByMatcher.java b/src/main/java/android/support/test/uiautomator/ByMatcher.java
index 3726f1a..62e9c47 100644
--- a/src/main/java/android/support/test/uiautomator/ByMatcher.java
+++ b/src/main/java/android/support/test/uiautomator/ByMatcher.java
@@ -16,6 +16,7 @@
package android.support.test.uiautomator;
+import android.os.SystemClock;
import android.util.Log;
import android.view.accessibility.AccessibilityNodeInfo;
@@ -40,7 +41,7 @@ class ByMatcher {
/**
* Constructs a new {@link ByMatcher} instance. Used by
- * {@link ByMatcher#findMatch(AccessibilityNodeInfo, BySelector)} to store state information
+ * {@link ByMatcher#findMatch(UiDevice, BySelector, AccessibilityNodeInfo...)} to store state information
* that does not change during recursive calls.
*
* @param selector The criteria used to determine if a {@link AccessibilityNodeInfo} is a match.
@@ -53,37 +54,48 @@ class ByMatcher {
}
/**
- * Traverses the {@link AccessibilityNodeInfo} hierarchy starting at {@code root}, and returns
- * the first node to match the {@code selector} criteria. <br />
- * <strong>Note:</strong> The caller must release the {@link AccessibilityNodeInfo} instance
- * by calling {@link AccessibilityNodeInfo#recycle()} to avoid leaking resources.
+ * Traverses the {@link AccessibilityNodeInfo} hierarchy starting at each {@code root} and
+ * returns the first node to match the {@code selector} criteria. <br />
+ * <strong>Note:</strong> The caller must release the {@link AccessibilityNodeInfo} instance by
+ * calling {@link AccessibilityNodeInfo#recycle()} to avoid leaking resources.
*
- * @param root The root {@link AccessibilityNodeInfo} from which to start the search.
+ * @param device A reference to the {@link UiDevice}.
* @param selector The {@link BySelector} criteria used to determine if a node is a match.
* @return The first {@link AccessibilityNodeInfo} which matched the search criteria.
*/
- static AccessibilityNodeInfo findMatch(UiDevice device, AccessibilityNodeInfo root,
- BySelector selector) {
+ static AccessibilityNodeInfo findMatch(UiDevice device, BySelector selector,
+ AccessibilityNodeInfo... roots) {
// TODO: Don't short-circuit when debugging, and warn if more than one match.
ByMatcher matcher = new ByMatcher(device, selector, true);
- List<AccessibilityNodeInfo> matches = matcher.findMatches(root);
- return matches.isEmpty() ? null : matches.get(0);
+ for (AccessibilityNodeInfo root : roots) {
+ List<AccessibilityNodeInfo> matches = matcher.findMatches(root);
+ if (!matches.isEmpty()) {
+ return matches.get(0);
+ }
+ }
+ return null;
}
/**
- * Traverses the {@link AccessibilityNodeInfo} hierarchy starting at {@code root}, and returns
- * a list of nodes which match the {@code selector} criteria. <br />
+ * Traverses the {@link AccessibilityNodeInfo} hierarchy starting at each {@code root} and
+ * returns a list of nodes which match the {@code selector} criteria. <br />
* <strong>Note:</strong> The caller must release each {@link AccessibilityNodeInfo} instance
* by calling {@link AccessibilityNodeInfo#recycle()} to avoid leaking resources.
*
- * @param root The root {@link AccessibilityNodeInfo} from which to start the search.
+ * @param device A reference to the {@link UiDevice}.
* @param selector The {@link BySelector} criteria used to determine if a node is a match.
* @return A list containing all of the nodes which matched the search criteria.
*/
- static List<AccessibilityNodeInfo> findMatches(UiDevice device, AccessibilityNodeInfo root,
- BySelector selector) {
- return new ByMatcher(device, selector, false).findMatches(root);
+ static List<AccessibilityNodeInfo> findMatches(UiDevice device, BySelector selector,
+ AccessibilityNodeInfo... roots) {
+
+ List<AccessibilityNodeInfo> ret = new ArrayList<AccessibilityNodeInfo>();
+ ByMatcher matcher = new ByMatcher(device, selector, false);
+ for (AccessibilityNodeInfo root : roots) {
+ ret.addAll(matcher.findMatches(root));
+ }
+ return ret;
}
/**
diff --git a/src/main/java/android/support/test/uiautomator/UiDevice.java b/src/main/java/android/support/test/uiautomator/UiDevice.java
index 6fbf759..ce151ee 100644
--- a/src/main/java/android/support/test/uiautomator/UiDevice.java
+++ b/src/main/java/android/support/test/uiautomator/UiDevice.java
@@ -16,6 +16,7 @@
package android.support.test.uiautomator;
+import android.accessibilityservice.AccessibilityServiceInfo;
import android.app.Instrumentation;
import android.app.UiAutomation;
import android.app.UiAutomation.AccessibilityEventFilter;
@@ -35,6 +36,7 @@ import android.view.KeyEvent;
import android.view.Surface;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityWindowInfo;
import java.io.BufferedOutputStream;
import java.io.File;
@@ -92,17 +94,25 @@ public class UiDevice implements Searchable {
+ ("REL".equals(Build.VERSION.CODENAME) ? 0 : 1);
/**
- * @deprecated Should use {@link UiDevice#UiDevice(InstrumentationUiAutomatorBridge)} instead.
+ * @deprecated Should use {@link UiDevice#UiDevice(Instrumentation)} instead.
*/
@Deprecated
private UiDevice() {}
- /** Private constructor. Clients should use {@link UiDevice#getInstance(Context)}. */
+ /** Private constructor. Clients should use {@link UiDevice#getInstance(Instrumentation)}. */
private UiDevice(Instrumentation instrumentation) {
mInstrumentation = instrumentation;
+ UiAutomation uiAutomation = instrumentation.getUiAutomation();
mUiAutomationBridge = new InstrumentationUiAutomatorBridge(
- instrumentation.getContext(),
- instrumentation.getUiAutomation());
+ instrumentation.getContext(), uiAutomation);
+
+ // Enable multi-window support for API level 21 and up
+ if (UiDevice.API_LEVEL_ACTUAL >= Build.VERSION_CODES.LOLLIPOP) {
+ // Subscribe to window information
+ AccessibilityServiceInfo info = uiAutomation.getServiceInfo();
+ info.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS;
+ uiAutomation.setServiceInfo(info);
+ }
}
/**
@@ -141,8 +151,7 @@ public class UiDevice implements Searchable {
/** Returns whether there is a match for the given {@code selector} criteria. */
public boolean hasObject(BySelector selector) {
- QueryController qc = getAutomatorBridge().getQueryController();
- AccessibilityNodeInfo node = ByMatcher.findMatch(this, qc.getRootNode(), selector);
+ AccessibilityNodeInfo node = ByMatcher.findMatch(this, selector, getWindowRoots());
if (node != null) {
node.recycle();
return true;
@@ -152,17 +161,14 @@ public class UiDevice implements Searchable {
/** Returns the first object to match the {@code selector} criteria. */
public UiObject2 findObject(BySelector selector) {
- QueryController qc = getAutomatorBridge().getQueryController();
- AccessibilityNodeInfo node = ByMatcher.findMatch(this, qc.getRootNode(), selector);
+ AccessibilityNodeInfo node = ByMatcher.findMatch(this, selector, getWindowRoots());
return node != null ? new UiObject2(this, selector, node) : null;
}
/** Returns all objects that match the {@code selector} criteria. */
public List<UiObject2> findObjects(BySelector selector) {
List<UiObject2> ret = new ArrayList<UiObject2>();
-
- QueryController qc = getAutomatorBridge().getQueryController();
- for (AccessibilityNodeInfo node : ByMatcher.findMatches(this, qc.getRootNode(), selector)) {
+ for (AccessibilityNodeInfo node : ByMatcher.findMatches(this, selector, getWindowRoots())) {
ret.add(new UiObject2(this, selector, node));
}
@@ -1063,8 +1069,27 @@ public class UiDevice implements Searchable {
return stdout.toString();
}
- AccessibilityNodeInfo getActiveWindowRoot() {
- QueryController qc = getAutomatorBridge().getQueryController();
- return qc.getRootNode();
+ /** Returns a list containing the root {@link AccessibilityNodeInfo}s for each active window */
+ AccessibilityNodeInfo[] getWindowRoots() {
+ waitForIdle();
+
+ ArrayList<AccessibilityNodeInfo> ret = new ArrayList<AccessibilityNodeInfo>();
+ // Support multi-window searches for API level 21 and up
+ if (UiDevice.API_LEVEL_ACTUAL >= Build.VERSION_CODES.LOLLIPOP) {
+ for (AccessibilityWindowInfo window : mInstrumentation.getUiAutomation().getWindows()) {
+ AccessibilityNodeInfo root = window.getRoot();
+
+ if (root == null) {
+ Log.w(LOG_TAG, String.format("Skipping null root node for window: %s",
+ window.toString()));
+ continue;
+ }
+ ret.add(root);
+ }
+ // Prior to API level 21 we can only access the active window
+ } else {
+ ret.add(mInstrumentation.getUiAutomation().getRootInActiveWindow());
+ }
+ return ret.toArray(new AccessibilityNodeInfo[ret.size()]);
}
}
diff --git a/src/main/java/android/support/test/uiautomator/UiObject2.java b/src/main/java/android/support/test/uiautomator/UiObject2.java
index 38530e6..a755e65 100644
--- a/src/main/java/android/support/test/uiautomator/UiObject2.java
+++ b/src/main/java/android/support/test/uiautomator/UiObject2.java
@@ -173,7 +173,7 @@ public class UiObject2 implements Searchable {
/** Returns whether there is a match for the given criteria under this object. */
public boolean hasObject(BySelector selector) {
AccessibilityNodeInfo node =
- ByMatcher.findMatch(mDevice, getAccessibilityNodeInfo(), selector);
+ ByMatcher.findMatch(mDevice, selector, getAccessibilityNodeInfo());
if (node != null) {
node.recycle();
return true;
@@ -186,7 +186,7 @@ public class UiObject2 implements Searchable {
*/
public UiObject2 findObject(BySelector selector) {
AccessibilityNodeInfo node =
- ByMatcher.findMatch(mDevice, getAccessibilityNodeInfo(), selector);
+ ByMatcher.findMatch(mDevice, selector, getAccessibilityNodeInfo());
return node != null ? new UiObject2(mDevice, selector, node) : null;
}
@@ -194,7 +194,7 @@ public class UiObject2 implements Searchable {
public List<UiObject2> findObjects(BySelector selector) {
List<UiObject2> ret = new ArrayList<UiObject2>();
for (AccessibilityNodeInfo node :
- ByMatcher.findMatches(mDevice, getAccessibilityNodeInfo(), selector)) {
+ ByMatcher.findMatches(mDevice, selector, getAccessibilityNodeInfo())) {
ret.add(new UiObject2(mDevice, selector, node));
}
@@ -621,6 +621,7 @@ public class UiObject2 implements Searchable {
throw new IllegalStateException("This object has already been recycled");
}
+ mDevice.waitForIdle();
if (!mCachedNode.refresh()) {
mDevice.runWatchers();
diff --git a/tests/src/androidTest/java/android/support/test/uiautomator/tests/MultiWindowTests.java b/tests/src/androidTest/java/android/support/test/uiautomator/tests/MultiWindowTests.java
new file mode 100644
index 0000000..abc4867
--- /dev/null
+++ b/tests/src/androidTest/java/android/support/test/uiautomator/tests/MultiWindowTests.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2015 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 android.support.test.uiautomator.tests;
+
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class MultiWindowTests {
+
+ private UiDevice mDevice;
+ private static final String TEST_APP = "android.support.test.uiautomator.testapp";
+
+ @Before
+ public void setUp() {
+ mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+
+ mDevice.pressHome();
+ mDevice.waitForIdle();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion=21)
+ public void testHasBackButton() {
+ Assert.assertTrue(mDevice.hasObject(By.res("com.android.systemui", "back")));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion=21)
+ public void testHasHomeButton() {
+ Assert.assertTrue(mDevice.hasObject(By.res("com.android.systemui", "home")));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion=21)
+ public void testHasRecentsButton() {
+ Assert.assertTrue(mDevice.hasObject(By.res("com.android.systemui", "recent_apps")));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion=21)
+ public void testHasStatusBar() {
+ Assert.assertTrue(mDevice.hasObject(By.res("com.android.systemui", "status_bar")));
+ }
+} \ No newline at end of file
diff --git a/util/build.gradle b/util/build.gradle
new file mode 100644
index 0000000..fc76655
--- /dev/null
+++ b/util/build.gradle
@@ -0,0 +1,11 @@
+apply plugin: 'android'
+
+android {
+ defaultConfig {
+ minSdkVersion 18
+ }
+}
+
+dependencies {
+ compile project(':uiautomator-v18')
+}
diff --git a/util/src/main/AndroidManifest.xml b/util/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..1dfdb0c
--- /dev/null
+++ b/util/src/main/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2015 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="android.support.test.uiautomator.util"
+ android:versionCode="1"
+ android:versionName="1.0.0">
+
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+
+ <instrumentation
+ android:name="android.support.test.uiautomator.util.Dump"
+ android:targetPackage="android.support.test.uiautomator.util" />
+
+ <instrumentation
+ android:name="android.support.test.uiautomator.util.Events"
+ android:targetPackage="android.support.test.uiautomator.util" />
+
+</manifest>
diff --git a/util/src/main/java/android/support/test/uiautomator/util/Dump.java b/util/src/main/java/android/support/test/uiautomator/util/Dump.java
new file mode 100644
index 0000000..c4bf4ed
--- /dev/null
+++ b/util/src/main/java/android/support/test/uiautomator/util/Dump.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2015 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 android.support.test.uiautomator.util;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.os.Bundle;
+import android.os.Environment;
+import android.support.test.uiautomator.UiDevice;
+
+import java.io.File;
+import java.io.IOException;
+
+/** Dumps the current UI hierarchy to an xml file. */
+public class Dump extends Instrumentation {
+
+ private boolean mCompressed = false;
+ private File mOut;
+
+ @Override
+ public void onCreate(Bundle arguments) {
+ super.onCreate(arguments);
+
+ if (arguments.containsKey("compressed")) {
+ String compressed = arguments.getString("compressed");
+ if (!compressed.equalsIgnoreCase("true") && !compressed.equalsIgnoreCase("false")) {
+ throw new IllegalArgumentException("compressed must be either true or false");
+ }
+ mCompressed = Boolean.parseBoolean(compressed);
+ }
+ arguments.getString("compressed", "false");
+
+ if (arguments.containsKey("out")) {
+ String out = arguments.getString("out");
+ mOut = new File(out);
+ if (!mOut.isAbsolute()) {
+ mOut = new File(Environment.getExternalStorageDirectory(), out);
+ }
+ } else {
+ mOut = new File(Environment.getExternalStorageDirectory(), "window_dump.xml");
+ }
+
+ start();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ UiDevice device = UiDevice.getInstance(this);
+ device.setCompressedLayoutHeirarchy(mCompressed);
+
+ Bundle status = new Bundle();
+ try {
+ // Dump the window hierarchy
+ device.dumpWindowHierarchy(mOut);
+
+ status.putString("Status", "Hierarchy dumped successfully");
+ } catch (IOException e) {
+ status.putString("Error", e.toString());
+ }
+
+ finish(Activity.RESULT_OK, status);
+ }
+}
diff --git a/util/src/main/java/android/support/test/uiautomator/util/Events.java b/util/src/main/java/android/support/test/uiautomator/util/Events.java
new file mode 100644
index 0000000..0c4e0c1
--- /dev/null
+++ b/util/src/main/java/android/support/test/uiautomator/util/Events.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2015 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 android.support.test.uiautomator.util;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.app.UiAutomation;
+import android.os.Bundle;
+import android.view.accessibility.AccessibilityEvent;
+
+/** Outputs AccessibilityEvents as Instrumentation status updates. */
+public class Events extends Instrumentation {
+
+ @Override
+ public void onCreate(Bundle arguments) {
+ super.onCreate(arguments);
+
+ // Default implementation doesn't actually call start(), so we need to call it ourselves.
+ start();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ getUiAutomation().setOnAccessibilityEventListener(
+ new UiAutomation.OnAccessibilityEventListener() {
+
+ @Override
+ public void onAccessibilityEvent(AccessibilityEvent event) {
+ Bundle status = new Bundle();
+ status.putString("Event", event.toString());
+ sendStatus(Activity.RESULT_OK, status);
+ }
+ });
+ }
+}