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); } }
/** * 打开通知栏消息 */ 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; // 只领最新的一个红包 } } }
/** * 拆红包 */ @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); }
版权声明:本文为博主原创文章,未经博主允许不得转载。