天天看点

AccessibilityService辅助功能的研究及实现自动抢红包功能原理使用范围注意事项使用步骤

辅助功能(AccessibilityService)其实是一个Android系统提供给的一种服务,本身是继承Service类的。这个服务提供了增强的用户界面,旨在帮助残障人士使用android设备和应用,在后台运行,可以监听用户界面的一些状态转换,例如页面切换、焦点改变、通知、Toast等,并在触发AccessibilityEvents时由系统接收回调。后来被开发者另辟蹊径,用于一些插件开发,比如微信红包助手,还有一些需要监听第三方应用的插件。

原理

AccessibilityService具有很多强大的功能。但是从开发者的角度看,其实最主要的就是提供两种功能:查找界面元素,实现模拟点击。这也是我们实现自动抢红包软件的关键。当View、ViewGroup、TextView等控件这些状态变化时控件会回调系统API,API系统然后对这些对象的数据进行组装,为了数据的安全性,系统会重新创建一些对象(AccessibilityEvent、AccessibilityNodeInfo)来间接保存这些数据,然后通过跨进程将这些数据返回给对应的Service中。

使用范围

辅助功能不可能直接操作外部对象,辅助功能只能在本进程调用指定系统方法,由系统再分发给指定外部对象,辅助功能做的事基本和用户能做的差不多。

注意事项

AccessibilityEvent、AccessibilityNodeInfo里面的所有set方法均无用(这些方法是系统调用把数据塞进去用的),我们能做的只有:get、is、find等获取数据的方法,以及极少的操作performAction,dispatchGesture等。

使用步骤

1. 配置自己的辅助功能服务

对于辅助功能类的配置有两种方式:

(1)在onServiceConnected() 中配置(不推荐,部分属性可能无法设置)

/**
     * 当系统连接上你的服务时被调用
     */
    @Override
    protected void onServiceConnected() {
        //设置监听的应用包名(微信和qq)
        AccessibilityServiceInfo info = getServiceInfo();
        info.packageNames = new String[]{WX_PKG, QQ_PKG};
        setServiceInfo(info);
        super.onServiceConnected();
    }
           

(2)在XML中配置(推荐)

新建配置 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:accessibilityFlags="flagDefault"
    android:canRetrieveWindowContent="true"
    android:notificationTimeout="100"
    android:packageNames="com.tencent.mm"
    android:description="@string/accessibility_description"/>
           

这里来简单介绍一下这些配置:

accessibilityEventTypes 过滤事件类型

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

accessibilityFeedbackType 反馈类型

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

canRetrieveWindowContent 请求访问权限

packageNames 需要监听的包名列表

notificationTimeout 响应时间设置

2. 在AndroidManifest.xml中声明服务和配置
<service
            android:name=".PacketService"
            android:enabled="true"
            android:exported="true"
            android:label="@string/lab_name"
            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/service_config"/>
        </service>
           
3. 创建自己的辅助功能类

编写自己的辅助服务类,需要继承AccessibilityService类。

生命周期

  • onServiceConnected()

    连接成功

  • onInterrupt()

    服务中断

  • onUnbind(Intent intent)

    服务关闭

  • onAccessibilityEvent()

    接收事件,这个是辅助类的关键方法。通过这个函数可以接收系统发送来的AccessibilityEvent,接收来的AccessibilityEvent是经过过滤的,过滤是在配置工作时设置的。下面说一下配置自己的辅助服务。

新建PacketService.java类

