导读:

资深的安卓程序员想必都了解,安卓的通知监听服务(NotificationListenerService)可以监听通知栏的信息,从通知栏信息里获取到我们想要的收款信息(比如收款类型、收款金额)。但是,这个通知监听服务有个弊端,如果APP没有发送通知,那就没办法知道有没有收到款,特别是现在的微信 和QQ,二维码收款不再发送通知,而是在自身的APP里给出提示,这样就没办法使用通知监听服务了。因此,我们需要使用安卓的无障碍服务(AccessibilityService),来监听APP界面的变化,实时获取微信、QQ的收款信息。

准备工作:

  • Android Studio开发工具;
  • 电脑安装安卓模拟器(夜神模拟器或者逍遥模拟器),模拟器里安装好微信、QQ;

步骤:

一、创建服务

1、新建一个Service类,继承自AccessibilityService,重写onServiceConnected()方法、onAccessibilityEvent()方法和onInterrupt()方法

package net.zy13.skhelper.service;

import android.accessibilityservice.AccessibilityService;
import android.view.accessibility.AccessibilityEvent;
import net.zy13.skhelper.utils.LogUtil;
/**
 * 收款无障碍服务
 */
public class SkAccessibilityService extends AccessibilityService{
    /**
     * 当服务启动的时候会被调用
     */
    @Override
    public void onServiceConnected(){
        LogUtil.debug( ":无障碍服务连接成功");
    }
    /**
     * 监听窗口变化的回调
     */
    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        // 获取事件类型,在对应的事件类型中对相应的节点进行操作
        int eventType = event.getEventType();
        //根据事件回调类型进行处理
        switch (eventType) {
            //当通知栏发生改变时
            case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:
                break;
            //当窗口的状态发生改变时(界面改变)
            case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
                break;
            //内容改变
            case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED:
                break;
            //滑动变化
            case AccessibilityEvent.TYPE_VIEW_SCROLLED:
                break;
        }
    }
    /**
     * 中断服务的回调
     */
    @Override
    public void onInterrupt() {

    }
}

 2、在AndroidManifest.xml清单里注册服务

<service
            android:name=".service.SkAccessibilityService"
            android:enabled="true"
            android:exported="true"
            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/config_accessibility" />
        </service>

3、在res资源目录里创建一个xml目录,然后在xml里创建config_accessibility.xml配置文件,这个config_accessibility.xml文件主要是保存AccessibilityService服务的配置参数,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFeedbackType="feedbackAllMask"
    android:canRetrieveWindowContent="true"
    android:notificationTimeout="100"
    android:packageNames="com.eg.android.AlipayGphone,com.tencent.mm,com.tencent.mobileqq"
    android:description="@string/access_desc" />

上面的android:description项要提取到strings.xml里,不要问为什么,因为不这样会报错。 

<resources>
    <string name="access_desc">收款助手使用无障碍服务监听收款信息</string>
</resources>

xml参数说明: 

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

accessibilityFeedbackType:表示反馈方式,比如是语音播放,还是震动;

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

notificationTimeout:接受事件的时间间隔,通常将其设置为100即可;

packageNames:表示对该服务是用来监听哪个包的产生的事件,要监听多个APP包,就用逗号隔开(如果不设置就是接收所有的);

description:对该无障碍功能的描述,会显示在系统开启服务的设置界面;

accessibilityEventTypes可选参数

typeAllMask

接收所有事件

typeWindowStateChanged

监听窗口状态变化,比如打开一个popupWindow,dialog,Activity切换等等

typeWindowContentChanged

监听窗口内容改变,比如根布局子view的变化

typeWindowsChanged

监听屏幕上显示的系统窗口中的事件更改,此事件类型只应由系统分派

typeNotificationStateChanged

监听通知变化,比如notifacation和toast

typeViewClicked

监听view点击事件

typeViewLongClicked

监听view长按事件

typeViewFocused

监听view焦点事件

typeViewSelected

监听AdapterView中的上下文选择事件

typeViewTextChanged

