首页 > 技术文章 > Android用AccessibilityService 辅助服务实现微信抢红包APP

kinglearnjava 2015-09-06 15:20 原文

Accessibility Service 可以替代应用与用户交流反馈。

抢红包APP的主要思路:当通知栏出现包含“[微信红包]”关键字的微信消息,就自动跳转到该消息的聊天界面,然后找到微信红包对应的View并模拟点击打开红包和拆红包。


下面以抢红包APP为例,详解其使用方法:

一、创建Accessibility Service

创建一个继承于AccessibilityService的类,并在manifest文件中声明这个Service。标明它监听处理android.accessibilityservice.AccessibilityService事件,声明android.permission.BIND_ACCESSIBILITY_SERVIC权限。由于这是系统级服务,安装后还需要用户在“设置”—->“辅助功能”中给该应用授权。

<service             
    android:label="@string/app_name"
    android:name=".QiangHongBaoService"
    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/qianghongbao_service_config"
    />
</service>


二、配置Accessibility Service

方法一:Java代码中配置

重写onServiceConnected()方法,并在这里进行Service的配置。

@Override
public void onServiceConnected() {
    AccessibilityServiceInfo info = new AccessibilityServiceInfo();    
    // 需要响应的事件类型
    // 此处为通知栏变化事件、界面变化事件
    info.eventTypes = AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED |
            AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED;
    // 如不指定包名,则此服务对所有应用有效
    // 此处指定微信包名
    info.packageNames = new String[]{"com.tencent.mm"};
    // 设置使用的反馈类型
    // 此处设为通用类型
    info.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC;
    // 设置响应时间
    info.notificationTimeout = 100;
    // 应用参数
    this.setServiceInfo(info);

}

方法二:XML文件中配置

从Android4.0开始可以res/xml/目录下添加配置文件,并在manifest文件中通过< meta-data >标签指定。一些特性的选项比如canRetrieveWindowContent仅仅可以在XML可以配置。

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/accessibility_description"
    android:accessibilityEventTypes="typeNotificationStateChanged|typeWindowStateChanged"
    android:packageNames="com.tencent.mm"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:notificationTimeout="100"
    android:accessibilityFlags=""
    android:canRetrieveWindowContent="true"/>
android:description 设置服务的描述,在用户授权的界面可以看到。

android:accessibilityEventTypes 配置要监听的辅助事件。上面只用到typeNotificationStateChanged(通知变化事件)、typeWindowStateChanged(界面变化事件)

android:packageNames 要监听应用的包名,监听多个应用用英文逗号分隔,这里只需要监听微信。

android:accessibilityFeedbackType 设置反馈方式。

FeedbackType 描述
feedbackSpoken 语音反馈
feedbackHaptic 触感反馈
feedbackAudible 表示声音(不是语音)反馈
feedbackVisual 视觉反馈
feedbackGeneric 通用反馈
feedbackAllMask 所有以上的反馈

android:canRetrieveWindowContent 是否能遍历View层级。可以从产生Accessibility 事件的组件与它的父子组件中提取必要的信息。


三、响应Accessibility Event

主要重写下面的前三个方法:
/**
 * 必须重载的方法
 * 接收系统发来的AccessbilityEvent,已经按照配置文件过滤
 * 可使用getEventType()来确定事件的类型
 * 可使用getContentDescription()来提取产生事件的View的相关的文本标签。
 */
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
    //接收事件,如触发了通知栏变化、界面变化等    
}

/**
 * 必须重载的方法 
 * 系统想要中断AccessibilityService返给的响应时会调用
 * 生命周期中会调用多次
 */
@Override
public void onInterrupt() {
    //服务中断,如授权关闭或者将服务杀死
}

/**
 * 可选的方法 
 * 系统会在成功连接上服务时候调用这个方法
 * 在这个方法里你可以做一下初始化工作
 * 例如设备的声音震动管理,也可以调用setServiceInfo()进行配置工作。
 */
@Override
protected void onServiceConnected() {
    super.onServiceConnected();
    //连接服务后,一般是在授权成功后会接收到
}

