导读:
资深的安卓程序员想必都了解,安卓的通知监听服务(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:对该无障碍功能的描述,会显示在系统开启服务的设置界面;
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中常用的方法的介绍:
- disableSelf():禁用当前服务,也就是在服务可以通过该方法停止运行
- findFoucs(int falg):查找拥有特定焦点类型的控件
- getRootInActiveWindow():如果配置能够获取窗口内容,则会返回当前活动窗口的根结点
- getSeviceInfo():获取当前服务的配置信息
- onAccessibilityEvent(AccessibilityEvent event):有关AccessibilityEvent事件的回调函数,系统通过sendAccessibiliyEvent()不断的发送AccessibilityEvent到此处
- performGlobalAction(int action):执行全局操作,比如返回,回到主页,打开最近等操作
- setServiceInfo(AccessibilityServiceInfo info):设置当前服务的配置信息
- getSystemService(String name):获取系统服务
- onKeyEvent(KeyEvent event):如果允许服务监听按键操作,该方法是按键事件的回调,需要注意,这个过程发生了系统处理按键事件之前
- onServiceConnected():系统成功绑定该服务时被触发,也就是当你在设置中开启相应的服务,系统成功的绑定了该服务时会触发,通常我们可以在这里做一些初始化操作
- 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,这里以微信为例子,我们在安卓模拟器里登录微信
2、在Android的Sdk目录里找到tools工具目录,双击打开里面的monitor.bat
3、在DDMS的Devices列表里会看到有一个安卓模拟器,如下图:
4、点击Dump View Hierarchy UI Automotor图标,会自动获取当前模拟器中打开的APP界面UI信息
5、打开后出现如下图,通过鼠标点击到界面的文字或图片上,可以在节点详情里看到控件ID
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,来监听微信的收款信息
2、通过查看debug日志 ,发现无障碍服务已经连接成功了,现在我用另一个微信号扫收款二维码进行付款,看看能不能监听到。
3、可以获取到收款信息,但有个bug,就是你要主动打开微信才可以获取到,如果微信退到后台或者关闭,无障碍服务就不能实时监听到收款信息了。