监听EditText的文本改变事件

typeViewHoverEnter

typeViewHoverExit

监听view的视图悬停进入和退出事件

typeViewScrolled

监听view滚动,此类事件通常不直接发送

typeViewTextSelectionChanged

监听EditText选择改变事件

typeViewAccessibilityFocused

监听view获得可访问性焦点事件

typeViewAccessibilityFocusCleared

监听view清除可访问性焦点事件

typeGestureDetectionStart

typeGestureDetectionEnd

监听手势开始和结束事件

typeTouchInteractionStart

typeTouchInteractionEnd

监听用户触摸屏幕事件的开始和结束

typeTouchExplorationGestureStart

typeTouchExplorationGestureEnd

监听触摸探索手势的开始和结束

二、使用服务 

1、当APP退出注销时,无障碍服务也会关闭,因此我们最好是在APP上判断服务是否开启,没开启的话,就提醒用户主动授权开启无障碍服务,判断无障碍服务是否开启的方法如下:

/**
     * 判断无障碍服务是否开启
     * @param mContext
     * @return
     */
    private 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 {
        }

        return false;
    }

2、只要用户授权,无障碍服务就会自动运行,下面这串代码是跳转到无障碍服务授权页面的,用户授权才可以真正使用无障碍服务

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

 三、无障碍服务处理事件信息

AccessibilityService中常用的方法的介绍:

  1. disableSelf():禁用当前服务,也就是在服务可以通过该方法停止运行
  2. findFoucs(int falg):查找拥有特定焦点类型的控件
  3. getRootInActiveWindow():如果配置能够获取窗口内容,则会返回当前活动窗口的根结点
  4. getSeviceInfo():获取当前服务的配置信息
  5. onAccessibilityEvent(AccessibilityEvent event):有关AccessibilityEvent事件的回调函数,系统通过sendAccessibiliyEvent()不断的发送AccessibilityEvent到此处
  6. performGlobalAction(int action):执行全局操作,比如返回,回到主页,打开最近等操作
  7. setServiceInfo(AccessibilityServiceInfo info):设置当前服务的配置信息
  8. getSystemService(String name):获取系统服务
  9. onKeyEvent(KeyEvent event):如果允许服务监听按键操作,该方法是按键事件的回调,需要注意,这个过程发生了系统处理按键事件之前
  10. onServiceConnected():系统成功绑定该服务时被触发,也就是当你在设置中开启相应的服务,系统成功的绑定了该服务时会触发,通常我们可以在这里做一些初始化操作
  11. onInterrupt():服务中断时的回调


这里我们主要关注onAccessibilityEvent事件:


/**
     * 监听窗口变化的回调
     */
    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        // 获取事件类型,在对应的事件类型中对相应的节点进行操作
        int eventType = event.getEventType();
        //根据事件回调类型进行处理
        switch (eventType) {
            //当通知栏发生改变时
            case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:
                break;
            //当窗口的状态发生改变时(界面改变)
            case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
                //主要是在这里监听支付宝、微信、QQ界面上的收款信息
                break;
            //内容改变
            case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED:
                break;
            //滑动变化
            case AccessibilityEvent.TYPE_VIEW_SCROLLED:
                break;
        }
    }

 事件回调类型AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED,表示窗口的内容发生改变时,比如微信收到消息,会显示在消息列表里,就会触发这个事件的回调,我们就可以在这个回调里获取相应的信息。

四、获取节点信息

获取了界面窗口变化后,这个时候就要获取控件的节点,整个窗口的节点本质是个树结构,通过以下方式操作节点信息

1、获取窗口节点(根节点)

AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();

2、获取指定子节点(控件节点)

//通过文本找到对应的节点集合
List<AccessibilityNodeInfo> list = nodeInfo.findAccessibilityNodeInfosByText(text);
//通过控件ID找到对应的节点集合,如com.tencent.mm:id/gd
List<AccessibilityNodeInfo> list = nodeInfo.findAccessibilityNodeInfosByViewId(id);

 通过控件ID获取节点是最方便的,下面开始教大家怎么获取APP的控件ID

