[whw] accessibilityservice in android

[WHW] AccessibilityService In Android

[WHW] AccessibilityService In Android

What

AccessibilityService是一个辅助类,其本质也是一个Service。可以监听我们手机的焦点,窗口内容变化,按钮点击等等的事件。实现它的服务需要在手机的设置->辅助功能里面找到你自己实现的辅助类,然后打开它就可以进行事件的监听了。还可以对监听的对象进行一些脚本的操作。
AccessibilityService设计的初衷是想为不同能力的人群提供更人性化更友好的用户体验,比如视障,听障,或者年老等特定人群,可以为他们提供特定的事件反馈。它一直在浩瀚的代码田中做一块静静的美代码,直到最近几年它开始受到开发者的青睐,使用的目的却与它设计的初衷有些不太一致。


How

创建

继承AccessibilityService类,实现一个自己的AccessibilityService类,其中要实现两个核心的方法:

    public class RobService extends AccessibilityService {

@Override
public void onAccessibilityEvent(AccessibilityEvent event) { }

@Override
public void onInterrupt() { }
}

该类常用的方法说明如下,详细内容参见官方文档

  • disableSelf() 禁用当前服务,在服务生命周期内可以通过该方法停止服务。
  • getRootInActiveWindow() 如果配置能够获取窗口内容,则会返回当前活动窗口的根结点。
  • setServiceInfo(AccessibilityServiceInfo) 设置当前服务的配置信息。
  • getSeviceInfo() 获取当前服务的配置信息。
  • onAccessibilityEvent(AccessibilityEvent) 用于处理我们监听的各种事件。
  • performGlobalAction(int) 执行全局操作,比如返回,回到主页,打开最近等操作。
  • onKeyEvent(KeyEvent) 如果允许服务监听按键操作,该方法是按键事件的回调,需要注意,这个过程发生在系统处理按键事件之前。
  • onServiceConnected() 系统成功绑定该服务时被触发,也就是当你在设置中开启相应的服务,系统成功的绑定了该服务时会触发。

声明

AccessibilityService本质上也是一个Service,也需要在AndroidManifest.xml中声明该服务。需要注意的是,该服务还必须配置指定的intent-filterperrmission:

   <service
android:name=".RobService"
android:enabled="true"
android:exported="true"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">

<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService"/>
</intent-filter>
</service>

这样,系统才能正确找到该辅助服务。

配置

AndroidManifest.xml声明了该服务之后,接下来就需要对该服务进行一些参数配置。该服务能够被配置用来接受指定类型的事件,监听指定的package,监听窗口内容变化,监听窗口状态变化,获取事件类型的时间等等。有两种配置方式:

  • 方法一: 通过meta-data标签进行配置。
    service的声明中提供一个meta-data标签,然后通过android:resource指定相应的配置文件(在res目录下创建xml目录,并在其中创建配置文件accessibility.xml):

    <service
    android:name=".RobService"
    android:enabled="true"
    android:exported="true"
    android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">

    <intent-filter>
    <action android:name="android.accessibilityservice.AccessibilityService"/>
    </intent-filter>

    <meta-data
    android:name="android.accessibilityservice"
    android:resource="@xml/accessibility"/>

    </service>
    <?xml version="1.0" encoding="utf-8"?>
    <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:accessibilityFlags="flagDefault"
    android:canRetrieveWindowContent="true"
    />

    Note:

    1. 这种方式是在4.0之后才加入的。
    2. 这种方式可以配置所有的辅助服务属性。
  • 方法二: 通过setServiceInfo()方法进行配置。

    通过该方法可以在运行期间动态修改服务配置,通常是在AccessibilityService.onServiceConnected()进行配置,如下:

    @Override
    protected void onServiceConnected() {
    AccessibilityServiceInfo serviceInfo = new AccessibilityServiceInfo();
    serviceInfo.eventTypes = AccessibilityEvent.TYPES_ALL_MASK;
    serviceInfo.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC;
    serviceInfo.packageNames = new String[]{"your package name"};
    serviceInfo.notificationTimeout=100;
    setServiceInfo(serviceInfo);
    }

    AccessibilityServiceInfo类内部封装了AccessibilityService的配置属性,被用于配置AccessibilityService

    Note:
    该方法只能用来配置动态属性:eventTypesfeedbackTypeflagsnotificaionTimeoutpackageNames

  • 配置中的重要属性说明:

    • accessibilityEventTypes: 表示该服务对界面中的哪些变化感兴趣,即哪些事件通知。比如窗口打开滑动,焦点变化,长按等。具体的值可以在AccessibilityEvent类中查到。如typeAllMask表示接受所有的事件通知。
    • accessibilityFeedbackType: 表示反馈方式,比如是语音播放,还是震动。
    • canRetrieveWindowContent: 表示该服务能否访问活动窗口中的内容。也就是如果你希望在服务中获取窗体内容的化,则需要设置其值为true
    • notificationTimeout: 接受事件的时间间隔。
    • packageNames: 表示该服务是用来监听哪个包产生的事件。
    • 更多的配置属性介绍可参阅官方文档

