Android AccessibilityService --- 小白视角

>>长路漫漫立志行高远,寒夜漆漆心勇敢独行。 --致青春

前言:

朋友的一个奇思妙想,促使我接触了 AccessibilityService ,这是个什么东西呢,我们看看申请权限时系统怎么提醒的

权限申请图

意思吧,很清楚,就是这项服务开启之后,您的屏幕操作动作,甚至是操作的屏幕上的视图的内容,这货都能获取到,发挥一下想象力,可以搞事情!(微信自动抢红包等自动化软件就是用的这货)。

 

正文:

STEP 1    Android studio 新建项目

首先,鉴于此功能一般有开发经验的人员才会想到或者用到,所以,android studio 那一堆新建项目之类的就直接跳过了。

STEP 2    项目配置

1、新建服务。比如:

public class AccessibilityServiceHelper extends AccessibilityService {

    private static final String TAG = "AccessibilityServiceHelper";


    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        try {
            if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
                
            } else if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
                
            } else if (event.getEventType() == AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED) {
                
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

   


    @Override
    public void onInterrupt() {

    }
}

根据本人测试的经验解释一下以上监听的三种状态(可监听的状态不止这三种,我这里只用到了这三种,具体可看AccessibilityEvent 类下的各个状态,基本上见名知意)

  •  event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:这种状态是页面切换才会触发,我测试出来的是,必须是Activity切换才会触发,如果Activity中包含ViewPager,ViewPager中是Fragment,那么切换Fragment是不会触发此状态的。
  • event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED:这种状态是监听窗口内容变化,看名字意思是窗口内容变化才会触发,但是我这里实测的是即使没看到变化也会触发,不知道是不是有肉眼看不到的变化。
  • event.getEventType() == AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:通知状态变化,实测是当有弹窗和有通知栏消息时都会触发。

 

2、新建xml配置。app--res 下新建 xml 文件夹,xml文件夹下新建 accessibility_service_config.xml,这个文件名随意起。内容示例如下:

<?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:canRetrieveWindowContent="true"
    android:description="@string/accessibility_service_description"
    android:notificationTimeout="100"
    android:packageNames="com.xxx.xxx"/>

<!--accessibilityEventTypes:表示该服务对界面中的哪些变化感兴趣,即哪些事件通知,比如窗口打开,
滑动,焦点变化,长按等.具体的值可以在AccessibilityEvent类中查到,如typeAllMask表示接受所有的事
件通知.-->

<!--accessibilityFeedbackType:表示反馈方式,比如是语音播放,还是震动-->

<!--canRetrieveWindowContent:表示该服务能否访问活动窗口中的内容.也就是如果你希望在服务中获取
窗体内容的化,则需要设置其值为true.-->

<!--notificationTimeout:接受事件的时间间隔,通常将其设置为100即可.-->

<!--packageNames:表示对该服务是用来监听哪个包的产生的事件-->

各个属性注释都有做解释。(此文件的配置也可在 AccessibilityServiceHelper  的 onServiceConnected 进行配置)

 

3、注册服务、配置服务。(AndroidManifest.xml 中注册服务就不用我说了吧)  示例内容如下:

<service
    android:name="com.xxx.xxx.AccessibilityServiceHelper"
    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_service_config"/>

</service>

CSDN这个代码编辑器太不智能了,都不会缩进,还得我一行一行调,心累。

注意:meta-data部分,android:resource 指向的就是上面 第 2 小步中的xml文件。

 

STEP 3    正主登场

1、判断当前服务开启状态。

/**
* 检测辅助功能是否开启<br>
*/
public static boolean isAccessibilitySettingsOn(Context mContext) {
    int accessibilityEnabled = 0;
    final String service = mContext.getPackageName() + "/" + 替换为自定义的服务类名.class.getCanonicalName();
    try {
        accessibilityEnabled = Settings.Secure.getInt(mContext.getApplicationContext().getContentResolver(),
                    android.provider.Settings.Secure.ACCESSIBILITY_ENABLED);
        } catch (Settings.SettingNotFoundException e) {
            
        }
    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;
                }
            }
        }
    } else {
        Log.v(TAG, "***ACCESSIBILITY IS DISABLED***");
    }
    return false;
}

 

2、跳转至设置也开启服务。

Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
startActivity(intent);

 

3、获取屏幕根节点。

在 AccessibilityServiceHelper 中的

@Override
public void onAccessibilityEvent(AccessibilityEvent event){
    
}

