Android的辅助功能accessibility的具体文档可以查看:google accessibility说明文档

accessibility是一个非常强大的功能,可以实现监听手机上的各种事件,比如窗口的变化,查找屏幕上当前显示的文字,以及模拟点击等功能,并且通过accessibility可以完成很多一般应用无法完成事件,比如发送物理或虚拟返回键的指令是通过如下代码实现的:

1
2
3
4
5
6
7
8
9
10
new Thread() {
public void run() {
try {
Instrumentation inst = new Instrumentation();
inst.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK);
} catch (Exception e) {
e.printStackTrace();
}
}
}.start();

该指令是需要在系统进程中运行的应用使用才会生效的命令,不过如果使用辅助功能就可以通过发送全局操作触发该操作.这样就可以完成在非root的手机上实现悬浮球的返回键的功能.

另外辅助功能还可以实现各种脚本和无root权限伪静默安装.绿色守护的非root模式和uc的静默安装还有那些抢红包的应用都是用该方式实现的,不过如果一些恶意应用拿到了辅助功能的权限是灾难性的.

辅助功能的开启关闭是在Android的系统设置中的辅助功能或者叫无障碍选项中.


如何编写一个辅助功能服务

1.创建一个class继承于AccessibilityService,会强制重写2个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 当系统检测到一个匹配你辅助服务过滤器中设置参数的AccessibilityEvent时,调用该方法,运行时多次调用
*
* @param event 在用户交互使用时系统返回的event事件
*/
@Override
public void onAccessibilityEvent(AccessibilityEvent event)

/**
* 当你的服务对系统事件的反馈被中断时,调用该方法。该方法同上面方法一样,也是被多次调用的
*/
@Override
public void onInterrupt()

2.接下来有2种选择

  1. 可以在res中创建xml文件夹,创建一个xml文件,文件名随意,在xml文件中对辅助服务进行配置,格式大致如下:

    1
    2
    3
    4
    5
    6
    7
    8
    <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeNotificationStateChanged"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:accessibilityFlags="flagReportViewIds"
    android:description="@string/app_name"
    android:notificationTimeout="100"
    android:canRetrieveWindowContent="true"
    android:packageNames="com.nesscurie.accessibility" />
  2. 重写onServiceConnected() (当应用成功连接到你的辅助性服务时,系统调用该方法)在该方法中进行配置

    1
    2
    3
    4
    5
    6
    7
    AccessibilityServiceInfo serviceInfo = new AccessibilityServiceInfo();
    serviceInfo.eventTypes = ...;
    serviceInfo.feedbackType = ...;
    serviceInfo.notificationTimeout = ...;
    serviceInfo.packageNames = new String[]{...};
    serviceInfo.flags = ...;
    setServiceInfo(serviceInfo);
各属性解释:
  1. accessibilityEventTypes / eventTypes 事件类型

    typeAllMask / AccessibilityEvent.TYPES_ALL_MASK 全局事件响应
    typeViewClicked / AccessibilityEvent.TYPE_VIEW_CLICKED 点击事件

  2. accessibilityFeedbackType / feedbackType 反馈方式

    feedbackGeneric / AccessibilityServiceInfo.FEEDBACK_GENERIC 通用的反馈
    feedbackAudible / AccessibilityServiceInfo.FEEDBACK_AUDIBLE 声音反馈
    feedbackSpoken / AccessibilityServiceInfo.FEEDBACK_SPOKEN 语音反馈

  3. notificationTimeout / notificationTimeout 响应毫秒值

  4. packageNames 监听的应用的包名,可指定多个包名,xml文件中使用,隔开

  5. accessibilityFlags / flags 用于之后node.getViewIdResourceName()的权限

  6. description 辅助功能的描述

  7. canRetrieveWindowContent 从一个AccessibilityEvent中调查完全视图层级的能力隐式地暴露私有用户信息给你的辅助服务,必须通过配置XML文件请求这个级别的访问权,不在你的服务配置xml文件中包含这个设置,那么对getSource()的调用会失败。

