Android Accessibility使用及事件流程简介

Accessibility是Android从API 4开始提供的一个功能,它主要目的是帮助一些因为有视觉,听觉,身体障碍而无法完全使用触摸屏或铃声等的用户来使用Android的。而实际上现在很多开发者都用它来实现一些其他功能了,比如说微信抢红包,自动安装APK,强制停止应用等。下面来简单介绍一下它的相关使用及原理。

AccessibilityService

它最主要的接口是类AccessibilityService。AccessibilityService是Service的子类,我们可以继承这个类并实现它的抽象方法来监视一个应用的界面元素状态的变化,比如focus变化,一个按钮被click等等。当有这些变化的时候,系统会将这些信息封装在AccessibilityEvent里面,回调AccessibilityService的onAccessibilityEvent(AccessibilityEvent)方法。我们可以实现onAccessibilityEvent来处理这些AccessibilityEvent。下面看一步一步地使用示例:

实现AccessibilityService

这里使用ApiDemo当中将文字转换为语音的例子来介绍,这段代码在/samples//ApiDemos/src/com/example/android/apis/accessibility/TaskBackService,如何使用ApiDemo可以参考Samples。下面是简单介绍里面的核心代码:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
/**
* This class demonstrates how an accessibility service can query
* window content to improve the feedback given to the user.
*/
public class TaskBackService extends AccessibilityService implements OnInitListener {
/** Tag for logging. */
private static final String LOG_TAG = "TaskBackService/onAccessibilityEvent";
/** Comma separator. */
private static final String SEPARATOR = ", ";
/** The class name of TaskListView - for simplicity we speak only its items. */
private static final String TASK_LIST_VIEW_CLASS_NAME =
"com.example.android.apis.accessibility.TaskListView";
/** Flag whether Text-To-Speech is initialized. */
private boolean mTextToSpeechInitialized;
/** Handle to the Text-To-Speech engine. */
private TextToSpeech mTts;
@Override
public void onServiceConnected() {
// Initializes the Text-To-Speech engine as soon as the service is connected.
mTts = new TextToSpeech(getApplicationContext(), this);
}
/**
* Processes an AccessibilityEvent, by traversing the View's tree and
* putting together a message to speak to the user.
*/
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
if (!mTextToSpeechInitialized) {
Log.e(LOG_TAG, "Text-To-Speech engine not ready. Bailing out.");
return;
}
// This AccessibilityNodeInfo represents the view that fired the
// AccessibilityEvent. The following code will use it to traverse the
// view hierarchy, using this node as a starting point.
//
// NOTE: Every method that returns an AccessibilityNodeInfo may return null,
// because the explored window is in another process and the
// corresponding View might be gone by the time your request reaches the
// view hierarchy.
AccessibilityNodeInfo source = event.getSource();
if (source == null) {
return;
}
// Grab the parent of the view that fired the event.
AccessibilityNodeInfo rowNode = getListItemNodeInfo(source);
if (rowNode == null) {
return;
}
// Using this parent, get references to both child nodes, the label and the checkbox.
AccessibilityNodeInfo labelNode = rowNode.getChild(0);
if (labelNode == null) {
rowNode.recycle();
return;
}
AccessibilityNodeInfo completeNode = rowNode.getChild(1);
if (completeNode == null) {
rowNode.recycle();
return;
}
// Determine what the task is and whether or not it's complete, based on
// the text inside the label, and the state of the check-box.
if (rowNode.getChildCount() < 2 || !rowNode.getChild(1).isCheckable()) {
rowNode.recycle();
return;
}
CharSequence taskLabel = labelNode.getText();
final boolean isComplete = completeNode.isChecked();
String completeStr = null;
if (isComplete) {
completeStr = getString(R.string.task_complete);
} else {
completeStr = getString(R.string.task_not_complete);
}
String taskStr = getString(R.string.task_complete_template, taskLabel, completeStr);
StringBuilder utterance = new StringBuilder(taskStr);
// The custom ListView added extra context to the event by adding an
// AccessibilityRecord to it. Extract that from the event and read it.
final int records = event.getRecordCount();
for (int i = 0; i < records; i++) {
AccessibilityRecord record = event.getRecord(i);
CharSequence contentDescription = record.getContentDescription();
if (!TextUtils.isEmpty(contentDescription )) {
utterance.append(SEPARATOR);
utterance.append(contentDescription);
}
}
// Announce the utterance.
mTts.speak(utterance.toString(), TextToSpeech.QUEUE_FLUSH, null);
Log.d(LOG_TAG, utterance.toString());
}
private AccessibilityNodeInfo getListItemNodeInfo(AccessibilityNodeInfo source) {
AccessibilityNodeInfo current = source;
while (true) {
AccessibilityNodeInfo parent = current.getParent();
if (parent == null) {
return null;
}
if (TASK_LIST_VIEW_CLASS_NAME.equals(parent.getClassName())) { //找到TaskListView
return current;
}
// NOTE: Recycle the infos. 记得回收AccessibilityNodeInfo
AccessibilityNodeInfo oldCurrent = current;
current = parent;
oldCurrent.recycle();
}
}
/**
* {@inheritDoc}
*/
@Override
public void onInterrupt() {
/* do nothing */
}