@Override
protected boolean onKeyEvent(KeyEvent event) {
    //接收按键事件
    return super.onKeyEvent(event);
}


下面是抢微信红包的onAccessibilityEvent方法:

/**
 * 必须重载的方法
 * 接收系统发来的AccessbilityEvent,已经按照配置文件过滤
 */
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
    final int eventType = event.getEventType();
    // 通知栏事件
    if (eventType == AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED) {
        // 通知栏出现新信息
        // 获取通知栏信息内容
        List<CharSequence> texts = event.getText();
        // 检查是否有红包信息
        if (!texts.isEmpty()) {
            for (CharSequence t : texts) {
                String text = String.valueOf(t);
                if (text.contains(HONGBAO_TEXT_KEY)) {
                    openNotify(event); // 点击通知
                    break;
                }
            }
        }
    } else if (eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
        // 窗口改变,如果是聊天界面,则调动打开红包 
        openHongBao(event); 
    }
}


当微信在后台运行时,如果有新的微信消息,通知栏会出现微信的通知(必须在设置中开启此选项,否则此APP无效)。这时,系统会发送一个通知栏变化的事件,即AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED。每个通知都是一个Notification对象,这个对象里会有一个contentIntent属性,该属性是点击该通知后所发送的PendingIntent。根据微信的特性,如果点击通知,则会跳转到对应的聊天界面。代码如下:

/**
 * 打开通知栏消息
 */
private void openNotify(AccessibilityEvent event){
    if (event.getParcelableData() == null || !(event.getParcelableData() instanceof Notification)) {
        return;
    }
    // 将微信的通知栏消息打开
    // 获取Notification对象 
    Notification notification = (Notification) event.getParcelableData();
    // 调用其中的PendingIntent,打开微信界面
    PendingIntent pendingIntent = notification.contentIntent;
    try {
        pendingIntent.send();
    } catch (CanceledException e) {
        e.printStackTrace();
    }
}

当跳转到微信聊天界面时,系统又会发送一个窗口变化的事件,即AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED。抢红包这个过程共包含三个不同的界面:一是聊天界面,点击抢红包的View;二是拆红包界面;三是拆完红包后,查看红包金额的界面。不同的界面,对应的代码不一样。我们可以根据事件的类名来判断当前处于哪个界面。代码如下:

/**
 * 打开微信后,判断是什么界面,并做相应的动作
 */
private void openHongBao(AccessibilityEvent event) {    
    if ("com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyReceiveUI".equals(event.getClassName())) {
        // 拆红包界面
        getPacket();
    } else if ("com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyDetailUI".equals(event.getClassName())) {
        // 拆完红包后,看红包金额的界面
        // 这里什么都不做
    } else if ("com.tencent.mm.ui.LauncherUI".equals(event.getClassName())) {
        // 聊天界面
        openPacket();
    }
}

AccessibilityService中的getRootInActiveWindow方法,可以获得当前活动窗口。然后通过findAccessibilityNodeInfosByText方法,可以找到该窗口下包含特定字符串“领取红包“的AccessibilityNodeInfo对象,最后通过AccessibilityNodeInfo下的performAction方法模拟点击来领取红包

/**
 * 在聊天界面中点红包
 */
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private void openPacket() {
    AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
    if (nodeInfo == null) {
        return;
    }
    // 找到领取红包的点击事件
    List<AccessibilityNodeInfo> list = nodeInfo.findAccessibilityNodeInfosByText("领取红包");
    // 最新的红包领起
    for (int i = list.size() - 1 ; i >= 0; i--) {
        // 通过调试可知[领取红包]是text,本身不可被点击,用getParent()获取可被点击的对象
        AccessibilityNodeInfo parent = list.get(i).getParent();
        // 谷歌重写了toString()方法,不能用它获取ClassName@hashCode串
        if ( parent != null ) {
            parent.performAction(AccessibilityNodeInfo.ACTION_CLICK);
            break; // 只领最新的一个红包
        }
    }
}


模拟点击领取红包后,就会跳转到拆红包的界面。用类似的方法找到包含“拆红包”关键字的View,然后通过performAction方法模拟点击拆红包。