3.在manifest中注册辅助功能的服务

1
2
3
4
5
6
7
8
<service
android:name=".MyAccessibilityService"
android:label="我的辅助功能"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
</service>

name:对应的是自定义service的包名

label:对应了在系统辅助功能开关界面中,你的service的名字

description:则是点击对应的服务进入开关界面后,该服务的简介

permission:对应的权限(亦可在service中单独写出来)

1
<uses-permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE"/>

intent-filter:指定了执行的组件为辅助功能类

如果是使用xml文件配置的辅助服务,还需要在service节点下添加meta-data节点,在resource指定自己编写的xml文件
1
2
3
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_config" />

4.接下来就可以在onAccessibilityEvent(AccessibilityEvent event)处理各种事件:

可以使用 event.getEventType()来获取各种事件消息:

  1. 基本窗口view的变化都可以使用这个type来监听:
    AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED

  2. 打开popupwindow,菜单,对话框时候会触发:
    AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED

  3. 更加精确的代表了基于当前event.source中的子view的内容变化:
    AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED

  4. 窗口的变化:
    AccessibilityEvent.TYPE_WINDOWS_CHANGED

当前event的节点信息.有两种方式获取:

  1. 使用event获取:
    AccessibilityNodeInfo nodeInfo = event.getSource();

  2. 在accessibility中直接获取:
    AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();

使用完要记得调用recycle();进行释放以免内存泄漏

nodeInfo.recycle();

接下来就可以使用下面的方法来获取对应的所有节点的集合

findAccessibilityNodeInfosByViewId(String str)
findAccessibilityNodeInfosByText(String str)

获取id的方法:

  1. 使用DDMS的hierarchy View来查找对应的viewId,但是有很多手机是没有办法获取,只会提示:Unable to get view server version from device XXXXX

  2. 在这个时候,在AccessibiltiyService的配置中添加的flag。flagReportViewIds就可以派上用场了

    在窗口改变时,获取并遍历所有的node,即打印出node对应的文字和id

    传入方法id的格式为: 应用的包名 + “:id/“ + 获取到的id

获取到节点之后即可使用节点进行点击等各种事件:
1
2
3
performAction(AccessibilityNodeInfo.ACTION_CLICK) 

//AccessibilityNodeInfo中有各种各样的事件以常量的形势声明.

除了使用节点进行调用还可以调用全局事件:

返回键
performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK);
HOME键
performGlobalAction(AccessibilityService.GLOBAL_ACTION_HOME);
最近打开应用列表
performGlobalAction(AccessibilityService.GLOBAL_ACTION_RECENTS);
打开通知栏
performGlobalAction(AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS);
锁屏
performGlobalAction(AccessibilityService.GLOBAL_ACTION_POWER_DIALOG);
设置
performGlobalAction(AccessibilityService.GLOBAL_ACTION_QUICK_SETTINGS);