上面这段代码是对TaskList的辅助。

AccessibilityService的配置声明

首先它跟普通的Service一样,需要在Manifest文件中声明:

1
2
3
4
5
6
7
8
<service android:name=".MyAccessibilityService">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data android:name="android.accessibilityservice" android:resource="@xml/taskbackconfig" />
</service>

与其他Service不同的是里面有个元数据标志了配置资源—taskbackconfig。taskbackconfig里面的内容如下:

1
2
3
4
5
6
7
8
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeAllMask"
android:packageNames="com.example.android.apis"
android:accessibilityFeedbackType="feedbackSpoken"
android:notificationTimeout="100"
android:canRetrieveWindowContent="true"
android:description="@string/accessibility_query_window_description" />

其中packageNames限制了监视的包名,accessibilityEventType表示该AccessibilityService想要收到的Event事件类型。accessibilityFeedbackType表示提供的feedback类型,canRetrieveWindowContent是否可以获取窗口的内容,description表示描述信息,在设置中辅助功能里面允许该应用进行辅助设置时,会显示在那个页面下面。

事件处理流程简述

Accessibility是能够获取到控件的消息,这些信息是从哪传递出来的呢?传递的整个过程是怎样的呢?

从View开始

事件是跟View相关的,它从View开始的,View实现了一个叫AccessibilityEventSource的接口,AccessibilityEventSource接口包含了两个函数:

1
2
3
4
public void sendAccessibilityEvent(int eventType);
public void sendAccessibilityEventUnchecked(AccessibilityEvent event);

这两个函数会用来发送AccessibilityEvent,比如在View收到点击事件时,当View的Focus状态变化时等等,View调用这两个接口来开启发送AccessibilityEvent。

反向递归请求到ViewRootImpl

另外ViewGroup也实现了ViewParent接口,而ViewParent有一个接口函数requestSendAccessibilityEvent,在子View中,sendAccessibilityEvent方法最终会调用getParent().requestSendAccessibilityEvent,一层一层地往上调用,最终在ViewRootImpl里面使用AccessibilityManager的sendAccessibilityEvent方法binder机制将AccessibilityEvent发送给AccessibilityManagerService。

AccessibilityManagerService 管理核心

AccessibilityManagerService是在SystemServer中初始化的,并且添加到ServiceManager中。AccessibilityManagerService在有包变化的时候(安装,卸载)更新AccessibilityService绑定,如下代码所示:

1
2
3
4
5
6
7
8
private void updateServicesLocked(UserState userState) {
if (userState.mIsAccessibilityEnabled) {
manageServicesLocked(userState);
} else {
unbindAllServicesLocked(userState);
}
}

发送到AccessibilityService

在出现新的AccessibilityService时会去绑定AccessibilityService获取绑定的结果IAccessibilityServiceClient(实际上是AccessibilityService中onBinder返回的IAccessibilityServiceClientWrapper的Proxy),跟AccessibilityService通信相关的信息保存在AccessibilityManagerService.Service类当中。当AccessibilityManagerService收到Event后,会遍历所有的Service,并通过IAccessibilityServiceClient将Event发送给AccessibilityService。

从AccessibilityEvent事件产生到发送到AccessibilityService,整个流程就是这样了。整个过程如下图所示:

这里写图片描述

这里就简单介绍一下流程,有机会再好好分析一下源码。

###总结

AccessibilityService给我们提供了很多方便,我们可以利用AccessibilityService做很多巧妙的事情。使用AccessibilityService主要步骤就是继承AccessibilityServcie服务,实现onAccessibilityEvent方法,配置好相关的内容,最后在AndroidMainfest声明相关配置。知道AccessibilityService怎么使用,最好也了解整个AccessibilityEvent事件流程,以能够对AccessibilityService有整体把握。