Skip to content

Commit

Permalink
refactor: Tune the logic of BySelector creation (#590)
Browse files Browse the repository at this point in the history
  • Loading branch information
mykola-mokhnach authored Jan 2, 2024
1 parent ada878b commit 9d2c2dd
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 45 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* 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());
return result.clickable(node.isClickable())
.longClickable(node.isLongClickable())
.focusable(node.isFocusable())
.scrollable(node.isScrollable());
}

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

private static BySelector makeDummySelector() {
return By.text(String.format("DUMMY:%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,7 @@
import io.appium.uiautomator2.utils.ReflectionUtils;

import static io.appium.uiautomator2.model.AccessibleUiObject.toAccessibleUiObject;
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 +57,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 +67,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 +95,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,28 +129,56 @@ 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);
Gestures gestures = new Gestures(getField("mGestures", dummyElement));
gestureController = new GestureController(getField("mGestureController", dummyElement), gestures);
Class<?> gesturesClass = ReflectionUtils.getClass("androidx.test.uiautomator.Gestures");
// TODO: UIAutomator lib has changed this class significantly in v2.3.0,
// TODO: so this approach won't work anymore
Method gesturesFactory = ReflectionUtils.getMethod(
gesturesClass, "getInstance", UiDevice.class
);
Gestures gestures;
try {
gestures = new Gestures(gesturesFactory.invoke(gesturesClass, getUiDevice()));
} catch (InvocationTargetException | IllegalAccessException e) {
throw new UiAutomator2Exception("Cannot get an instance of the Gestures class", e);
}
Class<?> gestureControllerClass = ReflectionUtils.getClass(
"androidx.test.uiautomator.GestureController"
);
Method gestureControllerFactory = ReflectionUtils.getMethod(
gestureControllerClass, "getInstance", UiDevice.class
);
try {
gestureController = new GestureController(
gestureControllerFactory.invoke(gestureControllerClass, getUiDevice()),
gestures
);
} catch (InvocationTargetException | IllegalAccessException e) {
throw new UiAutomator2Exception("Cannot get an instance of the GestureController class", e);
}
}
return gestureController;
}
Expand All @@ -171,25 +197,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 +224,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 @@ -21,7 +21,6 @@
import android.graphics.Point;
import android.graphics.Rect;

import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.uiautomator.Direction;
import androidx.test.uiautomator.UiObject2;

Expand Down
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

0 comments on commit 9d2c2dd

Please sign in to comment.