使用

完成上面的配置后,我们的应用就已经具备辅助服务功能,安装到手机上后,需要在设置->辅助功能中找到我们的服务。该服务默认处在关闭状态,手动开启后我们的辅助服务将开始接收它感兴趣的AccessibilityEvent

上面我们说道,onAccessibilityEvent(AccessibilityEvent)是辅助服务的核心方法,其中参数event封装来自界面相关事件的信息,比如我们可以获得该事件的事件类型,进而根据该类型选择不同的处理方式:

public void onAccessibilityEvent(AccessibilityEvent event) {
int eventType = event.getEventType();
switch (eventType) {
case AccessibilityEvent.TYPE_VIEW_CLICKED:
//界面点击
break;
case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED:
//界面文字改动
break;
}
}

说明

  • AccessibilityEvent.TYPE_VIEW_CLICKED: 点击事件。
  • AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED: 文本改变事件。

部分方法说明

  • getEventType(): 获取事件类型。
  • getSource(): 获取事件源对应的结点信息(AccessibilityNodeInfo)。
  • getClassName(): 获取事件源对应类的类型,比如点击事件是由某个Button产生的,那么此时获取的就是Button的完整类名。
  • getText(): 获取事件源的文本信息,这里获取的是一个集合对象。其中包含事件源及事件源的Children的所有文本信息。如果事件源不是ViewGroup,此时获取的集合只有一个元素,就是该事件源的文本内容。
  • isEnabled(): 事件源(对应的界面控件)是否处在可用状态。
  • getItemCount(): 如果事件源是树结构,将返回该树根节点下子节点的数量。

到这里,我们就可以在onAccessibilityEvent(AccessibilityEvent)方法中实现我们自己的“黑科技”了。AccessibilityService的基本使用就这么简单。更多的使用细节可以参阅官方文档


Why

事件从哪里来

为了简单这里以AccessibilityEvent.TYPE_VIEW_CLICKED类型事件为例。
AccessibilityService既然能够知道我们点击了什么内容,那肯定是在View的事件派发过程中做了什么手脚呗,于是撸起袖子翻源码:
View.java

    /**
* Call this view's OnClickListener, if it is defined. Performs all normal
* actions associated with clicking: reporting accessibility event, playing
* a sound, etc.
*
* @return True there was an assigned OnClickListener that was called, false
* otherwise is returned.
*/

public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}

// 看到了曙光,View通过该方法发送一个AccessibilityEvent
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}

几经搜索在performClick方法中发现了蛛丝马迹,即上面注释中提到的sendAccessibilityEvent(int)方法,下面列出该方法及它调用的几个方法:
View.java

    /**
* Sends an accessibility event of the given type. If accessibility is
* not enabled this method has no effect. The default implementation calls
* {@link #onInitializeAccessibilityEvent(AccessibilityEvent)} first
* to populate information about the event source (this View), then calls
* {@link #dispatchPopulateAccessibilityEvent(AccessibilityEvent)} to
* populate the text content of the event source including its descendants,
* and last calls
* {@link ViewParent#requestSendAccessibilityEvent(View, AccessibilityEvent)}
* on its parent to request sending of the event to interested parties.
* <p>
* If an {@link AccessibilityDelegate} has been specified via calling
* {@link #setAccessibilityDelegate(AccessibilityDelegate)} its
* {@link AccessibilityDelegate#sendAccessibilityEvent(View, int)} is
* responsible for handling this call.
* </p>
*
* @param eventType The type of the event to send, as defined by several types from
* {@link android.view.accessibility.AccessibilityEvent}, such as
* {@link android.view.accessibility.AccessibilityEvent#TYPE_VIEW_CLICKED} or
* {@link android.view.accessibility.AccessibilityEvent#TYPE_VIEW_HOVER_ENTER}.
*
* @see #onInitializeAccessibilityEvent(AccessibilityEvent)
* @see #dispatchPopulateAccessibilityEvent(AccessibilityEvent)
* @see ViewParent#requestSendAccessibilityEvent(View, AccessibilityEvent)
* @see AccessibilityDelegate
*/

