UIAutomator2 控件 setText 的实现逻辑

Jan 13 2020

在 Android UI 自动化测试中,Google 提供的 UIAutomator2 库中对编辑框设置文本采用的是 UiObject2#setText,今天来分析一下它的逻辑

这里我针对 Android O 的源码做分析,首先打开 UiObject2.java 定位到 setText(String text),里面分了两种情况来设置,一种是,当 API LEVEL <= KITKAT (19) 时,看看是怎么实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// UiObject2.java

public void setText(String text) {
AccessibilityNodeInfo node = getAccessibilityNodeInfo();

// Per framework convention, setText(null) means clearing it
if (text == null) {
text = "";
}

if (UiDevice.API_LEVEL_ACTUAL > Build.VERSION_CODES.KITKAT) {
// 这里先省略
...
} else {
CharSequence currentText = node.getText();
// 不重要的省略
...

// Send the delete key to clear the existing text, then send the new text
InteractionController ic = getDevice().getInteractionController();
ic.sendKey(KeyEvent.KEYCODE_DEL, 0);
ic.sendText(text);
}
}
}

这里先是发了一个删除键 KeyEvent.KEYCODE_DEL 把编辑框清空,然后再将要设置的 text 写入,继续看看这个 sendText 的操作逻辑是什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// InteractionController.java 

public boolean sendText(String text) {
KeyEvent[] events = mKeyCharacterMap.getEvents(text.toCharArray());

if (events != null) {
long keyDelay = Configurator.getInstance().getKeyInjectionDelay();
for (KeyEvent event2 : events) {
KeyEvent event = KeyEvent.changeTimeRepeat(event2,
SystemClock.uptimeMillis(), 0);
if (!injectEventSync(event)) {
return false;
}
SystemClock.sleep(keyDelay);
}
}
return true;
}

先是把 text 字符串转为字符数组事件,然后再逐个的 injectEventSync 下去,这个注入方法经过跳转,会走到 UiAutomation#injectInputEvent,进而走到 UiAutomationConnection#injectInputEvent,看看它内部逻辑如何

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// UiAutomationConnection.java

public boolean injectInputEvent(InputEvent event, int displayId, boolean sync) {
......

final int mode = (sync) ? InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH
: InputManager.INJECT_INPUT_EVENT_MODE_ASYNC;
final long identity = Binder.clearCallingIdentity();
try {
return InputManager.getInstance().injectInputEvent(event, displayId, mode);
} finally {
Binder.restoreCallingIdentity(identity);
}
}

调用了 InputManager#injectInputEvent,这里留意一下 mode,由于传入的 sync = true,所以 mode = INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH,凭字面意思,是说会等待注入事件完成,在此之前会阻塞

所以结论就是,当 API LEVEL <= 19 时,设置编辑框的 text 最终是同步地调用 InputManager#injectInputEvent,通过按键事件的方式注入

另一种情况,当 API LEVEL > 19 时

1
2
3
4
5
6
7
8
9
10
// UiObject2.java

if (UiDevice.API_LEVEL_ACTUAL > Build.VERSION_CODES.KITKAT) {
Bundle args = new Bundle();
args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text);
if (!node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args)) {
// TODO: Decide if we should throw here
Log.w(TAG, "AccessibilityNodeInfo#performAction(ACTION_SET_TEXT) failed");
}
}

执行的是 AccessibilityNodeInfo.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args),进一步分析下去,是执行 AccessibilityInteractionClient#performAccessibilityAction,然后通过远程服务调用 AccessibilityManagerService 的内部类 Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// AccessibilityInteractionClient.java

IAccessibilityServiceConnection connection = getConnection(connectionId);
if (connection != null) {
final int interactionId = mInteractionIdCounter.getAndIncrement();
final long identityToken = Binder.clearCallingIdentity();
final boolean success = connection.performAccessibilityAction(
accessibilityWindowId, accessibilityNodeId, action, arguments,
interactionId, this, Thread.currentThread().getId(), displayId);
Binder.restoreCallingIdentity(identityToken);
if (success) {
return getPerformAccessibilityActionResultAndClear(interactionId);
}
}

内容较多,核心的部分是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// AccessibilityManagerService.java

RemoteAccessibilityConnection connection;
synchronized (mLock) {
......

connection = getConnectionLocked(Display.DEFAULT_DISPLAY, resolvedWindowId);

......
}

......

connection.mConnection.performAccessibilityAction(accessibilityNodeId, action,
arguments, interactionId, callback, mFetchFlags, interrogatingPid,
interrogatingTid);