/**
     * 必须重写的方法:此方法用了接受系统发来的event。在你注册的event发生是被调用。在整个生命周期会被调用多次。
     */
    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        //得到对应的事件类型,这里有很多很多种的事件类型,具体可以自行翻阅AccessibilityEvent类中的定义。
        int eventType = event.getEventType();
        LogUtil.d("eventType ===> " + eventType);
        switch (eventType) {
            // 通知栏事件
            case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:
                //获取通知栏消息的文字
                List<CharSequence> texts = event.getText();
                if (!texts.isEmpty()) {
                    for (CharSequence text : texts) {
                        String content = text.toString();
                        LogUtil.d("notification content ===> " + content);
                        // 监听到微信红包的notification,打开通知
                        if (content.contains("[微信红包]")) {
                            //点击打开通知栏消息
                            openNotification(event);
                        }
                    }
                }
                break;
            //窗口状态改变
            case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
                //获取到窗口的类名
                String className = event.getClassName().toString();
                LogUtil.i("window state = " + className);
                if (className.equals(WX_GET)) {
                    // 微信聊天界面,领取微信红包
                    getWXPacket(event);
                } else if (className.equals(WX_OPEN_OLD)) {
                    //微信红包弹框界面,打开微信红包
                    openPacket(event);
                } else if (className.equals(WX_OPEN_NEW_YEAR)) {
                //拜年红包
                    openNewYearPacket(event);
                } else if (className.equals(WX_RESULT)) {
                    //进入红包结果页,获取抢到的红包金额
                    getNumber(event);
                }
                break;
    }
           

模拟点击打开通知消息

/**
     * 模拟点击打开通知消息
     */
    private void openNotification(AccessibilityEvent event) {
        if (event.getParcelableData() != null
                && event.getParcelableData() instanceof Notification) {
            Notification notification = (Notification) event.getParcelableData();
            //获取通知栏的intent
            PendingIntent pendingIntent = notification.contentIntent;
            try {
                //根据intent跳转
                pendingIntent.send();
            } catch (PendingIntent.CanceledException e) {
                e.printStackTrace();
            }
        }
    }
           

如何在界面中找到相应的控件节点进行模拟点击呢?

/**
     * 找到打开的按钮,模拟点击打开微信红包
     */
    private void openPacket(AccessibilityEvent event) {
        //根据打开按钮的id找出来,模拟点击
        AccessibilityNodeInfo info = findFirstViewById("com.tencent.mm:id/cyf");
        if (info != null) {
            boolean isSuccess = clickView(info);
            info.recycle();
        } 
    }
/**
     * 点击该控件
     *
     * @return true表示点击成功
     */
    public static boolean clickView(AccessibilityNodeInfo nodeInfo) {
        if (nodeInfo != null) {
            if (nodeInfo.isClickable()) {
                nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);
                return true;
            } else {
                AccessibilityNodeInfo parent = nodeInfo.getParent();
                if (parent != null) {
                    boolean b = clickView(parent);
                    parent.recycle();
                    if (b) return true;
                }
            }
        }
        return false;
    }
           

几个实用的API:

  • getRootInActiveWindow() 获取当前屏幕的布局
  • findAccessibilityNodeInfosByViewId() 根据id找到相应的view
  • findAccessibilityNodeInfosByViewId() 根据id找到相应的view
  • findAccessibilityNodeInfosByViewId() 根据id找到相应的view

如何找到相应的组件的id?

这里就需要用到Android studio自带的一个神器:uiautomatorviewer.bat

打开uiautomatorviewer,连接手机,即可查看当前手机界面的布局。

完整的服务类代码:

/**
 * 通过利用AccessibilityService辅助服务,监测屏幕内容,如监听状态栏的信息,屏幕跳转等,以此来实现自动拆红包的功能
 */
public class PacketService extends AccessibilityService {

    //微信包名
    private final static String WX_PKG = "com.tencent.mm";
    //微信聊天界面
    private final static String WX_GET = "com.tencent.mm.ui.LauncherUI";
    //微信弹出红包界面
    private final static String WX_OPEN_OLD = "com.tencent.mm.plugin.luckymoney.ui" +
            ".LuckyMoneyNotHookReceiveUI";
    //新年红包打开弹框
    private final static String WX_OPEN_NEW_YEAR = "com.tencent.mm.plugin.luckymoney.ui" +
            ".LuckyMoneyNewYearReceiveUI";
    //红包结果页
    private final static String WX_RESULT = "com.tencent.mm.plugin.luckymoney.ui" +
            ".LuckyMoneyDetailUI";
            
    private List<AccessibilityNodeInfo> list;

    /**
     * 辅助功能是否启动
     */
    public static boolean isStart = false;
    private boolean isAny;