public void sendAccessibilityEvent(int eventType) {

// mAccessibilityDelegate是个代理,一般情况下都是null
// 后面跟AccessibilityEvent相关的方法都会是这样的模式,先判断代理存在与否,不存在则使用View自己的实现
// 为了后面描述简单,后面在遇到mAccessibilityDelegate相关的判断逻辑时会直接以“代理判断”带过,并直接分析XXXInternal方法的实现。
if (mAccessibilityDelegate != null) {
mAccessibilityDelegate.sendAccessibilityEvent(this, eventType);
} else {
sendAccessibilityEventInternal(eventType);
}
}

public void sendAccessibilityEventInternal(int eventType) {

// 只有当AccessibilityService可用时才会发送AccessibilityEvent
if (AccessibilityManager.getInstance(mContext).isEnabled()) {

// AccessibilityEvent是通过AccessibilityEvent.obtain(eventType)从pools中获取的
// 类似Message的内部pools机制,更有效的利用系统资源
sendAccessibilityEventUnchecked(AccessibilityEvent.obtain(eventType));
}
}

public void sendAccessibilityEventUnchecked(AccessibilityEvent event) {

// 通过一个“代理判断”,最终执行逻辑在sendAccessibilityEventUncheckedInternal中
if (mAccessibilityDelegate != null) {
mAccessibilityDelegate.sendAccessibilityEventUnchecked(this, event);
} else {
sendAccessibilityEventUncheckedInternal(event);
}
}

public void sendAccessibilityEventUncheckedInternal(AccessibilityEvent event) {
if (!isShown()) {
return;
}

// 下面是发送一个AccessibilityEvent的三个步骤
// step 1
onInitializeAccessibilityEvent(event);
// Only a subset of accessibility events populates text content.
if ((event.getEventType() & POPULATING_ACCESSIBILITY_EVENT_TYPES) != 0) {

// step 2
dispatchPopulateAccessibilityEvent(event);
}

// In the beginning we called #isShown(), so we know that getParent() is not null.
// step 3
getParent().requestSendAccessibilityEvent(this, event);
}

sendAccessibilityEvent(int)方法的官方注释里能看到,该方法会发送一个AccessibilityEvent,发送一个AccessibilityEvent分为三步:

  1. onInitializeAccessibilityEvent(AccessibilityEvent): 收集关于事件源的信息(主要是填充AccessibilityEvent内部的mSourceNode)。
  2. dispatchPopulateAccessibilityEvent(AccessibilityEvent): 收集事件源的文本内容,如果触发事件的View是一个ViewGroup的话,还会将该步骤派发给该ViewGroup的Children,最终收集到的文本是一个集合。
  3. ViewParent#requestSendAccessibilityEvent(View, AccessibilityEvent): 通过向当前Viewparent请求来发送这个AccessibilityEvent给所有感兴趣的AccessibilityService

接下来就顺序分析发送AccessibilityEvent的三个步骤。

Step 1

onInitializeAccessibilityEvent(AccessibilityEvent)通过一个“代理判断后”最终的实现在onInitializeAccessibilityEventInternal(AccessibilityEvent)中:
View.java

    public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {

// 主要关注该方法,step 1的主要逻辑就是填充该event的source信息
// 注意这里传入的参数为this,也就是当前点击事件发生的View
event.setSource(this);
event.setClassName(getAccessibilityClassName());
event.setPackageName(getContext().getPackageName());
event.setEnabled(isEnabled());
event.setContentDescription(mContentDescription);

switch (event.getEventType()) {
case AccessibilityEvent.TYPE_VIEW_FOCUSED: {
ArrayList<View> focusablesTempList = (mAttachInfo != null)
? mAttachInfo.mTempArrayList : new ArrayList<View>();
getRootView().addFocusables(focusablesTempList, View.FOCUS_FORWARD, FOCUSABLES_ALL);
event.setItemCount(focusablesTempList.size());
event.setCurrentItemIndex(focusablesTempList.indexOf(this));
if (mAttachInfo != null) {
focusablesTempList.clear();
}
} break;
case AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED: {
CharSequence text = getIterableTextForAccessibility();
if (text != null && text.length() > 0) {
event.setFromIndex(getAccessibilitySelectionStart());
event.setToIndex(getAccessibilitySelectionEnd());
event.setItemCount(text.length());
}
} break;
}
}