判断对应的辅助功能有没有打开的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public boolean isAccessibilitySettingsOn(Context mContext) {
int accessibilityEnabled = 0;
final String service = getPackageName() + "/" + MyAccessibilityService.class.getCanonicalName(); //这里改成自己的class
try {
accessibilityEnabled = Settings.Secure.getInt(mContext.getApplicationContext().getContentResolver(),
Settings.Secure.ACCESSIBILITY_ENABLED);
} catch (Settings.SettingNotFoundException ignored) {
}
TextUtils.SimpleStringSplitter mStringColonSplitter = new TextUtils.SimpleStringSplitter(':');
if (accessibilityEnabled == 1) {
String settingValue = Settings.Secure.getString(mContext.getApplicationContext().getContentResolver(),
Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
if (settingValue != null) {
mStringColonSplitter.setString(settingValue);
while (mStringColonSplitter.hasNext()) {
String accessibilityService = mStringColonSplitter.next();
if (accessibilityService.equalsIgnoreCase(service)) {
return true;
}
}
}
}
return false;
}

这个方法就是通过获取系统设置的存储辅助功能的数据库的内容提供者,然后查看已开的辅助功能的包名+类名
是否有自己,有就表示开了.(后面的用java程序悄悄打开对应的辅助功能其实就是去修改这个值)

打开系统设置辅助功能的界面:
1
2
Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
startActivity(intent);

辅助功能的介绍就是这些,下面开始使用adb运行java程序悄悄打开辅助功能:系统设置的各个选项通过数据库形式进行保存,通过user为shell的高权限将系统设置存储的配置进行修改.

在此之前先写一个简单的辅助功能应用:

  1. 编写一个类继承AccessibilityService,只在onStartCommand做一个返回键的全局操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class MyAccessibilityService extends AccessibilityService {

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {

    }

    @Override
    public void onInterrupt() {

    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
    performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK);
    return super.onStartCommand(intent, flags, startId);
    }
    }
  2. xml配置最普通的内容

    1
    2
    3
    4
    5
    6
    7
    8
    <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeNotificationStateChanged"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:accessibilityFlags="flagDefault"
    android:description="@string/app_name"
    android:notificationTimeout="100"
    android:canRetrieveWindowContent="true"
    android:packageNames="com.nesscurie.accessibility" />
  3. 在清单文件进行注册

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <service
    android:name=".MyAccessibilityService"
    android: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_config" />
    </service>
  4. 在Activity的xml文件中设置一个button,为了方便,声明onClick属性为onClick,在activity中编写onClick方法为如下内容

    1
    2
    3
    4
    5
    6
    7
    8
    public void onClick(View view) {
    if (!isAccessibilitySettingsOn(this)) {
    Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
    startActivity(intent);
    return;
    }
    startService(new Intent(this, MyAccessibilityService.class));
    }

部署到设备上,打开应用点击按钮可以发现打开了系统设置的辅助功能的界面,开启对应的辅助功能,回到app,可以发现点击按钮后触发了返回键的功能.


关闭该应用辅助功能,接下来就是编写能修改系统设置的纯java代码:使用adb的settings命令将设置的数据库中对应字段改变

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
public class Temp {
public static void main(String[] args) {
System.out.println("running");
//这里是包名+辅助功能类名
String cmd1 = "settings put secure enabled_accessibility_services com.nesscurie.accessibility/com.nesscurie.accessibility.MyAccessibilityService";

String cmd2 = "settings put secure accessibility_enabled 1";
execShell(cmd1);
execShell(cmd2);
}

//运行命令行的方法
private static void execShell(String cmd) {
try {
Process p = Runtime.getRuntime().exec(cmd);
BufferedReader br = new BufferedReader(new InputStreamReader(
p.getInputStream()));
String readLine = br.readLine();
while (readLine != null) {
System.out.println(readLine);
readLine = br.readLine();
}
if (br != null) {
br.close();
}
p.destroy();
p = null;
} catch (IOException e) {
e.printStackTrace();
}
}
}

使用上一篇中的方法,将代码编译为.class后转为.dex,push到手机中,比如data/local/tmp目录:

1
2
3
adb shell
cd data/local/tmp
app_process -Djava.class.path=Temp.dex data/local/tmp Temp

可以看到打印出runnig后,系统设置的该应用的辅助功能界面刷新为开,本来是需要系统权限(设置运行在系统进程中,具有系统权限)才能进行修改的选项,就这么悄悄的打开了.


辅助功能是一个非常强大的功能,不仅能为不少不方便的人群提供使用手机的方式,还可以带来不少特殊的体验,不过国内大部分app都没有针对少数不方便的人群进行辅助功能的开发,并且很多时候辅助功能被用在了不正确的途径,不禁是一件令人深思的事情.