实际上是执行的 connection.mConnection 的 performAccessibilityAction,这个 connection 类型是 IAccessibilityInteractionConnection,这是一个接口,由于这里直接是 get 方法拿到的,我们从此处并不知道它的实现类是谁,我们需要先找到是哪里 set 或者 add 进来的

分析 getConnectionLocked(Display.DEFAULT_DISPLAY, resolvedWindowId),发现它既可能是全局的 RemoteAccessibilityConnection,也可能是某个 user 的。并且,先尝试获取全局的,如果没有,再去拿该 user 的

1
2
3
4
5
6
7
// AccessibilityManagerService.java

RemoteAccessibilityConnection wrapper = getGlobalInteractionConnections(displayId).get(windowId);

if (wrapper == null) {
wrapper = getCurrentUserStateLocked().mInteractionConnections.get(windowId);
}

并找到 AMS 中只有一处可以添加它的地方,就是在方法 addAccessibilityInteractionConnection 中,调用了该方法的地方在 AccessibilityManager#addAccessibilityInteractionConnection 中

1
2
3
4
5
6
7
8
9
10
11
12
13
// AccessibilityManager.java

public int addAccessibilityInteractionConnection(IWindow windowToken, int displayId, String packageName,
IAccessibilityInteractionConnection connection) {
......

try {
return service.addAccessibilityInteractionConnection(windowToken, displayId, connection, packageName, userId);
} catch (RemoteException re) {
Log.e(LOG_TAG, "Error while adding an accessibility interaction connection. ", re);
}
return View.NO_ID;
}

再继续反推,发现是 ViewRootImpl 中调用了这个方法

1
2
3
4
5
6
7
8
9
10
11
// ViewRootImpl.java

public void ensureConnection() {
final boolean registered = mAttachInfo.mAccessibilityWindowId
!= AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
if (!registered) {
mAttachInfo.mAccessibilityWindowId =
mAccessibilityManager.addAccessibilityInteractionConnection(mWindow,
new AccessibilityInteractionConnection(ViewRootImpl.this));
}
}

原来,AccessibilityInteractionConnection 这玩意儿是 ViewRootImpl 的内部类。这里我们再跳回到 AMS 那一段代码,所以实际操作的是这个内部类的 performAccessibilityAction 方法,在这个方法中,会初始化 AccessibilityInteractionController 去进行操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ViewRootImpl.java

public void performAccessibilityAction(long accessibilityNodeId, int action,Bundle arguments, int interactionId,
IAccessibilityInteractionConnectionCallback callback, int flags,
int interrogatingPid, long interrogatingTid) {

......

if (viewRootImpl != null && viewRootImpl.mView != null) {
viewRootImpl.getAccessibilityInteractionController()
.performAccessibilityActionClientThread(accessibilityNodeId, action, arguments,
interactionId, callback, flags, interrogatingPid, interrogatingTid);
}

......
}

它通过发送 Handler 消息来操作通信,发的消息是 PrivateHandler.MSG_PERFORM_ACCESSIBILITY_ACTION,于是在内部类 PrivateHandler#handleMessage 中我们定位到这个消息对应的操作,以及生效的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// AccessibilityInteractionController.java

private void performAccessibilityActionUiThread(Message message) {

......

View target = null;
if (accessibilityViewId != AccessibilityNodeInfo.ROOT_ITEM_ID) {
target = findViewByAccessibilityId(accessibilityViewId);
} else {
target = mViewRootImpl.mView;
}

......

else if (virtualDescendantId == AccessibilityNodeProvider.HOST_VIEW_ID) {
succeeded = target.performAccessibilityAction(action, arguments);
}

......

}

回到 UiObject2.setText,最初我们调用的是 node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args),最后实际执行的是 View#performAccessibilityAction,所以是直接作用在了对应的视图控件上,相当于是直接修改了 View 的 text 属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
case AccessibilityNodeInfo.ACTION_SET_TEXT: {
if (!isEnabled() || (mBufferType != BufferType.EDITABLE)) {
return false;
}
CharSequence text = (arguments != null) ? arguments.getCharSequence(
AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE) : null;

setText(text);
if (mText != null) {
int updatedTextLength = mText.length();
if (updatedTextLength > 0) {
Selection.setSelection((Spannable) mText, updatedTextLength);
}
}
}

由此得出结论,当 API LEVEL > 19 时,对 UI 控件的 setText 操作,实际上是对该 View 的 text 属性直接操作

今天分析了一下 UIAutomator2 控件 setText 的源码实现逻辑,初步接触了 AccessibilityManagerService 在过程中的连接作用,这是一个 Android 系统服务,Android 中的自动化操作,都是基于它来实现的