该方法主要是对传进来的AccessibilityEvent的一些成员做初始换赋值操作。这里主要关注event.setSource(this)方法。需要先介绍下AccessibilityEvent的结构,AccessibilityEvent是派生自AccessibilityRecord的,它并没有重写AccessibilityRecordsetSource(View)方法,所以直接看AccessibilityRecord.setSource(View):
AccessibilityRecord.java

public class AccessibilityRecord {

...

private static final int UNDEFINED = -1;

// setSource就是设置该成员变量
AccessibilityNodeInfo mSourceNode;

// step 2中获取的文本信息会放到这个集合中
final List<CharSequence> mText = new ArrayList<CharSequence>();

int mConnectionId = UNDEFINED;

AccessibilityRecord() {
}

public void setSource(View source) {
setSource(source, UNDEFINED);
}

// 传入的virtualDescendantId == UNDEFINED == -1
public void setSource(View root, int virtualDescendantId) {
enforceNotSealed();
boolean important = true;
mSourceWindowId = UNDEFINED;
clearSourceNode();
if (root != null) {
if (virtualDescendantId == UNDEFINED ||
virtualDescendantId == AccessibilityNodeInfo.UNDEFINED_ITEM_ID) {

// 因为 virtualDescendantId == UNDEFINED,进入该分支
important = root.isImportantForAccessibility();

// 这里构造一个AccessibilityNodeInfo,这里的root是通过参数传进来的,也就是上一步传进来的发生点击事件的view
mSourceNode = root.createAccessibilityNodeInfo();
} else {
AccessibilityNodeProvider provider = root.getAccessibilityNodeProvider();
if (provider != null) {
mSourceNode = provider.createAccessibilityNodeInfo(virtualDescendantId);
}
}

mSourceWindowId = root.getAccessibilityWindowId();
}
setBooleanProperty(PROPERTY_IMPORTANT_FOR_ACCESSIBILITY, important);
}

...

}

setSource(View)会继续调用setSource(View, int)方法,我在代码注释中简单表述了代码的运行逻辑,接下来主要关心mSourceNode的构造过程,createAccessibilityNodeInfo()经过一个”代理判断”最终执行createAccessibilityNodeInfoInternal()方法:

View.java

    /**
* @see #createAccessibilityNodeInfo()
*
* @hide
*/

public AccessibilityNodeInfo createAccessibilityNodeInfoInternal() {

// getAccessibilityNodeProvider方法同样会经过一个"代理判断",最终返回null
AccessibilityNodeProvider provider = getAccessibilityNodeProvider();
if (provider != null) {
return provider.createAccessibilityNodeInfo(AccessibilityNodeProvider.HOST_VIEW_ID);
} else {

// 因为得到的provider == null,所以进入该分支
AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(this);
onInitializeAccessibilityNodeInfo(info);
return info;
}
}

Note:
AccessibilityNodeInfo跟上面说的到AccessibilityEvent一样也采用类似Messagepools机制来更合理的使用系统资源。

代码流程在注释中已标出,onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo)经过一个“代理判断”,最终调用onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo)
View.java

    /**
* @see #onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo)
*
* Note: Called from the default {@link AccessibilityDelegate}.
*
* @hide
*/