可以通过如下代码获取当前屏幕根节点:

AccessibilityNodeInfo accessibilityNodeInfo = accessibilityService.getRootInActiveWindow();

TIP 1: AccessibilityNodeInfo 有以下几个方法,用于组件的查找:

//通过屏幕显示的内容,比如:下一步、完成等字眼,获取包含该文本的所有节点
List<AccessibilityNodeInfo> nodeInfoList = rootNode.findAccessibilityNodeInfosByText(text);

//通过屏幕显示的view组件的id查找节点,不过前提是要知道该view的节点id,可以通过android SDK -- tools -- monitor.bat 启动 Android Device Monitor 通过捕获屏幕组件获取各个组件的信息,不过这家伙太TM不好用了,点100次也不一定能成功一次,所以建议在 AccessiblityServicerHelper 中遍历根节点,找到你要找的组件的节点,然后查看其信息,进行下一步操作
List<AccessibilityNodeInfo> nodeInfoList = accessibilityNodeInfo.findAccessibilityNodeInfosByViewId(id);

有以下几个方法,用于组件的操作:

//对当前nodeInfo执行单击操作
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);

//对当前nodeInfo执行长按操作
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK);

//对当前nodeInfo执行复制操作
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_COPY);

//对当前nodeInfo执行剪切操作
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CUT);

//对当前nodeInfo执行粘贴操作
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_PASTE);

仅列出常用的几个,在 AccessibilityNodeInfo 中也可以找到更多操作指令。

 

TIP 2: AccessibilityService 也有一个本人比较常用的方法,用于执行全局返回操作:

//执行全局返回操作
service.performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK);

AccessibilityService 中也可以找到更多操作指令。

 

4、通知内容的捕获,就是 event.getEventType() == AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED 时获取的内容 。

//捕获的信息都在集合里了,断点看一下就知道都有啥了,后续内容处理不再赘述
List<CharSequence> text = event.getText();

 

5、获取 WebView 内容。

@Override
protected void onServiceConnected() {
    super.onServiceConnected();
    AccessibilityServiceInfo serviceInfo = new AccessibilityServiceInfo();
    serviceInfo.eventTypes = AccessibilityEvent.TYPES_ALL_MASK;
    serviceInfo.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC;
    serviceInfo.packageNames = new String[]{"com.unionpay"};// 监控的app
    serviceInfo.notificationTimeout = 100;
    serviceInfo.flags = serviceInfo.flags | AccessibilityServiceInfo.FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY;
    setServiceInfo(serviceInfo);
}

重点:serviceInfo.flags = serviceInfo.flags | AccessibilityServiceInfo.FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY;

这个设置可以让 AccessibilityService 能捕获到WebView,只有捕获到了才能拿里面的内容。

其他设置可以保持和 accessibility_service_config.xml 中的各项设置一致,不设置好像会让 accessibility_service_config.xml 对应属性失效,建议设置一遍。

 

下面是对获取到的WebView节点的实际操作:

//这里我是根据页面标题定位获取的,因为我不知道WebView的相关信息,但是我遍历根节点时拿到了页面标题的信息,然后通过节点的 getParent()方法往上找其父节点,一直找到页面根节点,然后再往下找各个子节点,直到找到需要的那个WebView节点
AccessibilityNodeInfo text = AccessibilityUtils.findViewId(this, "com.xxx.xxx:id/tv_title").get(0);

//找到父节点为一个FrameLayout
AccessibilityNodeInfo framelayout = text.getParent();

//发现FrameLayout下的第三个字节点为WebView,但此WebView节点的子节点并不是我要的内容,继续往下找
AccessibilityNodeInfo webview = framelayout.getChild(2);

//又是一个WebView,仍不是目标view
AccessibilityNodeInfo textwebview = webview.getChild(0);

//继续深入找
AccessibilityNodeInfo textwebview1 = textwebview.getChild(0);

//终于不是WebView了,而是一个View结合,其下有很多子view,而子view的信息正是我要找的
AccessibilityNodeInfo views = textwebview1.getChild(0);

//获取子view的内容
String content = views.getChild(0).getContentDescription().toString();
                            

注释已很详细,每个要监听的app页面内容都是不一样的,这里只是一个示例,不要死板的复制,这里只是一个查找思路和方法。

 

至此,AccessibilityService 的相关操作就结束了,可能还有没有说到的地方,但,我相信上面所讲的已经能满足大部分需求了。

也算是呕心沥血了,认真看完必有收获。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章