Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Tune the logic of BySelector creation #590

Merged
merged 3 commits into from
Jan 2, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
* 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 io.appium.uiautomator2.model;

import android.view.accessibility.AccessibilityNodeInfo;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.uiautomator.By;
import androidx.test.uiautomator.BySelector;

import java.util.UUID;

public class BySelectorHelper {
@NonNull
public static BySelector toBySelector(@Nullable AccessibilityNodeInfo node) {
if (node == null) {
return makeDummySelector();
}

BySelector result = null;
// The below conditions might be simplified, but need API 24+ with lambdas support
CharSequence className = node.getClassName();
if (hasValue(className)) {
result = By.clazz(className.toString());
}
CharSequence desc = node.getContentDescription();
if (hasValue(desc)) {
result = result == null ? By.desc(desc.toString()) : result.desc(desc.toString());
}
CharSequence pkg = node.getPackageName();
if (hasValue(pkg)) {
result = result == null ? By.pkg(pkg.toString()) : result.pkg(pkg.toString());
}
CharSequence res = node.getViewIdResourceName();
if (hasValue(res)) {
result = result == null ? By.res(res.toString()) : result.res(res.toString());
}
CharSequence text = node.getText();
if (hasValue(text)) {
result = result == null ? By.text(text.toString()) : result.text(text.toString());
}

result = result == null
? By.checkable(node.isCheckable())
: result.checkable(node.isCheckable());
result = result == null
mykola-mokhnach marked this conversation as resolved.
Show resolved Hide resolved
? By.clickable(node.isClickable())
: result.clickable(node.isClickable());
result = result == null
? By.longClickable(node.isLongClickable())
: result.longClickable(node.isLongClickable());
result = result == null
? By.focusable(node.isFocusable())
: result.focusable(node.isFocusable());
result = result == null
? By.scrollable(node.isScrollable())
: result.scrollable(node.isScrollable());

return result == null ? makeDummySelector() : result;
}

private static boolean hasValue(@Nullable CharSequence cs) {
return cs != null && cs.length() > 0;
}

public static BySelector makeDummySelector() {
return By.res(String.format("DUMMY:id/%s", UUID.randomUUID()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,7 @@

package io.appium.uiautomator2.model;

import android.annotation.TargetApi;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Pair;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.Toast;
Expand All @@ -44,16 +41,13 @@
import io.appium.uiautomator2.utils.Attribute;
import io.appium.uiautomator2.utils.Logger;

import static androidx.test.internal.util.Checks.checkNotNull;
import static io.appium.uiautomator2.core.AxNodeInfoExtractor.toAxNodeInfo;
import static io.appium.uiautomator2.utils.ReflectionUtils.setField;
import static io.appium.uiautomator2.utils.StringHelpers.charSequenceToNullableString;

/**
* A UiElement that gets attributes via the Accessibility API.
* https://android.googlesource.com/platform/frameworks/testing/+/476328047e3f82d6d9be8ab23f502a670613f94c/uiautomator/library/src/com/android/uiautomator/core/AccessibilityNodeInfoDumper.java
*/
@TargetApi(18)
public class UiElementSnapshot extends UiElement<AccessibilityNodeInfo, UiElementSnapshot> {
private final static String ROOT_NODE_NAME = "hierarchy";
// The same order will be used for node attributes in xml page source
Expand Down Expand Up @@ -81,7 +75,7 @@ public class UiElementSnapshot extends UiElement<AccessibilityNodeInfo, UiElemen

private UiElementSnapshot(AccessibilityNodeInfo node, int index, int depth, int maxDepth,
Set<Attribute> includedAttributes) {
super(checkNotNull(node));
super(Objects.requireNonNull(node));
this.depth = depth;
this.maxDepth = maxDepth;
this.index = index;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
import android.os.SystemClock;
import android.view.accessibility.AccessibilityNodeInfo;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.uiautomator.By;
import androidx.test.uiautomator.BySelector;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.UiObject2;
Expand All @@ -47,6 +47,8 @@
import io.appium.uiautomator2.utils.ReflectionUtils;

import static io.appium.uiautomator2.model.AccessibleUiObject.toAccessibleUiObject;
import static io.appium.uiautomator2.model.BySelectorHelper.makeDummySelector;
import static io.appium.uiautomator2.model.BySelectorHelper.toBySelector;
import static io.appium.uiautomator2.utils.AXWindowHelpers.getCachedWindowRoots;
import static io.appium.uiautomator2.utils.Device.getUiDevice;
import static io.appium.uiautomator2.utils.ReflectionUtils.getConstructor;
Expand All @@ -56,7 +58,6 @@

public class CustomUiDevice {
private static final int CHANGE_ROTATION_TIMEOUT_MS = 2000;

private static final String FIELD_M_INSTRUMENTATION = "mInstrumentation";

private static CustomUiDevice INSTANCE = null;
Expand All @@ -67,6 +68,7 @@ public class CustomUiDevice {
private final Instrumentation mInstrumentation;
private GestureController gestureController;


private CustomUiDevice() {
this.mInstrumentation = (Instrumentation) getField(UiDevice.class, FIELD_M_INSTRUMENTATION, Device.getUiDevice());
this.ByMatcherClass = ReflectionUtils.getClass("androidx.test.uiautomator.ByMatcher");
Expand Down Expand Up @@ -94,24 +96,21 @@ public UiAutomation getUiAutomation() {
return getInstrumentation().getUiAutomation();
}

private UiObject2 toUiObject2(@Nullable Object selector, @Nullable AccessibilityNodeInfo node) {
if (selector == null) {
// FIXME: The 'selector' should be proper By instance as non-null in the interaction.
Logger.debug("FIXME: selector argument should not be null in androidx.test.uiautomator:uiautomator:2.3.0");
}

private UiObject2 toUiObject2(@NonNull BySelector selector, @Nullable AccessibilityNodeInfo node) {
// TODO: remove this comment after upgrading to androidx.test.uiautomator:uiautomator:2.3.0
// UiObject2 with androidx.test.uiautomator:uiautomator:2.3.0 has below code to crate the instance,
// thus if the node was None, it should create an empty element for the AccessibilityNodeInfo.
// <pre>
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// AccessibilityWindowInfo window = UiObject2.Api21Impl.getWindow(cachedNode);
// mDisplayId = window == null ? Display.DEFAULT_DISPLAY : UiObject2.Api30Impl.getDisplayId(window);
// } else {
// mDisplayId = Display.DEFAULT_DISPLAY;
// }
// </pre>
AccessibilityNodeInfo accessibilityNodeInfo =
(node == null && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R)
? new AccessibilityNodeInfo()
? new AccessibilityNodeInfo()
: node;
Object[] constructorParams = {getUiDevice(), selector, accessibilityNodeInfo};
try {
Expand All @@ -131,26 +130,31 @@ private UiObject2 toUiObject2(@Nullable Object selector, @Nullable Accessibility
@Nullable
public AccessibleUiObject findObject(Object selector) throws UiAutomator2Exception {
final AccessibilityNodeInfo node;
final BySelector realSelector;
if (selector instanceof BySelector) {
node = (AccessibilityNodeInfo) invoke(METHOD_FIND_MATCH, ByMatcherClass,
Device.getUiDevice(), selector, getCachedWindowRoots());
realSelector = (BySelector) selector;
} else if (selector instanceof NodeInfoList) {
node = ((NodeInfoList) selector).getFirst();
selector = toSelector(node);
realSelector = toBySelector(node);
} else if (selector instanceof AccessibilityNodeInfo) {
node = (AccessibilityNodeInfo) selector;
selector = toSelector(node);
realSelector = toBySelector(node);
} else if (selector instanceof UiSelector) {
return toAccessibleUiObject(getUiDevice().findObject((UiSelector) selector));
} else {
throw new InvalidSelectorException("Selector of type " + selector.getClass().getName() + " not supported");
throw new InvalidSelectorException(String.format(
"Selector of type %s not supported",
selector == null ? null : selector.getClass().getName()
));
}
return node == null ? null : new AccessibleUiObject(toUiObject2(selector, node), node);
return node == null ? null : new AccessibleUiObject(toUiObject2(realSelector, node), node);
}

public synchronized GestureController getGestureController() {
if (gestureController == null) {
UiObject2 dummyElement = toUiObject2(null, null);
UiObject2 dummyElement = toUiObject2(makeDummySelector(), null);
mykola-mokhnach marked this conversation as resolved.
Show resolved Hide resolved
Gestures gestures = new Gestures(getField("mGestures", dummyElement));
gestureController = new GestureController(getField("mGestureController", dummyElement), gestures);
}
Expand All @@ -171,25 +175,19 @@ public List<AccessibleUiObject> findObjects(Object selector) throws UiAutomator2
} else if (selector instanceof NodeInfoList) {
axNodesList = ((NodeInfoList) selector).getAll();
} else {
throw new InvalidSelectorException("Selector of type " + selector.getClass().getName() + " not supported");
throw new InvalidSelectorException(String.format(
"Selector of type %s not supported",
selector == null ? null : selector.getClass().getName()
));
}
for (AccessibilityNodeInfo node : axNodesList) {
UiObject2 uiObject2 = toUiObject2(toSelector(node), node);
UiObject2 uiObject2 = toUiObject2(toBySelector(node), node);
ret.add(new AccessibleUiObject(uiObject2, node));
}

return ret;
}

@Nullable
private static BySelector toSelector(@Nullable AccessibilityNodeInfo nodeInfo) {
if (nodeInfo == null) {
return null;
}
final CharSequence className = nodeInfo.getClassName();
return className == null ? null : By.clazz(className.toString());
}

public ScreenRotation setRotationSync(ScreenRotation desired) {
if (ScreenRotation.current() == desired) {
return desired;
Expand All @@ -204,6 +202,6 @@ public ScreenRotation setRotationSync(ScreenRotation desired) {
SystemClock.sleep(100);
} while (System.currentTimeMillis() - start < CHANGE_ROTATION_TIMEOUT_MS);
throw new InvalidElementStateException(String.format("Screen rotation cannot be changed to %s after %sms. " +
"Is it locked programmatically?", desired.toString(), CHANGE_ROTATION_TIMEOUT_MS));
"Is it locked programmatically?", desired, CHANGE_ROTATION_TIMEOUT_MS));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@
import java.util.List;
import java.util.Objects;

import static androidx.test.internal.util.Checks.checkNotNull;

import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;

import io.appium.uiautomator2.common.exceptions.UiAutomator2Exception;
Expand Down Expand Up @@ -116,7 +114,7 @@ private static AccessibilityNodeInfo[] getWindowRoots() {
}

private static AccessibilityNodeInfo getTopmostWindowRootFromActivePackage() {
CharSequence activeRootPackageName = checkNotNull(getActiveWindowRoot().getPackageName());
CharSequence activeRootPackageName = Objects.requireNonNull(getActiveWindowRoot().getPackageName());

List<AccessibilityWindowInfo> windows = getWindows();
Collections.sort(windows, new Comparator<AccessibilityWindowInfo>() {
Expand All @@ -139,13 +137,11 @@ public int compare(AccessibilityWindowInfo w1, AccessibilityWindowInfo w2) {

public static AccessibilityNodeInfo[] getCachedWindowRoots() {
if (cachedWindowRoots == null) {
// Multi-window searches are supported since API level 21
boolean isMultiWindowSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
boolean shouldRetrieveAllWindowRoots = isMultiWindowSupported
&& Settings.get(EnableMultiWindows.class).getValue();
boolean shouldRetrieveAllWindowRoots = Settings.get(EnableMultiWindows.class).getValue();
// Multi-window retrieval is needed to search the topmost window from active package.
boolean shouldRetrieveTopmostWindowRootFromActivePackage = isMultiWindowSupported
&& Settings.get(EnableTopmostWindowFromActivePackage.class).getValue();
boolean shouldRetrieveTopmostWindowRootFromActivePackage = Settings.get(
EnableTopmostWindowFromActivePackage.class
).getValue();
/*
* ENABLE_MULTI_WINDOWS and ENABLE_TOPMOST_WINDOW_FROM_ACTIVE_PACKAGE
* are disabled by default
Expand Down
Loading