public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {

// 这个方法主要就是在初始化传进来的AccessibilityNodeInfo对象,填充其的各个成员

if (mAttachInfo == null) {
return;
}

Rect bounds = mAttachInfo.mTmpInvalRect;

getDrawingRect(bounds);
info.setBoundsInParent(bounds);

getBoundsOnScreen(bounds, true);
info.setBoundsInScreen(bounds);

// 注意
// 首先,AccessibilityNodeInfo也需要设置parent,那众多的AccessibilityNodeInfo会构成一个AccessibilityNodeInfo Tree
// AccessibilityNodeInfo Tree是跟View Tree的结构相似
// 但是,AccessibilityNodeInfo Tree跟View Tree并不是完全一致!
// 但是,AccessibilityNodeInfo Tree跟View Tree并不是完全一致!!
// 但是,AccessibilityNodeInfo Tree跟View Tree并不是完全一致!!!
// 重要的事情说三遍...
ViewParent parent = getParentForAccessibility();
if (parent instanceof View) {
info.setParent((View) parent);
}

if (mID != View.NO_ID) {
View rootView = getRootView();
if (rootView == null) {
rootView = this;
}

View label = rootView.findLabelForView(this, mID);
if (label != null) {
info.setLabeledBy(label);
}

if ((mAttachInfo.mAccessibilityFetchFlags
& AccessibilityNodeInfo.FLAG_REPORT_VIEW_IDS) != 0
&& Resources.resourceHasPackage(mID)) {
try {
String viewId = getResources().getResourceName(mID);
info.setViewIdResourceName(viewId);
} catch (Resources.NotFoundException nfe) {
/* ignore */
}
}
}

if (mLabelForId != View.NO_ID) {
View rootView = getRootView();
if (rootView == null) {
rootView = this;
}
View labeled = rootView.findViewInsideOutShouldExist(this, mLabelForId);
if (labeled != null) {
info.setLabelFor(labeled);
}
}

if (mAccessibilityTraversalBeforeId != View.NO_ID) {
View rootView = getRootView();
if (rootView == null) {
rootView = this;
}
View next = rootView.findViewInsideOutShouldExist(this,
mAccessibilityTraversalBeforeId);
if (next != null && next.includeForAccessibility()) {
info.setTraversalBefore(next);
}
}

if (mAccessibilityTraversalAfterId != View.NO_ID) {
View rootView = getRootView();
if (rootView == null) {
rootView = this;
}
View next = rootView.findViewInsideOutShouldExist(this,
mAccessibilityTraversalAfterId);
if (next != null && next.includeForAccessibility()) {
info.setTraversalAfter(next);
}
}

info.setVisibleToUser(isVisibleToUser());

if ((mAttachInfo != null) && ((mAttachInfo.mAccessibilityFetchFlags
& AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS) != 0)) {
info.setImportantForAccessibility(isImportantForAccessibility());
} else {
info.setImportantForAccessibility(true);
}

info.setPackageName(mContext.getPackageName());
info.setClassName(getAccessibilityClassName());
info.setContentDescription(getContentDescription());

info.setEnabled(isEnabled());
info.setClickable(isClickable());
info.setFocusable(isFocusable());
info.setFocused(isFocused());
info.setAccessibilityFocused(isAccessibilityFocused());
info.setSelected(isSelected());
info.setLongClickable(isLongClickable());
info.setContextClickable(isContextClickable());
info.setLiveRegion(getAccessibilityLiveRegion());

// TODO: These make sense only if we are in an AdapterView but all
// views can be selected. Maybe from accessibility perspective
// we should report as selectable view in an AdapterView.
info.addAction(AccessibilityNodeInfo.ACTION_SELECT);
info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_SELECTION);

if (isFocusable()) {
if (isFocused()) {
info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_FOCUS);
} else {
info.addAction(AccessibilityNodeInfo.ACTION_FOCUS);
}
}

if (!isAccessibilityFocused()) {
info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
} else {
info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
}

if (isClickable() && isEnabled()) {
info.addAction(AccessibilityNodeInfo.ACTION_CLICK);
}

if (isLongClickable() && isEnabled()) {
info.addAction(AccessibilityNodeInfo.ACTION_LONG_CLICK);
}

if (isContextClickable() && isEnabled()) {
info.addAction(AccessibilityAction.ACTION_CONTEXT_CLICK);
}

CharSequence text = getIterableTextForAccessibility();
if (text != null && text.length() > 0) {
info.setTextSelection(getAccessibilitySelectionStart(), getAccessibilitySelectionEnd());

info.addAction(AccessibilityNodeInfo.ACTION_SET_SELECTION);
info.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
info.setMovementGranularities(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER
| AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD
| AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH);
}

info.addAction(AccessibilityAction.ACTION_SHOW_ON_SCREEN);
populateAccessibilityNodeInfoDrawingOrderInParent(info);
}

Note:
我们实现的AccessibilityService中捕获到AccessibilityEvent时可以通过AccessibilityEvent.getSource()拿到该event对应的AccessibilityNodeInfo对象来获得更丰富的上下文信息。

