diff options
author | Allen Hair <allenhair@google.com> | 2015-05-26 19:03:29 +0000 |
---|---|---|
committer | Allen Hair <allenhair@google.com> | 2015-05-26 19:03:29 +0000 |
commit | 55de088f338baf200fdae9bcb00a6dd47e3e3b40 (patch) | |
tree | ef073685d0c8e2927e38f5308bd56ae33b433a73 | |
parent | 5304893badddc79409f2a4354652660424498dce (diff) | |
download | uiautomator-55de088f338baf200fdae9bcb00a6dd47e3e3b40.tar.gz |
Revert "Remove multi-window changes."
This reverts commit 5304893badddc79409f2a4354652660424498dce.
Change-Id: I5004ddb97e6f0f4f4033e349705254c70b11cbf6
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); + } + }); + } +} |