五、获取APP的控件ID 

 先说明一下,控件ID实际上是我们在界面布局里设定的android:id的编译后的值,这个是唯一的。

1、前面的准备工作里,我们已经在安卓模拟器里安装了支付宝、微信和QQ,这里以微信为例子,我们在安卓模拟器里登录微信

android监控微信消息 安卓手机 监控微信_无障碍

2、在Android的Sdk目录里找到tools工具目录,双击打开里面的monitor.bat

android监控微信消息 安卓手机 监控微信_微信_02

 3、在DDMS的Devices列表里会看到有一个安卓模拟器,如下图:

android监控微信消息 安卓手机 监控微信_android监控微信消息_03

4、点击Dump View Hierarchy UI Automotor图标,会自动获取当前模拟器中打开的APP界面UI信息

android监控微信消息 安卓手机 监控微信_android_04

 5、打开后出现如下图,通过鼠标点击到界面的文字或图片上,可以在节点详情里看到控件ID

android监控微信消息 安卓手机 监控微信_android监控微信消息_05

6、通过控件ID获取节点信息的方法

/**
     * 通过控件ID获取节点信息
     * @param id
     * @return
     */
    @SuppressLint("NewApi")
    public String getNodeInfo(String id){
        String result="";
        // 获取当前整个活动窗口的根节点
        AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
        // 获取父节点
        //nodeInfo.getParent();
        // 获取子节点
        //nodeInfo.getChild(0);
        if (nodeInfo != null) {
            // 通过文本找到对应的节点集合
            // List<AccessibilityNodeInfo> list = nodeInfo.findAccessibilityNodeInfosByText("");
            // 通过控件ID找到对应的节点集合,如com.tencent.mm:id/gd
            List<AccessibilityNodeInfo> list = nodeInfo.findAccessibilityNodeInfosByViewId(id);
            for (AccessibilityNodeInfo item : list) {
                String str=item.getText().toString();
                if (str != null && str.length() != 0){
                    result=str;
                    break;
                }
                //模拟点击
                //item.performAction(AccessibilityNodeInfo.ACTION_CLICK);
                //模拟长按
                //item.performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK);
                //模拟获取焦点
                //item.performAction(AccessibilityNodeInfo.ACTION_FOCUS);
                //模拟粘贴
                //item.performAction(AccessibilityNodeInfo.ACTION_PASTE);
            }
        }
        return result;
    }

 六、通过无障碍服务获取微信收款信息的代码:

package net.zy13.skhelper.service;

import android.accessibilityservice.AccessibilityService;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.PowerManager;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;


import net.zy13.skhelper.MainApplication;
import net.zy13.skhelper.handle.PostHandle;
import net.zy13.skhelper.utils.LogUtil;
import net.zy13.skhelper.utils.PreferenceUtil;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 收款无障碍服务
 */