这个方法是在为传进来的AccessibilityNodeInfo初始化各个成员信息。这里主要关注AccessibilityNodeInfo Tree的构建,即AccessibilityNodeInfoParent的指向,这个Parent的赋值是决定于View.getParentForAccessibility()方法的返回值。
上面说道AccessibilityNodeInfo Tree的结构跟View Tree的结构并不完全一致,看下View.getParentForAccessibility()的实现你会明白:
View.java

    /**
* Gets the parent for accessibility purposes. Note that the parent for
* accessibility is not necessary the immediate parent. It is the first
* predecessor that is important for accessibility.
*
* @return The parent for accessibility purposes.
*/

public ViewParent getParentForAccessibility() {
// 该方法的返回值将作为当前View对应的AccessibilityNodeInfo在AccessibilityNodeInfo Tree中的Parent

if (mParent instanceof View) {
View parentView = (View) mParent;

// 关键的判断在这里
if (parentView.includeForAccessibility()) {

// 如果进入到这个分支,则该AccessibilityNodeInfo在AccessibilityNodeInfo Tree中与其Parent的关系与对应View在View Tree中与其Parent的关系保持一致
return mParent;
} else {
// 如果进入到这个分支,则说明当前View的parent被skipped
// 接下来会递归调用当前View的Parent的getParentForAccessibility()方法
// 所以返回值有可能是当前AccessibilityNodeInfo对应的View在View Tree中的爷爷甚至祖先结点
return mParent.getParentForAccessibility();
}
}
return null;
}

注释中解释了AccessibilityNodeInfo TreeView Tree可能不完全一致的原因,一句话简单说就是在View Tree中我是你爸爸,但在AccessibilityNodeInfo Tree中有可能我的爸爸才是你的爸爸(即你爷爷变成你爸爸)。虽然有点拗口,但是道理不难懂。
具体到底谁是你爸爸,关键看includeForAccessibility()这个方法:
View.java

    /**
* Whether to regard this view for accessibility. A view is regarded for
* accessibility if it is important for accessibility or the querying
* accessibility service has explicitly requested that view not
* important for accessibility are regarded.
*
* @return Whether to regard the view for accessibility.
*
* @hide
*/

public boolean includeForAccessibility() {
if (mAttachInfo != null) {
return (mAttachInfo.mAccessibilityFetchFlags
& AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS) != 0
|| isImportantForAccessibility();
}
return false;
}

/**
* Computes whether this view should be exposed for accessibility. In
* general, views that are interactive or provide information are exposed
* while views that serve only as containers are hidden.
*
* @return Whether the view is exposed for accessibility.
* @see #setImportantForAccessibility(int)
* @see #getImportantForAccessibility()
*/

public boolean isImportantForAccessibility() {
final int mode = (mPrivateFlags2 & PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_MASK)
>> PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_SHIFT;
if (mode == IMPORTANT_FOR_ACCESSIBILITY_NO
|| mode == IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) {
return false;
}

// Check parent mode to ensure we're not hidden.
ViewParent parent = mParent;
while (parent instanceof View) {
if (((View) parent).getImportantForAccessibility()
== IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) {
return false;
}
parent = parent.getParent();
}

return mode == IMPORTANT_FOR_ACCESSIBILITY_YES || isActionableForAccessibility()
|| hasListenersForAccessibility() || getAccessibilityNodeProvider() != null
|| getAccessibilityLiveRegion() != ACCESSIBILITY_LIVE_REGION_NONE;
}

这两个方法就是判断谁是你爸爸的逻辑,看官方的注释解释大致意思是如果当前View是对于AccessibilityService来说是重要的,或者AccessibilityService设置了FLAG_INCLUDE_NOT_IMPORTANT_VIEWS标志,那么你爸爸就还是你爸爸。
找到爸爸后,onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo)就会把找到的爸爸赋值给当前在初始化的AccessibilityNodeInfo,并继续初始化它的其他成员变量。细节部分可参阅源码。
初始化完AccessibilityEventAccessibilityNodeInfo的信息后,Step 1告一段落。

Step 2

step 2主要是为event填充Text信息。通过View.dispatchPopulateAccessibilityEvent(AccessibilityEvent)实现,该方法也是经过一个“代理判断”后最终进入到View.dispatchPopulateAccessibilityEventInternal(AccessibilityEvent)
View.java

    /**
* @see #dispatchPopulateAccessibilityEvent(AccessibilityEvent)
*
* Note: Called from the default {@link AccessibilityDelegate}.
*
* @hide
*/

public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) {

// 该方法内部通过代理判断后最终进入onPopulateAccessibilityEventInternal方法
onPopulateAccessibilityEvent(event);

// 注意这里的返回值,默认是返回false
return false;
}