/**
 * 拆红包
 */
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private void getPacket() {
    AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
    if (nodeInfo == null) {
        return;
    }
    List<AccessibilityNodeInfo> list = nodeInfo.findAccessibilityNodeInfosByText("拆红包");
    for (AccessibilityNodeInfo n :list) {
        n.performAction(AccessibilityNodeInfo.ACTION_CLICK);
    }
}

由于这是系统级服务,需要用户在设置中手动开户,所以在MainActivity中添加如下代码:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    
    Button btnStart = (Button) findViewById(R.id.start_button);
    btnStart.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
            open();
        }
    });
}

private void open(){
    try{
        Intent intent = new Intent(android.provider.Settings.ACTION_ACCESSIBILITY_SETTINGS);
        startActivity(intent);
        Toast.makeText(this, "找到伸手党抢红包,然后开启服务即可", Toast.LENGTH_LONG).show();
    } catch (Exception e){
        e.printStackTrace();
    }
}



缺点:

1.依赖微信的通知。如果微信群设置了消息免打扰,那么这个群的一切消息,都不会出现在通知栏上,这样抢红包外挂就无法正常工作了。只有手动打开该微信群,才会抢到最新的一个红包。但是注意,这里仅会抢到最新的那个。

2.如果手机待机了,那么抢红包外挂也无法正常工作。

3.如果现在正打开着微信聊天界面,这时聊天对象发红包了,抢红包外挂也不会工作。因为外挂监听的是通知栏变化事件和窗口变化事件。这时由于与发红包的对象正聊天,所以微信不会在通知栏上给提示,系统也不会发送什么事件。当然,如果不是当前聊天的对象,那么还是可以正常抢到红包的。

四、从View层级中提取更多信息

Android 4.0版本中增加了一个新特性,就是能够用AccessibilityService来遍历View层级,并从产生Accessibility 事件的组件与它的父子组件中提取必要的信息。为了实现这个目的,你需要在XML文件中进行如下的配置:

android:canRetrieveWindowContent="true"
一旦完成,使用getSource()获取一个AccessibilityNodeInfo对象,如果触发事件的窗口是活动窗口,该调用只返回一个对象,如果不是,它将返回null,做出相应的反响。

下面的示例是一个代码片段,与抢红包APP无关,当它接收到一个事件时,执行以下步骤:
1.立即获取到产生这个事件的Parent
2.在这个Parent中寻找文本标签或勾选框
3.如果找到,创建一个文本内容来反馈给用户,提示内容和是否已勾选。
4.如果当遍历View的时候某处返回了null值,那么就直接结束这个方法。

// Alternative onAccessibilityEvent, that uses AccessibilityNodeInfo

@Override
public void onAccessibilityEvent(AccessibilityEvent event) {

    AccessibilityNodeInfo source = event.getSource();
    if (source == null) {
        return;
    }

    // Grab the parent of the view that fired the event.
    AccessibilityNodeInfo rowNode = getListItemNodeInfo(source);
    if (rowNode == null) {
        return;
    }

    // Using this parent, get references to both child nodes, the label and the checkbox.
    AccessibilityNodeInfo labelNode = rowNode.getChild(0);
    if (labelNode == null) {
        rowNode.recycle();
        return;
    }

    AccessibilityNodeInfo completeNode = rowNode.getChild(1);
    if (completeNode == null) {
        rowNode.recycle();
        return;
    }

    // Determine what the task is and whether or not it's complete, based on
    // the text inside the label, and the state of the check-box.
    if (rowNode.getChildCount() < 2 || !rowNode.getChild(1).isCheckable()) {
        rowNode.recycle();
        return;
    }

    CharSequence taskLabel = labelNode.getText();
    final boolean isComplete = completeNode.isChecked();
    String completeStr = null;

    if (isComplete) {
        completeStr = getString(R.string.checked);
    } else {
        completeStr = getString(R.string.not_checked);
    }
    String reportStr = taskLabel + completeStr;
    speakToUser(reportStr);
}


版权声明:本文为博主原创文章,未经博主允许不得转载。

推荐阅读