
第二步、声明权限和配置
这个服务需要注明一个权限:
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
<?xml version="1.0" encoding="utf-8"?> <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:accessibilityEventTypes="typeNotificationStateChanged|typeWindowStateChanged" android:accessibilityFeedbackType="feedbackGeneric" android:accessibilityFlags="flagDefault" android:canRetrieveWindowContent="true" android:description="@string/desc" android:notificationTimeout="100" android:packageNames="com.tencent.mm" />这里我们看到有很多选项,我们看一下常用的几个属性:
@Override protected void onServiceConnected() { AccessibilityServiceInfo info = getServiceInfo(); info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK; info.feedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN; info.notificationTimeout = 100; setServiceInfo(info); info.packageNames = new String[]{"xxx.xxx.xxx", "yyy.yyy.yyy","...."}; setServiceInfo(info); super.onServiceConnected(); }2、这里我们一般都会在这里写上我们需要监听的应用的包名,但是有时候我们需要监听多个应用,那么这时候我们该怎么办呢?@Override protected void onServiceConnected() { AccessibilityServiceInfo info = getServiceInfo(); //这里可以设置多个包名,监听多个应用 info.packageNames = new String[]{"xxx.xxx.xxx", "yyy.yyy.yyy","...."}; setServiceInfo(info); super.onServiceConnected(); }第二种:我们在onAccessibilityEvent事件监听的方法中做包名的过滤(这种方式最常用)@Override public void onAccessibilityEvent(AccessibilityEvent event) { String pkgName = event.getPackageName().toString(); if("xxx.xxx.xxx".equals(pkgName)){ }else if("yyy.yyy.yyy".equals(pkgName)){ }else if("....".equals(pkgName)){ } }第三步、在onAccessibilityEvent方法中监听指定的事件@Override public void onAccessibilityEvent(AccessibilityEvent event) { int eventType = event.getEventType(); switch (eventType) { case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED: //....... } }这个事件类型很多的,我们可以查看AccessibilityEvent类的源码:@Deprecated public static final int MAX_TEXT_LENGTH = 500; /** * Represents the event of clicking on a {@link android.view.View} like * {@link android.widget.Button}, {@link android.widget.CompoundButton}, etc. */ public static final int TYPE_VIEW_CLICKED = 0x00000001; /** * Represents the event of long clicking on a {@link android.view.View} like * {@link android.widget.Button}, {@link android.widget.CompoundButton}, etc. */ public static final int TYPE_VIEW_LONG_CLICKED = 0x00000002; /** * Represents the event of selecting an item usually in the context of an * {@link android.widget.AdapterView}. */ public static final int TYPE_VIEW_SELECTED = 0x00000004; /** * Represents the event of setting input focus of a {@link android.view.View}. */ public static final int TYPE_VIEW_FOCUSED = 0x00000008; /** * Represents the event of changing the text of an {@link android.widget.EditText}. */ public static final int TYPE_VIEW_TEXT_CHANGED = 0x00000010; /** * Represents the event of opening a {@link android.widget.PopupWindow}, * {@link android.view.Menu}, {@link android.app.Dialog}, etc. */ public static final int TYPE_WINDOW_STATE_CHANGED = 0x00000020; /** * Represents the event showing a {@link android.app.Notification}. */ public static final int TYPE_NOTIFICATION_STATE_CHANGED = 0x00000040; /** * Represents the event of a hover enter over a {@link android.view.View}. */ public static final int TYPE_VIEW_HOVER_ENTER = 0x00000080; /** * Represents the event of a hover exit over a {@link android.view.View}. */ public static final int TYPE_VIEW_HOVER_EXIT = 0x00000100; /** * Represents the event of starting a touch exploration gesture. */ public static final int TYPE_TOUCH_EXPLORATION_GESTURE_START = 0x00000200; /** * Represents the event of ending a touch exploration gesture. */ public static final int TYPE_TOUCH_EXPLORATION_GESTURE_END = 0x00000400; /** * Represents the event of changing the content of a window and more * specifically the sub-tree rooted at the event"s source. */ public static final int TYPE_WINDOW_CONTENT_CHANGED = 0x00000800; /** * Represents the event of scrolling a view. */ public static final int TYPE_VIEW_SCROLLED = 0x00001000; /** * Represents the event of changing the selection in an {@link android.widget.EditText}. */ public static final int TYPE_VIEW_TEXT_SELECTION_CHANGED = 0x00002000; /** * Represents the event of an application making an announcement. */ public static final int TYPE_ANNOUNCEMENT = 0x00004000; /** * Represents the event of gaining accessibility focus. */ public static final int TYPE_VIEW_ACCESSIBILITY_FOCUSED = 0x00008000; /** * Represents the event of clearing accessibility focus. */ public static final int TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED = 0x00010000; /** * Represents the event of traversing the text of a view at a given movement granularity. */ public static final int TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY = 0x00020000; /** * Represents the event of beginning gesture detection. */ public static final int TYPE_GESTURE_DETECTION_START = 0x00040000; /** * Represents the event of ending gesture detection. */ public static final int TYPE_GESTURE_DETECTION_END = 0x00080000; /** * Represents the event of the user starting to touch the screen. */ public static final int TYPE_TOUCH_INTERACTION_START = 0x00100000; /** * Represents the event of the user ending to touch the screen. */ public static final int TYPE_TOUCH_INTERACTION_END = 0x00200000; /** * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event: * The type of change is not defined. */ public static final int CONTENT_CHANGE_TYPE_UNDEFINED = 0x00000000; /** * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event: * A node in the subtree rooted at the source node was added or removed. */ public static final int CONTENT_CHANGE_TYPE_SUBTREE = 0x00000001; /** * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event: * The node"s text changed. */ public static final int CONTENT_CHANGE_TYPE_TEXT = 0x00000002; /** * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event: * The node"s content description changed. */ public static final int CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION = 0x00000004; 这里有很多事件,这些事件我们通过名字就可以看出来有很多我们可能都知道,比如当窗口发生变化的时候,当某个View被点击了,被滚动了等消息都是可以知道的。那么我们有了这些事件我们就可以做我们的事情了,因为我们知道事件触发了。
我们查找包含有:领取红包 的文本内容的节点View,然后模拟点击,进入第三步:
第三步、我们点击领取红包
"如下图:
这里我们在查找包含有:拆红包 的文本内容的节点View,然后模拟点击

下面我们来看一下代码中的具体实现:
package krelve.demo.rob; import java.util.List; import android.accessibilityservice.AccessibilityService; import android.accessibilityservice.AccessibilityServiceInfo; import android.annotation.SuppressLint; import android.app.Notification; import android.app.PendingIntent; import android.app.PendingIntent.CanceledException; import android.util.Log; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; public class RobMoney extends AccessibilityService { @Override public void onAccessibilityEvent(AccessibilityEvent event) { int eventType = event.getEventType(); switch (eventType) { //第一步:监听通知栏消息 case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED: List<CharSequence> texts = event.getText(); if (!texts.isEmpty()) { for (CharSequence text : texts) { String content = text.toString(); Log.i("demo", "text:"+content); if (content.contains("[微信红包]")) { //模拟打开通知栏消息 if (event.getParcelableData() != null && event.getParcelableData() instanceof Notification) { Notification notification = (Notification) event.getParcelableData(); PendingIntent pendingIntent = notification.contentIntent; try { pendingIntent.send(); } catch (CanceledException e) { e.printStackTrace(); } } } } } break; //第二步:监听是否进入微信红包消息界面 case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: String className = event.getClassName().toString(); if (className.equals("com.tencent.mm.ui.LauncherUI")) { //开始抢红包 getPacket(); } else if (className.equals("com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyReceiveUI")) { //开始打开红包 openPacket(); } break; } } /** * 查找到 */ @SuppressLint("NewApi") private void openPacket() { AccessibilityNodeInfo nodeInfo = getRootInActiveWindow(); if (nodeInfo != null) { List<AccessibilityNodeInfo> list = nodeInfo .findAccessibilityNodeInfosByText("抢红包"); for (AccessibilityNodeInfo n : list) { n.performAction(AccessibilityNodeInfo.ACTION_CLICK); } } } @SuppressLint("NewApi") private void getPacket() { AccessibilityNodeInfo rootNode = getRootInActiveWindow(); recycle(rootNode); } /** * 打印一个节点的结构 * @param info */ @SuppressLint("NewApi") public void recycle(AccessibilityNodeInfo info) { if (info.getChildCount() == 0) { if(info.getText() != null){ if("领取红包".equals(info.getText().toString())){ //这里有一个问题需要注意,就是需要找到一个可以点击的View Log.i("demo", "Click"+",isClick:"+info.isClickable()); info.performAction(AccessibilityNodeInfo.ACTION_CLICK); AccessibilityNodeInfo parent = info.getParent(); while(parent != null){ Log.i("demo", "parent isClick:"+parent.isClickable()); if(parent.isClickable()){ parent.performAction(AccessibilityNodeInfo.ACTION_CLICK); break; } parent = parent.getParent(); } } } } else { for (int i = 0; i < info.getChildCount(); i++) { if(info.getChild(i)!=null){ recycle(info.getChild(i)); } } } } @Override public void onInterrupt() { } }代码没什么好说的了,按照我们之前说的三个步骤来就可以了,但是这里需要注意点细节上的问题:if (content.contains("[微信红包]")) { //模拟打开通知栏消息 if (event.getParcelableData() != null && event.getParcelableData() instanceof Notification) { Notification notification = (Notification) event.getParcelableData(); PendingIntent pendingIntent = notification.contentIntent; try { pendingIntent.send(); } catch (CanceledException e) { e.printStackTrace(); } } }2、我们在模拟点击通知栏消息之后,还是需要监听:AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED 这个事件,这个事件我们以后会经常用到,这个事件就是在窗口发生改变的时候发出来的事件,很常用的,比如我们可以通过这个事件来监听TopActivity,然后得到包名,这也是一个实现应用锁的一个原理。if(info.getText() != null){ if("领取红包".equals(info.getText().toString())){ //这里有一个问题需要注意,就是需要找到一个可以点击的View Log.i("demo", "Click"+",isClick:"+info.isClickable()); info.performAction(AccessibilityNodeInfo.ACTION_CLICK); AccessibilityNodeInfo parent = info.getParent(); while(parent != null){ Log.i("demo", "parent isClick:"+parent.isClickable()); if(parent.isClickable()){ parent.performAction(AccessibilityNodeInfo.ACTION_CLICK); break; } parent = parent.getParent(); } } } 这里为什么这么做,其实原理很简单,因为我们不知道微信他的界面布局,也不知道他对哪个View进行了setOnClickListener。我们可以写一个例子,performAction方法只对调用了setOnClickListener方法的View模拟点击才有效,其实看View的源码也是可以看出来的.这里就不多解释了。所以我们就需要得到一个View节点之后,从下往上找,直到找到一个可以click的View为止。


这里我们可以看到View的详细布局,还有每个View的属性,还有很重要的信息resource-id,这个就是我们在xml中定义的id,这个id我们也可以使用前面说到的findAccessibilityNodeInfosByViewId("@id/xxx")来查找控件了
这个也算是学习了,学会使用DDMS来分析View结构。
四、延展
关于微信抢红包的原理解析上面已经做了分析了,但是要想做到极致,这里还有很多问题的,比如我们还需要过滤一些已经领取过的红包,这样的话效率也是很高的。这个都是算法精确的问题了,我想在这里说的是,我们不仅可以用辅助功能来实现抢红包,还可以实现很多功能,比如
1、静默安装
对于这两个要求,我们或许很难得到,那么现在如果有了辅助功能,我们就好做了:

我们可以监听系统的这个安装界面,然后得到安装节点View,然后模拟点击即可,卸载也是同样的原理
2、强制停止应用
我们知道Android中停止应用有很多方法,kill进程,stopService,但是这些方法,有一些应用它们都是有对策的,那么我们之前用到的强制停止的方法是获取root权限调用系统的forceStop的api来停止,但是前提还是有root。那么现在如果我们有了辅助功能的话,我们可以这么做:

我们可以监听系统的应用详情页面,然后找到:结束运行的节点View,然后模拟点击即可
当然上面我就说了两个简单的例子,还有很多辅助功能都是可以做的。他的好处就是不需要root权限。但是他也是需要用户授权的:

如果用户没有授权的话,那么所有的工作都没办法开始了,所以说这个方法也不是万能的。当然说句题外话:有了辅助功能的话,他的危险性比root之后的危险性更大,比如我们上面的抢红包插件,其实我们稍作修改,就可以获取微信通讯录信息,微信支付的密码。这些事都是可以做的,所以说,我们在作为用户的时候,进行授权的时候还是需要三思而后行。
五、总结
关于辅助功能,之前没有太多的接触,是在一次工作中用到了这个功能,就去学习了一下,作为自己的兴趣,就延展了学习了如何写一个微信抢红包的插件,同时可以考虑了使用辅助功能能够做我们之前需要root做的事情。当然辅助功能是google对于肢体上有障碍的人开发出来的一个功能,我们开发者或许使用这个功能,可以做一下产品的拓展功能,当然这些是google没有想到的事情,但是这个至少是我们开发者在以后的开发道路上的一个解决问题的一个办法和途径,谨记此功能!