这是该方法在View.java中的实现,因为View作为一个直接呈现内容的单元,只要负责填充自身的Textevent就好了,所以这里直接通过onPopulateAccessibilityEvent(AccessibilityEvent)方法完成填充Text的逻辑。最后返回false

接着看一下View.onPopulateAccessibilityEventInternal(AccessibilityEvent)方法是如何实现的:
View.java

    /**
* @see #onPopulateAccessibilityEvent(AccessibilityEvent)
*
* Note: Called from the default {@link AccessibilityDelegate}.
*
* @hide
*/

public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) {
}

没错,一个空方法。因为View作为一个高度抽象的控件并无法准确的决定是否往AccessibilityEvent内填充内容,填充什么内容,所以具体的逻辑交由各个不同的派生View各自实现。我们不妨来看看TextViewonPopulateAccessibilityEventInternal(AccessibilityEvent)方法:
TextView.java

    /** @hide */
@Override
public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) {
super.onPopulateAccessibilityEventInternal(event);

final CharSequence text = getTextForAccessibility();
if (!TextUtils.isEmpty(text)) {
event.getText().add(text);
}
}

这里看到在TextView中果然实现了该方法,并将该TextView的内容加入到AccessibilityEvent的文本集合中。

Note:
AccessibilityEvent中文本的保存是以一个集合来存储的,上面分析Step 1时也提到过,该集合是AccessibilityEvent派生自AccessibilityRecord的一个属性。如果该AccessibilityEvent对应的是一个ViewGroup的话,该集合中会存放ViewGroup下所有ChildrenText内容,后面将详细说明。

前面看到的是当点击事件发生在View上时的处理逻辑,当点击事件发生在ViewGroup时,dispatchPopulateAccessibilityEventInternal的实现才真正体现了dispatch的意义:
ViewGroup.java

    /** @hide */
@Override
public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) {

// 该返回值决定这收集工作的流向,类似View中事件的处理逻辑
boolean handled = false;
if (includeForAccessibility()) {

// 调用View的dispatchPopulateAccessibilityEventInternal收集该ViewGroup自身的文本内容
handled = super.dispatchPopulateAccessibilityEventInternal(event);
if (handled) {
return handled;
}
}
// Let our children have a shot in populating the event.
ChildListForAccessibility children = ChildListForAccessibility.obtain(this, true);
try {

// 遍历Children,分别调用他们的dispatchPopulateAccessibilityEvent去收集Children的文本内容
final int childCount = children.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = children.getChildAt(i);
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
handled = child.dispatchPopulateAccessibilityEvent(event);
if (handled) {
return handled;
}
}
}
} finally {
children.recycle();
}
return false;
}

逻辑相对也比较简单,在注释中有简要说明。最终的结果就是在AccessibilityEvent的文本集合中保存了被点击的ViewGroup以及其Children的所有Text

Note:
这里的派发逻辑跟Touch事件的派发逻辑很相似,只要有一个ViewdispatchPopulateAccessibilityEventInternal(AccessibilityEvent)方法中返回true,则会结束整个派发流程。(默认返回false)。
dispatchPopulateAccessibilityEventInternal(AccessibilityEvent)结束后,AccessibilityEvent的文本内容收集完毕,Step 2告一段落。

Step 3

Step 3就是将AccessibilityEvent发送出去,AccessibilityEvent的发送是通过向View Tree中的父节点请求来完成的,即通过调用当前ViewParentrequestSendAccessibilityEvent(View, AccessibilityEvent)方法实现:
ViewGroup.java

    @Override
public boolean requestSendAccessibilityEvent(View child, AccessibilityEvent event) {
ViewParent parent = mParent;
if (parent == null) {
return false;
}

// onRequestSendAccessibilityEvent内部同样经过代理判断后,最终调用onRequestSendAccessibilityEventInternal,而该方法默认返回true
final boolean propagate = onRequestSendAccessibilityEvent(child, event);
if (!propagate) {
return false;
}
// 最终递归调用parent.requestSendAccessibilityEvent(this, event)
return parent.requestSendAccessibilityEvent(this, event);
}

这里看到通过一个递归,其实将发送AccessibilityEvent的逻辑一直往上扔,直到View Tree的根节点。递归最终会调用到ViewRootImpl.requestSendAccessibilityEvent(View, AccessibilityEvent)方法中。