    /**
     * 必须重写的方法:此方法用了接受系统发来的event。在你注册的event发生是被调用。在整个生命周期会被调用多次。
     */
    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        //得到对应的事件类型,这里有很多很多种的事件类型,具体可以自行翻阅AccessibilityEvent类中的定义。
        int eventType = event.getEventType();
        LogUtil.d("eventType ===> " + eventType);
        switch (eventType) {
            // 通知栏事件
            case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:
                //获取通知栏消息的文字
                List<CharSequence> texts = event.getText();
                if (!texts.isEmpty()) {
                    for (CharSequence text : texts) {
                        String content = text.toString();
                        LogUtil.d("notification content ===> " + content);
                        // 监听到微信QQ红包的notification,打开通知
                        if (content.contains("[微信红包]")) {
                            //解开屏幕锁
                            if (isScreenLocked()) {
                                unlockScreen();
                            }
                            //点击打开通知栏消息
                            openNotification(event);
                        }
                    }
                }
                break;
            //窗口状态改变
            case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
                //获取到窗口的类名
                String className = event.getClassName().toString();
                LogUtil.i("window state = " + className);
                if (className.equals(WX_GET)) {
                    // 微信聊天界面,领取微信红包
                    getWXPacket(event);
                } else if (className.equals(WX_OPEN_OLD)) {
                    //微信红包弹框界面,打开微信红包
                    openPacket(event);
                } else if (className.equals(WX_OPEN_NEW_YEAR)) {
                    openNewYearPacket(event);
                } else if (className.equals(WX_RESULT)) {
                    //进入红包结果页
                    getNumber(event);
                }
                break;
            //监听界面文字改变(太耗资源,列表页会出现卡顿)
//            case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED:
//                LogUtil.i("content = " + event);
//                if (event.getPackageName().equals(WX_PKG)) {
//                    mWeiXinClicked = false;
//                    // 领取微信红包
//                    getWXPacket();
//                }
//                break;
            default:
                break;
        }
    }

    /**
     * 必须重写的方法:系统要中断此service返回的响应时会调用。在整个生命周期会被调用多次。
     */
    @Override
    public void onInterrupt() {
        LogUtil.d("红包功能被迫中断");
        isStart = false;
    }

    /**
     * 当系统连接上你的服务时被调用
     */
    @Override
    protected void onServiceConnected() {
        super.onServiceConnected();
        LogUtil.d("红包功能检测中....");
        isStart = true;
        if (isScreenLocked()) {
            unlockScreen();
        }
    }


    /**
     * 在系统要关闭此service时调用。
     */
    @Override
    public boolean onUnbind(Intent intent) {
        LogUtil.d("红包功能已断开");
        isStart = false;
        return super.onUnbind(intent);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        LogUtil.d("红包功能已关闭");
        isStart = false;
    }

    /**
     * 模拟点击打开通知消息
     */
    private void openNotification(AccessibilityEvent event) {
        if (event.getParcelableData() != null
                && event.getParcelableData() instanceof Notification) {
            Notification notification = (Notification) event.getParcelableData();
            //获取通知栏的intent
            PendingIntent pendingIntent = notification.contentIntent;
            try {
                //根据intent跳转
                pendingIntent.send();
            } catch (PendingIntent.CanceledException e) {
                e.printStackTrace();
            }
        }
    }


    /**
     * 拿到可领取的微信红包的节点,点击打开弹框
     */
    private void getWXPacket(AccessibilityEvent event) {
        if (mWeiXinClicked) {
            return;
        }
        LogUtil.d("-->进入聊天界面:");
        //找到所有红包对应的id列表
        List<AccessibilityNodeInfo> list = findViewById("com.tencent.mm:id/aou");
        if (!isEmptyArray(list)) {
            LogUtil.d("-->有红包个数:" + list.size());
            //过滤有多少个待领取的红包
            List<Integer> listNum = new ArrayList<>();
            for (int i = 0; i < list.size(); i++) {
                AccessibilityNodeInfo info = list.get(i);
                List<AccessibilityNodeInfo> listOld = info.findAccessibilityNodeInfosByViewId
                        ("com.tencent.mm:id/aq6");
                List<AccessibilityNodeInfo> listNew = info.findAccessibilityNodeInfosByViewId
                        ("com.tencent.mm:id/aqb");
                if (isEmptyArray(listOld) && isEmptyArray(listNew)) {
                    //如果有,证明是普通红包
                    List<AccessibilityNodeInfo> list3 = info.findAccessibilityNodeInfosByViewId
                            ("com.tencent.mm:id/aq3");
                    if (!isEmptyArray(list3)) {
                        //如果是普通红包,并且有这个的话才是别人发过来的
                        List<AccessibilityNodeInfo> list5 = info.findAccessibilityNodeInfosByViewId
                                ("com.tencent.mm:id/aq5");
                        if (!isEmptyArray(list5)) {
                            listNum.add(i);
                            list5.get(0).recycle();
                        }
                        list3.get(0).recycle();
                    } else {
                        listNum.add(i);
                    }
                }
                if (!isEmptyArray(listOld)) {
                    listOld.get(0).recycle();
                }
                if (!isEmptyArray(listNew)) {
                    listNew.get(0).recycle();
                }
                //如果不是第一个可领取的,都回收
                if (listNum.size() == 0 || i != listNum.get(0)) {
                    info.recycle();
                }
            }
            if (listNum.size() > 0) {
                LogUtil.d("-->待领取红包数:" + listNum.size());
                AccessibilityNodeInfo info = list.get(listNum.get(0));
                if (info.isClickable()) {
                    isOpen = false;
                    LogUtil.d("-->点击打开普通红包弹框:");
                    clickView(info);
                } else {
                    List<AccessibilityNodeInfo> listClick = info.getParent()
                            .findAccessibilityNodeInfosByViewId("com.tencent.mm:id/aqc");
                    if (!isEmptyArray(listClick)) {
                        LogUtil.d("-->点击打开拜年红包弹框:");
                        clickView(listClick.get(0));
                    }
                }
                isAny = listNum.size() > 1;
                info.recycle();
            }
        }

    }


    /**
     * 找到打开的按钮,模拟点击打开微信红包
     */
    private void openPacket(AccessibilityEvent event) {
        //根据打开按钮的id找出来,模拟点击
        AccessibilityNodeInfo info = findFirstViewById("com.tencent.mm:id/cyf");
        if (info != null) {
            boolean isSuccess = clickView(info);
            info.recycle();
        }
    }

    /**
     * 点击拜年红包,弹框自动领取
     */
    private void openNewYearPacket(AccessibilityEvent event) {
        if (isAny) {
            isAny = false;
            performBackClick();
        }
    }

    /**
     * 获取抢到的红包价格
     */
    private void getNumber(AccessibilityEvent event) {
        AccessibilityNodeInfo info = findFirstViewById("com.tencent.mm:id/cqv");
        if (info != null) {
            String number = info.getText().toString();
           //获取到抢到的金额
        }
        if (isAny) {
            isAny = false;
            performBackClick();
        }
    }
    
   //工具方法/

    /**
     * 系统是否在锁屏状态
     */
    private boolean isScreenLocked() {
        PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
        boolean isScreenOn = pm.isScreenOn();//如果为true,则表示屏幕“亮”了,否则屏幕“暗”了。
        return !isScreenOn;
    }

    private void unlockScreen() {

        PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
        PowerManager.WakeLock wakeLock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK
                | PowerManager.ACQUIRE_CAUSES_WAKEUP
                | PowerManager.ON_AFTER_RELEASE, "MyWakeLock");

        wakeLock.acquire();

        KeyguardManager keyguardManager = (KeyguardManager) getSystemService(Context
                .KEYGUARD_SERVICE);
        final KeyguardManager.KeyguardLock keyguardLock = keyguardManager.newKeyguardLock
                ("MyKeyguardLock");
        keyguardLock.disableKeyguard();
    }
    
    /**
     * 判断列表是否为空
     */
    private boolean isEmptyArray(List list) {
        return list == null || list.size() == 0;
    }

    /**
     * 根据getRootInActiveWindow查找当前id的控件集合(类似listview这种一个页面重复的id很多)
     *
     * @param idfullName id全称:com.android.xxx:id/tv_main
     */
    @Nullable
    public List<AccessibilityNodeInfo> findViewById(String idfullName, AccessibilityNodeInfo
            rootInfo) {
        try {
            if (rootInfo == null) {
                rootInfo = getRootInActiveWindow();
            }
            if (rootInfo == null) return null;
            List<AccessibilityNodeInfo> list = rootInfo.findAccessibilityNodeInfosByViewId
                    (idfullName);
            rootInfo.recycle();
            return list;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    public List<AccessibilityNodeInfo> findViewById(String idfullName) {
        return findViewById(idfullName, null);
    }

    /**
     * 根据getRootInActiveWindow查找当前id的控件集合(类似listview这种一个页面重复的id很多,只取第一个)
     *
     * @param idfullName id全称:com.android.xxx:id/tv_main
     */
    @Nullable
    public AccessibilityNodeInfo findFirstViewById(String idfullName, AccessibilityNodeInfo
            rootInfo) {
        List<AccessibilityNodeInfo> list = findViewById(idfullName, rootInfo);
        return isEmptyArray(list) ? null : list.get(0);
    }

    public AccessibilityNodeInfo findFirstViewById(String idfullName) {
        return findFirstViewById(idfullName, null);
    }


    /**
     * 点击该控件
     *
     * @return true表示点击成功
     */
    public static boolean clickView(AccessibilityNodeInfo nodeInfo) {
        if (nodeInfo != null) {
            if (nodeInfo.isClickable()) {
                nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);
                return true;
            } else {
                AccessibilityNodeInfo parent = nodeInfo.getParent();
                if (parent != null) {
                    boolean b = clickView(parent);
                    parent.recycle();
                    if (b) return true;
                }
            }
        }
        return false;
    }

    /**
     * 根据getRootInActiveWindow查找包含当前text的控件
     *
     * @param containsText 只要内容包含就会找到(应该是根据drawText找的)
     */
    @Nullable
    public List<AccessibilityNodeInfo> findViewByContainsText(@NonNull String containsText) {
        AccessibilityNodeInfo info = getRootInActiveWindow();
        if (info == null) return null;
        List<AccessibilityNodeInfo> list = info.findAccessibilityNodeInfosByText(containsText);
        info.recycle();
        return list;
    }

    /**
     * 根据getRootInActiveWindow查找和当前text相等的控件
     *
     * @param equalsText 需要找的text
     */
    @Nullable
    public List<AccessibilityNodeInfo> findViewByEqualsText(@NonNull String equalsText) {
        List<AccessibilityNodeInfo> listOld = findViewByContainsText(equalsText);
        LogUtil.d("-->获取包含领取红包的TextView:" + listOld.size());
        if (isEmptyArray(listOld)) {
            return null;
        }
        LogUtil.d("-->获取包含领取红包的TextView:" + listOld.size());
        ArrayList<AccessibilityNodeInfo> listNew = new ArrayList<>();
        for (AccessibilityNodeInfo ani : listOld) {
            LogUtil.d("-->包含领取红包字样的view的文字:" + ani.getText().toString());
            if (ani.getText() != null && equalsText.equals(ani.getText().toString())) {
                listNew.add(ani);
            } else {
                ani.recycle();
            }
        }
        return listNew;
    }

    /**
     * Check当前辅助服务是否启用
     *
     * @param serviceName serviceName
     * @return 是否启用
     */
    private boolean checkAccessibilityEnabled(Context context, String serviceName) {
        AccessibilityManager mAccessibilityManager = (AccessibilityManager) context
                .getSystemService(Context.ACCESSIBILITY_SERVICE);
        List<AccessibilityServiceInfo> accessibilityServices =
                mAccessibilityManager.getEnabledAccessibilityServiceList(AccessibilityServiceInfo
                        .FEEDBACK_GENERIC);
        for (AccessibilityServiceInfo info : accessibilityServices) {
            if (info.getId().equals(serviceName)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 前往开启辅助服务界面
     */
    public void goAccess(Context context) {
        Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(intent);
    }

    /**
     * 模拟点击事件
     *
     * @param nodeInfo nodeInfo
     */
    public void performViewClick(AccessibilityNodeInfo nodeInfo) {
        if (nodeInfo == null) {
            return;
        }
        while (nodeInfo != null) {
            if (nodeInfo.isClickable()) {
                nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);
                break;
            }
            nodeInfo = nodeInfo.getParent();
        }
    }

    /**
     * 模拟返回操作
     */
    public void performBackClick() {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        performGlobalAction(GLOBAL_ACTION_BACK);
    }
}