public class SkAccessibilityService extends AccessibilityService{
    private PreferenceUtil preference;
    private PowerManager.WakeLock wakeLock = null;
    private PostHandle postHandle=null;
    /**
     * 当服务启动的时候会被调用
     */
    @Override
    public void onServiceConnected(){
        LogUtil.debug( ":无障碍服务连接成功");
        if(preference==null) {
            preference = PreferenceUtil.getInstance(MainApplication.getAppContext());
        }
        //开启wakelock,使CPU处于不休眠的状态,开启后需要重启手机
        if(preference.isWakelock()){
            PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
            wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, SkAccessibilityService.class.getName());
            wakeLock.acquire();
        }
    }
    /**
     * 监听窗口变化的回调
     */
    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        // 获取事件类型,在对应的事件类型中对相应的节点进行操作
        int eventType = event.getEventType();
        //根据事件回调类型进行处理
        switch (eventType) {
            //当通知栏发生改变时
            case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:
                break;
            //当窗口的状态发生改变时(界面改变)
            case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
                String className = event.getClassName().toString();
                //获取界面信息
                getUiInfo(className);
                break;
            //内容改变
            case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED:
                break;
            //滑动变化
            case AccessibilityEvent.TYPE_VIEW_SCROLLED:
                break;
        }
    }
    /**
     * 中断服务的回调
     */
    @Override
    public void onInterrupt() {

    }

    /**
     * 通过控件ID获取节点信息
     * @param id
     * @return
     */
    @SuppressLint("NewApi")
    public String getNodeInfo(String id){
        String result="";
        // 获取当前整个活动窗口的根节点
        AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
        // 获取父节点
        //nodeInfo.getParent();
        // 获取子节点
        //nodeInfo.getChild(0);
        if (nodeInfo != null) {
            // 通过文本找到对应的节点集合
            // List<AccessibilityNodeInfo> list = nodeInfo.findAccessibilityNodeInfosByText("");
            // 通过控件ID找到对应的节点集合,如com.tencent.mm:id/gd
            List<AccessibilityNodeInfo> list = nodeInfo.findAccessibilityNodeInfosByViewId(id);
            for (AccessibilityNodeInfo item : list) {
                String str=item.getText().toString();
                if (str != null && str.length() != 0){
                    result=str;
                    break;
                }
                //模拟点击
                //item.performAction(AccessibilityNodeInfo.ACTION_CLICK);
                //模拟长按
                //item.performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK);
                //模拟获取焦点
                //item.performAction(AccessibilityNodeInfo.ACTION_FOCUS);
                //模拟粘贴
                //item.performAction(AccessibilityNodeInfo.ACTION_PASTE);
            }
        }
        return result;
    }
    /**
     * 获取界面信息
     * @param classname
     */
    @SuppressLint("NewApi")
    public void getUiInfo(String classname){
        LogUtil.debug( "无障碍服务窗口状态改变,类名为"+classname);
        //通过类名判断是不是微信
        if(classname.equals("com.tencent.mm.ui.LauncherUI")) {
            LogUtil.debug( "正在使用无障碍服务获取微信收款信息:");
            String title=getNodeInfo("com.tencent.mm:id/fzg");
            if(title.contains("微信支付")||title.contains("微信收款助手")) {
                String content = getNodeInfo("com.tencent.mm:id/e7t");
                //String time = getNodeInfo("com.tencent.mm:id/j0l");
                Map<String,String> postmap=new HashMap<String,String>();
                postmap.put("type","weixin");
                postmap.put("title",title);
                postmap.put("money",extractMoney(content));
                postmap.put("content",content);
                SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                postmap.put("time",sdf.format(new Date()));
                LogUtil.debug("获取到的信息集合:"+postmap.toString());
            }
        }
    }
    /**
     * 从字符串里提取金额
     * @param content
     * @return
     */
    protected  String extractMoney(String content){
        Pattern pattern = Pattern.compile("(收款|收款¥|向你付款|向您付款|入账|到帐)(([1-9]{1}\\d*)|([0]{1}))(\\.(\\d){0,2})?元");
        Matcher matcher = pattern.matcher(content);
        List<String> list = new ArrayList<>();
        while(matcher.find()){
            list.add(matcher.group());
        }
        if(list.size()>0){
            String tmp=list.get(list.size()-1);
            Pattern patternnum = Pattern.compile("(([1-9]{1}\\d*)|([0]{1}))(\\.(\\d){0,2})?");
            Matcher matchernum = patternnum.matcher(tmp);
            if(matchernum.find())
                return matchernum.group();
            return null;
        }else
            return null;
    }
}

1、现在在安卓模拟器里运行我们的APP,来监听微信的收款信息 

android监控微信消息 安卓手机 监控微信_微信_06

2、通过查看debug日志 ,发现无障碍服务已经连接成功了,现在我用另一个微信号扫收款二维码进行付款,看看能不能监听到。

android监控微信消息 安卓手机 监控微信_无障碍_07

3、可以获取到收款信息,但有个bug,就是你要主动打开微信才可以获取到,如果微信退到后台或者关闭,无障碍服务就不能实时监听到收款信息了。