https://developer.android.google.cn/guide/topics/ui/accessibility/services.html
无障碍服务,可以监听界面的操作,比如:点击、拖动、界面更新等信息。更为强大的是可以获取屏幕信息,同时具备普通Service的能力。(在别人手机中植入一个无障碍服务并开启,可以监听他的手机操作和屏幕信息,eg:获取微信、QQ当前聊天文字并上传)
因为无障碍服务相比一般Service过于强大,安装后还需要在设置->辅助功能中手动开启。
https://developer.android.google.cn/reference/android/accessibilityservice/AccessibilityService.html
public class MyAccessibilityService extends AccessibilityService{
@Override
protected void onServiceConnected() {
super.onServiceConnected();
}
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
}
@Override
public void onInterrupt() {
}
@Override
public boolean onUnbind(Intent intent) {
return super.onUnbind(intent);
}
}
AccessibilityService继承自普通的Service,因而具备普通Service的生存周期,同时具有自己的一些生命周期。
函数名 | 描述 |
---|---|
onServiceConnected() | (可选)当系统成功连接到该AccessibilityService时,将调用此方法。主要用与一次性配置或调整的代码。 |
onAccessibilityEvent() | (必要)当系统监测到相匹配的AccessibilityEvent事件时,将调用此方法,在整个Service的生命周期中,该方法将被多次调用。 |
onInterrupt() | (必要)系统需要中断AccessibilityService反馈时,将调用此方法。AccessibilityService反馈包括服务发起的震动、音频等行为。 |
onUnbind() | (可选)系统要关闭该服务是,将调用此方法。主要用来释放资源。 |
和普通Service一样,AccessibilityService同样需要在Manifest.xml中注册。
<service
android:name=".MyAccessibilityService"
android:label="@string/accessibility_service_label"
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_config" />
service>
android.permission.BIND_ACCESSIBILITY_SERVICE权限和action是必须的。
同时AccessibilityService需要提供设置列表(meta-data),该设置也可以在运行时通过AccessibilityService.setServiceInfo (AccessibilityServiceInfo info)进行动态设置。不过该方式并不能设置所有的参数,因而推荐的方法为(meta-data)。
android:resource中的地址为:/res/xml/accessibility_service_config.xml。
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/accessibility_service_description"
android:packageNames="com.example.android.apis"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFlags="flagDefault"
android:accessibilityFeedbackType="feedbackSpoken"
android:notificationTimeout="100"
android:canRetrieveWindowContent="true"
android:settingsActivity="com.example.android.accessibility.ServiceSettingsActivity"
/>
各项参数参考AccessibilityServiceInfo。
https://developer.android.google.cn/reference/android/accessibilityservice/AccessibilityServiceInfo.html
AccessibilityService的配置类,可使用setService进行动态设置,同时和上述的accessibility_service_config.xml相对应。
以下参数可以使用“|”表示使用多个选项。
AccessibilityService服务响应的事件类型,只有声明了的类型,系统才会调用该服务的onAccessibilityEvent。
常量 | 描述 |
---|---|
typeViewClicked | 点击事件 |
typeViewSelected | view被选择 |
typeViewScrolled | 滑动事件 |
typeWindowContentChanged | 窗口内容该表 |
typeAllMask | 所有事件 |
完整列表如下:
typeViewClicked typeViewLongClicked typeViewSelected typeViewFocused typeViewTextChanged typeWindowStateChanged typeNotificationStateChanged typeViewHoverEnter typeViewHoverExit typeTouchExplorationGestureStart typeTouchExplorationGestureEnd typeWindowContentChanged typeViewScrolled typeViewTextSelectionChanged typeAnnouncement typeViewAccessibilityFocused typeViewAccessibilityFocusCleared typeViewTextTraversedAtMovementGranularity typeGestureDetectionStart typeGestureDetectionEnd typeTouchInteractionStart typeTouchInteractionEnd typeWindowsChanged typeContextClicked typeAssistReadingContext typeAllMask
AccessibilityService服务的反馈类型。
常量 | 描述 |
---|---|
feedbackSpoken | 语音反馈 |
feedbackHaptic | 触觉(震动)反馈 |
feedbackAudible | 音频反馈 |
feedbackVisual | 视频反馈 |
feedbackGeneric | 通用反馈 |
feedbackAllMask | 以上都具有 |
一些格外的参数。
常量 | 描述 |
---|---|
flagDefault | 默认 |
flagIncludeNotImportantViews | |
flagRequestTouchExplorationMode | |
flagRequestEnhancedWebAccessibility | |
flagReportViewIds | 允许获得view id,需要获取viewid的时候需要该参数,开始没声明导致nodeInfo. getViewIdResourceName()返回的为null |
flagRequestFilterKeyEvents | |
flagRetrieveInteractiveWindows | 允许获得windows,使用getWindows时需要该参数,否则会返回空列表 |
参数名 | 描述 |
---|---|
android:canRetrieveWindowContent | 设置为“true”表示允许获取屏幕信息,使用getWindows、getRootInActiveWindow等函数时需要为“true” |
android:packageNames | 服务响应的事件来源,若设置,则服务只能获取该package发出的事件,不设置可获得所有的事件源 |
android:notificationTimeout | 同一种事件类型触发的最短时间间隔(毫秒) |
android:description | 服务和行为的简短描述 |
… | … |
https://developer.android.google.cn/reference/android/view/accessibility/AccessibilityEvent.html
常用实例化途径:在MyAccessibilityService 中 onAccessibilityEvent(AccessibilityEvent event) 获得该实例,表示监听事件触发。
返回值 | 方法 | 描述 |
---|---|---|
int | getEventType() | 获取事件类型(点击等) |
CharSequence | getPackageName() | 时间来源包名 |
String | toString() | 打印事件 |
https://developer.android.google.cn/reference/android/view/accessibility/AccessibilityNodeInfo.html
常用实例化途径:AccessibilityService.getRootInActiveWindow()获得(当前活动窗口的根节点)。AccessibilityWindowInfo类也能获取该window下的node。
该类与下图有对应关系(通过uiautomatorviewer工具获得):
常用方法:
返回值 | 方法 | 描述 |
---|---|---|
List | findAccessibilityNodeInfosByText(String text) | 通过text寻找子节点 |
List | findAccessibilityNodeInfosByViewId(String viewId) | 通过id查找子节点 |
CharSequence | getPackageName() | Gets the package this node comes from. |
AccessibilityNodeInfo | getParent() | Gets the parent. |
AccessibilityNodeInfo | getChild(int index) | Get the child at given index. |
int | getChildCount() | Gets the number of children. |
CharSequence | getText() | Gets the text of this node. |
String | getViewIdResourceName() | Gets the fully qualified resource name of the source view’s id. |
AccessibilityWindowInfo | getWindow() | Gets the window to which this node belongs. |
boolean | isChecked() | Gets whether this node is checked. |
String | toString() | Returns a string representation of the object. |
比如: 遍历打印当前布局信息可以使用一下代码(布局节点树)
@Override
public void onAccessibilityEvent(AccessibilityEvent accessibilityEvent) {
if(accessibilityEvent.getEventType() == AccessibilityEvent.TYPE_VIEW_CLICKED){
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
dfsnode(nodeInfo,0);
}
}
public void dfsnode(AccessibilityNodeInfo node , int num){
StringBuilder stringBuilder = new StringBuilder();
for(int i = 0 ;i < num ; i++){
stringBuilder.append("__ "); //父子节点之间的缩进
}
Log.i("####",stringBuilder.toString() + node.toString()); //打印
for(int i = 0 ; i < node.getChildCount() ; i++){ //遍历子节点
dfsnode(node.getChild(i),num+1);
}
}
https://developer.android.google.cn/reference/android/view/accessibility/AccessibilityWindowInfo.html
常用实例化途径:AccessibilityService.getWindows()获得,返回值是一个list列表。
返回值 | 方法 | 描述 |
---|---|---|
AccessibilityNodeInfo | getRoot() | 获得该窗口的根节点信息 |
AccessibilityWindowInfo | getChild(int index) | Gets the child window at a given index. |
int | getChildCount() | Gets the number of child windows. |
int | getId() | Gets the unique window id. |
boolean | isActive() | Gets if this window is active. |
boolean | isFocused() | Gets if this window has input focus. |
代码来自涅槃1992:http://www.jianshu.com/p/4cd8c109cdfb
配置代码:
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeNotificationStateChanged|typeWindowStateChanged|
typeWindowContentChanged"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagDefault"
android:canRetrieveWindowContent="true"
android:notificationTimeout="100"
android:packageNames="com.tencent.mm" />
Service代码:
public class RobService extends AccessibilityService {
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
int eventType = event.getEventType();
switch (eventType) {
case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:
handleNotification(event);
break;
case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED:
String className = event.getClassName().toString();
if (className.equals("com.tencent.mm.ui.LauncherUI")) {
getPacket();
} else if (className.equals("com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyReceiveUI")) {
openPacket();
} else if (className.equals("com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyDetailUI")) {
close();
}
break;
}
}
/**
* 处理通知栏信息
*
* 如果是微信红包的提示信息,则模拟点击
*
* @param event
*/
private void handleNotification(AccessibilityEvent event) {
List texts = event.getText();
if (!texts.isEmpty()) {
for (CharSequence text : texts) {
String content = text.toString();
//如果微信红包的提示信息,则模拟点击进入相应的聊天窗口
if (content.contains("[微信红包]")) {
if (event.getParcelableData() != null && event.getParcelableData() instanceof Notification) {
Notification notification = (Notification) event.getParcelableData();
PendingIntent pendingIntent = notification.contentIntent;
try {
pendingIntent.send();
} catch (PendingIntent.CanceledException e) {
e.printStackTrace();
}
}
}
}
}
}
/**
* 关闭红包详情界面,实现自动返回聊天窗口
*/
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
private void close() {
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
if (nodeInfo != null) {
//为了演示,直接查看了关闭按钮的id
List infos = nodeInfo.findAccessibilityNodeInfosByViewId("@id/ez");
nodeInfo.recycle();
for (AccessibilityNodeInfo item : infos) {
item.performAction(AccessibilityNodeInfo.ACTION_CLICK);
}
}
}
/**
* 模拟点击,拆开红包
*/
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
private void openPacket() {
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
if (nodeInfo != null) {
//为了演示,直接查看了红包控件的id
List list = nodeInfo.findAccessibilityNodeInfosByViewId("@id/b9m");
nodeInfo.recycle();
for (AccessibilityNodeInfo item : list) {
item.performAction(AccessibilityNodeInfo.ACTION_CLICK);
}
}
}
/**
* 模拟点击,打开抢红包界面
*/
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private void getPacket() {
AccessibilityNodeInfo rootNode = getRootInActiveWindow();
AccessibilityNodeInfo node = recycle(rootNode);
node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
AccessibilityNodeInfo parent = node.getParent();
while (parent != null) {
if (parent.isClickable()) {
parent.performAction(AccessibilityNodeInfo.ACTION_CLICK);
break;
}
parent = parent.getParent();
}
}
/**
* 递归查找当前聊天窗口中的红包信息
*
* 聊天窗口中的红包都存在"领取红包"一词,因此可根据该词查找红包
*
* @param node
*/
public AccessibilityNodeInfo recycle(AccessibilityNodeInfo node) {
if (node.getChildCount() == 0) {
if (node.getText() != null) {
if ("领取红包".equals(node.getText().toString())) {
return node;
}
}
} else {
for (int i = 0; i < node.getChildCount(); i++) {
if (node.getChild(i) != null) {
recycle(node.getChild(i));
}
}
}
return node;
}
@Override
public void onInterrupt() {
}
@Override
protected void onServiceConnected() {
super.onServiceConnected();
}
}