Note:
ViewRootImpl.setView(View)中我们设置了根节点ViewParentViewRootImpl。所以最终会调用到ViewRootImpl中。

ViewRootImpl.java

    @Override
public boolean requestSendAccessibilityEvent(View child, AccessibilityEvent event) {
if (mView == null || mStopped || mPausedForTransition) {
return false;
}
// Intercept accessibility focus events fired by virtual nodes to keep
// track of accessibility focus position in such nodes.
final int eventType = event.getEventType();
switch (eventType) {
case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED: {
final long sourceNodeId = event.getSourceNodeId();
final int accessibilityViewId = AccessibilityNodeInfo.getAccessibilityViewId(
sourceNodeId);
View source = mView.findViewByAccessibilityId(accessibilityViewId);
if (source != null) {
AccessibilityNodeProvider provider = source.getAccessibilityNodeProvider();
if (provider != null) {
final int virtualNodeId = AccessibilityNodeInfo.getVirtualDescendantId(
sourceNodeId);
final AccessibilityNodeInfo node;
if (virtualNodeId == AccessibilityNodeInfo.UNDEFINED_ITEM_ID) {
node = provider.createAccessibilityNodeInfo(
AccessibilityNodeProvider.HOST_VIEW_ID);
} else {
node = provider.createAccessibilityNodeInfo(virtualNodeId);
}
setAccessibilityFocus(source, node);
}
}
} break;
case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED: {
final long sourceNodeId = event.getSourceNodeId();
final int accessibilityViewId = AccessibilityNodeInfo.getAccessibilityViewId(
sourceNodeId);
View source = mView.findViewByAccessibilityId(accessibilityViewId);
if (source != null) {
AccessibilityNodeProvider provider = source.getAccessibilityNodeProvider();
if (provider != null) {
setAccessibilityFocus(null, null);
}
}
} break;


case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: {
handleWindowContentChangedEvent(event);
} break;
}

// 通过AccessibilityManager来发送该辅助服务事件
mAccessibilityManager.sendAccessibilityEvent(event);
return true;
}

该方法中前面的逻辑从官方注释上看是针对由虚拟结点触发的辅助服务焦点事件做了一个拦截处理,这里不是我们分析的重点。到方法的最后AccessibilityEventAccessibilityManager完成发送。(憋了那么久,终于发车了。。。)

事件到哪里去

AccessibilityManagerAccessibilityManagerService服务在Client端的代理的一个Wrapper。有点类似ActivityManagerActivityManagerService的关系。从这里能够看出AccessibilityEvent通过应用进程的初始化加工后最终发送给了system_server进程由
AccessibilityManagerService服务来管理分配。AccessibilityManagerService内部管理了所有应用实现的AccessibilityService,它会将该事件发送给已打开并绑定成功的AccessibilityService来处理。倘若这个时候我们实现的AccessibilityService是被打开的,那么AccessibilityManagerService将向我们实现的AccessibilityService发送一个AccessibilityEvent

小结

  • AccessibilityEvent由产生事件的View负责构造并初始化。
  • AccessibilityEvent由产生事件的View开始向parent节点请求发送,最终交由ViewRootImpl将事件通过binder机制发送给system_server处理。
  • 虽然其他类型的AccessibilityEvent的事件源或者某些细节与TYPE_VIEW_CLICK类型的事件可能有所差别,但是大致流程其实是不变的:从某个事件源产生事件后->初始化该事件信息->最终将事件发送给system_server处理

总结

本文从WHW三个角度试图将AccessibilityService讲解的更清楚些。Why能够帮助我们更深入的理解AccessibilityService,并运用到实际有需要的项目中去。更多的是站在应用层的角度并结合了一个比较简单的TYPE_VIEW_CLICK事件类型去解释。目的在于由浅入深理解整个大致流程。很多细节部分并没有一一说明,毕竟源码才是最好的老师(RTFSC)。希望能够给大家有所帮助,如果有错误的地方也欢迎大牛指正。后面有时间的话会站在系统层的角度基于AccessibilityManagerService再做一次学习记录。

谢谢~

条件允许的话尽量把新的,没用过的东西都按WHW的模式走一遍,记录下来,对自己也是一个积累沉淀的过程。


参考

https://developer.android.com/reference/android/accessibilityservice/AccessibilityService.html
http://www.jianshu.com/p/4cd8c109cdfb
http://blog.csdn.net/jwzhangjie/article/details/47205299