Android辅助功能实现自动抢红包(附源码)
作者:卜俊文
一、描述
最近看到同事有用抢红包的软件,就想看看抢红包的具体实现是如何的,所以了解了一下,有用辅助功能实现的,所以在下面的示例中会展示一个抢红包的小Demo,附带源码抢红包源码。
二、效果图
在桌面收到红包进行抢
在聊天页面收到口令红包
三、AccessibilityService使用
创建辅助服务类,继承AccessibilityService,实现两个接口,接收系统的事件
public class MyService extends AccessibilityService { @Override public void onAccessibilityEvent(AccessibilityEvent event) { } @Override public void onInterrupt() { } }
辅助服务的配置文件,配置事件,在 res/xml下创建accessibility_service_info.xml
//具体属性的说明在第5点有说明 <?xml version="1.0" encoding="utf-8"?> <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:description="@string/accessibility_service_description" android:accessibilityEventTypes="typeAllMask" android:accessibilityFeedbackType="feedbackGeneric" android:notificationTimeout="100" android:accessibilityFlags="flagDefault" android:canRetrieveWindowContent="true" android:packageNames="top.cokernut.sample" android:settingsActivity="com.example.android.accessibility.ServiceSettingsActivity" />
注册Service辅助服务,并且为Service附加上第二步创建的xml,看清除下面的一些属性,必须要加,如果有的没加的话是没效果的
<service android:name=".MyService" 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/accessibility_service_info" /> </service>
4 清单文件中添加权限
<uses-permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE" />
辅助服务配置文件xml属性说明:
//是否可以检索整个层级下的内容 android:canRetrieveWindowContent="true"级下的信息 //事件通知触发点,比如窗口打开,滑动,焦点变化,长按等。 android:accessibilityEventTypes="typeAllMask" #TYPES_ALL_MASK:所有类型 #TYPE_VIEW_CLICKED :单击 #TYPE_VIEW_LONG_CLICKED :长按 #TYPE_VIEW_SELECTED :选中 #TYPE_VIEW_FOCUSED :获取焦点 #TYPE_VIEW_TEXT_CHANGED :文字改变 #TYPE_WINDOW_STATE_CHANGED :窗口状态改变 //表示反馈方式,比如是语音播放,还是震动 android:accessibilityFeedbackType="feedbackGeneric" //接受事件的时间间隔,通常将其设置为100即可. android:notificationTimeout="100" //表示该服务是用来单独监听哪个应用的产生的事件,其他的都会过滤,如果不填就是对所有的应用进行监听,填入包名即可。 android:packageNames="top.cokernut.sample" //在代码中我们就可以通过node节点来getViewIdResourceName()获取对应的节点的id android:accessibilityFlags="flagDefault"
提供一个AccessibilityService的基类,集成了一些常用方法:
public class BaseAccessibilityService extends AccessibilityService { private AccessibilityManager mAccessibilityManager; private Context mContext; private static BaseAccessibilityService mInstance; public void init(Context context) { mContext = context.getApplicationContext(); mAccessibilityManager = (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); } public static BaseAccessibilityService getInstance() { if (mInstance == null) { mInstance = new BaseAccessibilityService(); } return mInstance; } /** * Check当前辅助服务是否启用 * * @param serviceName serviceName * @return 是否启用 */ private boolean checkAccessibilityEnabled(String serviceName) { List<AccessibilityServiceInfo> accessibilityServices = mAccessibilityManager.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_GENERIC); for (AccessibilityServiceInfo info : accessibilityServices) { if (info.getId().equals(serviceName)) { return true; } } return false; } /** * 前往开启辅助服务界面 */ public void goAccess() { Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); mContext.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); } /** * 模拟下滑操作 */ public void performScrollBackward() { try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } performGlobalAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); } /** * 模拟上滑操作 */ public void performScrollForward() { try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } performGlobalAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); } /** * 查找对应文本的View * * @param text text * @return View */ public AccessibilityNodeInfo findViewByText(String text) { return findViewByText(text, false); } /** * 查找对应文本的View * * @param text text * @param clickable 该View是否可以点击 * @return View */ public AccessibilityNodeInfo findViewByText(String text, boolean clickable) { AccessibilityNodeInfo accessibilityNodeInfo = getRootInActiveWindow(); if (accessibilityNodeInfo == null) { return null; } List<AccessibilityNodeInfo> nodeInfoList = accessibilityNodeInfo.findAccessibilityNodeInfosByText(text); if (nodeInfoList != null && !nodeInfoList.isEmpty()) { for (AccessibilityNodeInfo nodeInfo : nodeInfoList) { if (nodeInfo != null && (nodeInfo.isClickable() == clickable)) { return nodeInfo; } } } return null; } /** * 查找对应ID的View * * @param id id * @return View */ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) public AccessibilityNodeInfo findViewByID(String id) { AccessibilityNodeInfo accessibilityNodeInfo = getRootInActiveWindow(); if (accessibilityNodeInfo == null) { return null; } List<AccessibilityNodeInfo> nodeInfoList = accessibilityNodeInfo.findAccessibilityNodeInfosByViewId(id); if (nodeInfoList != null && !nodeInfoList.isEmpty()) { for (AccessibilityNodeInfo nodeInfo : nodeInfoList) { if (nodeInfo != null) { return nodeInfo; } } } return null; } public void clickTextViewByText(String text) { AccessibilityNodeInfo accessibilityNodeInfo = getRootInActiveWindow(); if (accessibilityNodeInfo == null) { return; } List<AccessibilityNodeInfo> nodeInfoList = accessibilityNodeInfo.findAccessibilityNodeInfosByText(text); if (nodeInfoList != null && !nodeInfoList.isEmpty()) { for (AccessibilityNodeInfo nodeInfo : nodeInfoList) { if (nodeInfo != null) { performViewClick(nodeInfo); break; } } } } @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) public void clickTextViewByID(String id) { AccessibilityNodeInfo accessibilityNodeInfo = getRootInActiveWindow(); if (accessibilityNodeInfo == null) { return; } List<AccessibilityNodeInfo> nodeInfoList = accessibilityNodeInfo.findAccessibilityNodeInfosByViewId(id); if (nodeInfoList != null && !nodeInfoList.isEmpty()) { for (AccessibilityNodeInfo nodeInfo : nodeInfoList) { if (nodeInfo != null) { performViewClick(nodeInfo); break; } } } } /** * 模拟输入 * * @param nodeInfo nodeInfo * @param text text */ public void inputText(AccessibilityNodeInfo nodeInfo, String text) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { Bundle arguments = new Bundle(); arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text); nodeInfo.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); ClipData clip = ClipData.newPlainText("label", text); clipboard.setPrimaryClip(clip); nodeInfo.performAction(AccessibilityNodeInfo.ACTION_FOCUS); nodeInfo.performAction(AccessibilityNodeInfo.ACTION_PASTE); } } @Override public void onAccessibilityEvent(AccessibilityEvent event) { } @Override public void onInterrupt() { } }
四、QQ抢红包
(一)抢红包流程:
- 通知栏收到QQ的消息,发现是QQ红包,模拟点击消息进入聊天页面
- 检索页面上的所有元素,发现有包含“点击拆开”的字眼,就模拟点击打开红包窗口
- 一两秒后执行Back操作,关闭红包窗口。
- 继续等待消息来到。
(二)实现功能:
- 锁屏抢红包(不可以有密码或者图案之类的锁屏)
- 口令红包,自动输入口令并且发送
- 抢完红包后,自动回复感谢语,可在红包设置里自行设置内容
- 其他的功能就没继续往下做了,知道方法,其他都可能慢慢研究出来。
(三)抢红包辅助功能类,注释都写好了,很好理解,类中有用到QQConstant类,在第四点贴出了代码
/** * 描述:QQ抢红包服务 * 作者:卜俊文 * 邮箱:344176791@qq.com * 日期:2017/11/6 上午9:25 */ public class EnvelopeService extends BaseAccessibilityService { //锁屏、解锁相关 private KeyguardManager.KeyguardLock kl; //唤醒屏幕相关 private PowerManager.WakeLock wl = null; private long delayTime = 0;//延迟抢的时间 /** * 描述:所有事件响应的时候会回调 * 作者:卜俊文 * 邮箱:344176791@qq.com * 日期:2017/11/6 上午9:26 */ @Override public void onAccessibilityEvent(AccessibilityEvent event) { //验证抢红包的开关 if (!invalidEnable()) { return; } //事件类型 int eventType = event.getEventType(); //获取包名 CharSequence packageName = event.getPackageName(); if (TextUtils.isEmpty(packageName)) { return; } switch (eventType) { //状态栏变化 case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED: if (QQConstant.QQ_PACKAGE_NAME.equals(packageName)) { //处理状态栏上QQ的消息,如果是红包就跳转过去 progressQQStatusBar(event); } break; //窗口切换的时候回调 case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: if (QQConstant.QQ_PACKAGE_NAME.equals(packageName)) { //处理正在QQ聊天窗口页面,有其他群或者人有新的红包提醒,跳转过去。 progressNewMessage(event); //处理聊天页面的红包 progressQQChat(event); } break; } } /** * 描述:处理新消息 * 作者:卜俊文 * 邮箱:344176791@qq.com * 日期:2017/11/3 下午11:21 */ private void progressNewMessage(AccessibilityEvent event) { if (event == null) { return; } AccessibilityNodeInfo source = event.getSource(); if (source == null) { return; } //根据event的source里的text,来判断这个消息是否包含[QQ红包]的字眼,有的话就跳转过去 CharSequence text = source.getText(); if (!TextUtils.isEmpty(text) && text.toString().contains(QQConstant.QQ_ENVELOPE_KEYWORD)) { performViewClick(source); } } /** * 描述:验证抢红包是否开启 * 作者:卜俊文 * 邮箱:344176791@qq.com * 日期:2017/11/3 下午4:57 */ private boolean invalidEnable() { return SettingConfig.getInstance().getReEnable(); } /** * 描述:处理QQ状态栏 * 作者:卜俊文 * 邮箱:344176791@qq.com * 日期:2017/11/1 下午1:49 */ public void progressQQStatusBar(AccessibilityEvent event) { List<CharSequence> text = event.getText(); //开始检索界面上是否有QQ红包的文本,并且他是通知栏的信息 if (text != null && text.size() > 0) { for (CharSequence charSequence : text) { if (charSequence.toString().contains(QQConstant.QQ_ENVELOPE_KEYWORD)) { //说明存在红包弹窗,马上进去 if (event.getParcelableData() != null && event.getParcelableData() instanceof Notification) { Notification notification = (Notification) event.getParcelableData(); if (notification == null) { return; } PendingIntent pendingIntent = notification.contentIntent; if (pendingIntent == null) { return; } try { //要跳转之前,先进行解锁屏幕,然后再跳转,有可能你现在屏幕是锁屏状态,先进行解锁,然后打开页面,有密码的可能就不行了 wakeUpAndUnlock(MyApp.context); //跳转 pendingIntent.send(); } catch (PendingIntent.CanceledException e) { e.printStackTrace(); } } } } } } /** * 描述:处理QQ聊天红包 * 作者:卜俊文 * 邮箱:344176791@qq.com * 日期:2017/11/1 下午1:56 */ public void progressQQChat(AccessibilityEvent event) { if (TextUtils.isEmpty(event.getClassName())) { return; //如果当前页面是聊天页面或者当前的描述信息是"返回消息界面",就肯定是对话页面 } //验证当前事件是否符合查询页面上的红包 if (!invalidEnvelopeUi(event)) { return; } //延迟点击红包,防止被检测到开了抢红包,不过感觉还是感觉会被检测到,应该有的效果吧... try { Thread.sleep(delayTime); } catch (InterruptedException e) { e.printStackTrace(); } //普通红包,检索点击拆开的字眼。 List<AccessibilityNodeInfo> envelope = findViewListByText(QQConstant.QQ_CLICK_TAKE_APART, false); //处理普通红包 progressNormal(envelope); //口令红包,检索口令红包的字眼。 List<AccessibilityNodeInfo> passwordList = findViewListByText(QQConstant.QQ_CLICK_PASSWORD_DIALOG, false); //处理口令红包 progressPassword(passwordList); } /** * 描述:验证是否现在是在聊天页面,可以进行抢红包处理 * 作者:卜俊文 * 邮箱:344176791@qq.com * 日期:2017/11/3 上午11:52 * * @param event */ public boolean invalidEnvelopeUi(AccessibilityEvent event) { //判断类名是否是聊天页面 if (!QQConstant.QQ_IM_CHAT_ACTIVITY.equals(event.getClassName().toString())) { return true; } //判断页面中的元素是否有点击拆开的文本,有就返回可以进行查询了 int recordCount = event.getRecordCount(); if (recordCount > 0) { for (int i = 0; i < recordCount; i++) { AccessibilityRecord record = event.getRecord(i); if (record == null) { break; } List<CharSequence> text = record.getText(); if (text != null && text.size() > 0 && text.contains(QQConstant.QQ_CLICK_TAKE_APART)) { //如果文本中有点击拆开的字眼,就返回可以进行查询了 return true; } } } return false; } /** * 回到系统桌面 */ private void back2Home(int time) { try { Thread.sleep(time); } catch (InterruptedException e) { e.printStackTrace(); } Intent home = new Intent(Intent.ACTION_MAIN); home.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); home.addCategory(Intent.CATEGORY_HOME); startActivity(home); } /** * 描述:处理普通红包 * 作者:卜俊文 * 邮箱:344176791@qq.com * 日期:2017/11/1 下午5:02 */ public void progressNormal(List<AccessibilityNodeInfo> passwordList) { if (passwordList != null && passwordList.size() > 0) { for (AccessibilityNodeInfo accessibilityNodeInfo : passwordList) { if (accessibilityNodeInfo != null && !TextUtils.isEmpty(accessibilityNodeInfo.getText()) && QQConstant.QQ_CLICK_TAKE_APART.equals(accessibilityNodeInfo.getText().toString())) { //点击拆开红包 performViewClick(accessibilityNodeInfo); //回复感谢信息,根据配置文件中配置的回复信息回复 String reReplyMessage = SettingConfig.getInstance().getReReplyMessage(); if (!TextUtils.isEmpty(reReplyMessage)) { replyMessage(reReplyMessage); } } } //最后延迟事件触发返回事件,关闭红包页面 performBackClick(1200); } } /** * 描述:处理口令红包 * 作者:卜俊文 * 邮箱:344176791@qq.com * 日期:2017/11/1 下午4:58 * * @param passwordList */ public void progressPassword(List<AccessibilityNodeInfo> passwordList) { if (passwordList != null && passwordList.size() > 0) { for (AccessibilityNodeInfo accessibilityNodeInfo : passwordList) { if (accessibilityNodeInfo != null && !TextUtils.isEmpty(accessibilityNodeInfo.getText()) && QQConstant.QQ_CLICK_PASSWORD_DIALOG.equals(accessibilityNodeInfo.getText().toString())) { //如果口令红包存在,就在输入框中进行输入,然后发送 AccessibilityNodeInfo parent = accessibilityNodeInfo.getParent(); if (parent != null) { CharSequence contentDescription = parent.getContentDescription(); if (!TextUtils.isEmpty(contentDescription)) { //1. 获取口令 String key = (String) contentDescription; if (key.contains(",") && key.contains("口令:")) { key = key.substring(key.indexOf("口令:") + 3, key.lastIndexOf(",")); } Log.e("口令", key); //2. 填写口令到编辑框上然后进行发送 replyMessage(key); //返回,关闭红包页面 performBackClick(1200); } } } } } } /** * 唤醒屏幕并解锁权限 * <uses-permission android:name="android.permission.WAKE_LOCK" /> */ @SuppressLint("Wakelock") @SuppressWarnings("deprecation") public void wakeUpAndUnlock(Context context) { // 点亮屏幕 wl.acquire(); // 释放 wl.release(); // 解锁 kl.disableKeyguard(); } /** * 描述:回复消息,无延迟 * 作者:卜俊文 * 邮箱:344176791@qq.com * 日期:2017/11/3 下午5:10 */ public void replyMessage(String key) { replyMessage(key, 0); } /** * 描述:回复消息 * 作者:卜俊文 * 邮箱:344176791@qq.com * 日期:2017/11/3 下午5:10 */ public void replyMessage(String key, int time) { //延迟 if (time > 0) { try { Thread.sleep(time); } catch (InterruptedException e) { e.printStackTrace(); } } //获取QQ聊天页面输入框 AccessibilityNodeInfo chat_edit = findViewByID(QQConstant.QQ_CHAT_MESSAGE_INPUT); if (chat_edit != null) { //把口令粘贴到输入框中 pastaText(chat_edit, MyApp.context, key); //获取QQ聊天页面发送消息按钮 AccessibilityNodeInfo sendMessage = findViewByID(QQConstant.QQ_CHAT_MESSAGE_SEND); //然后就按下发送按钮 if (sendMessage != null && Button.class.getName().equals(sendMessage.getClassName())) { performViewClick(sendMessage); } } } @Override public void onInterrupt() { } @Override protected void onServiceConnected() { super.onServiceConnected(); // 获取电源管理器对象 PowerManager pm = (PowerManager) MyApp.context .getSystemService(Context.POWER_SERVICE); // 获取PowerManager.WakeLock对象,后面的参数|表示同时传入两个值,最后的是调试用的Tag wl = pm.newWakeLock( PowerManager.ACQUIRE_CAUSES_WAKEUP | PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "bright"); KeyguardManager km = (KeyguardManager) MyApp.context.getSystemService(Context.KEYGUARD_SERVICE); kl = km.newKeyguardLock("unLock"); //初始化屏幕的监听 ScreenListener screenListener = new ScreenListener(MyApp.context); screenListener.begin(new ScreenListener.ScreenStateListener() { @Override public void onScreenOn() { Log.e("ScreenListener", "屏幕打开了"); } @Override public void onScreenOff() { //在屏幕关闭的时候,进行锁屏,不执行的话,锁屏就失效了,因为要实现锁屏状态下也可以进行抢红包。 Log.e("ScreenListener", "屏幕关闭了"); if (kl != null) { kl.disableKeyguard(); kl.reenableKeyguard(); } } @Override public void onUserPresent() { Log.e("ScreenListener", "解锁了"); } }); } @Override public void onDestroy() { super.onDestroy(); } }
(四)QQ辅助服务里有用到的常量
public class QQConstant { //QQ的应用包名 public static final String QQ_PACKAGE_NAME = "com.tencent.mobileqq"; //状态栏红包关键字 public static final String QQ_ENVELOPE_KEYWORD = "[QQ红包]"; //QQ聊天页面 public static final String QQ_IM_CHAT_ACTIVITY = "com.tencent.mobileqq.activity.SplashActivity"; //点击拆开 public static final String QQ_CLICK_TAKE_APART = "点击拆开"; //口令红包 public static final String QQ_CLICK_PASSWORD_DIALOG = "口令红包"; //聊天页面,输入框ID public static final String QQ_CHAT_MESSAGE_INPUT = "com.tencent.mobileqq:id/input"; //聊天页面,发送按钮 public static final String QQ_CHAT_MESSAGE_SEND = "com.tencent.mobileqq:id/fun_btn"; }
五、红包问题
用的时候偶尔会被QQ检测到用了红包插件,可能是因为抢的速度太快,导致数据不符合正常的点击时间,我有加入一个延迟时间,不知道有没有效果,如果有知道的也可以留言,谢谢。
在QQ的主页面上,收到消息的时候通知栏是不会通知的,所以这里不能进行解析通知栏跳转聊天页面,没有找到什么元素可以告诉我怎么进入红包的聊天页面,如果有知道的可以留言,谢谢。
这种辅助服务的方式抢红包,进入聊天页面后,他检索字段只会检索当前页面可视的元素,某些红包要是在聊天记录上面看不见的,需要滑动上去才可以触发解析红包,不过一般不会一次性10个红包都发出来吧,嘿嘿。
六、总结
学习制作了这个项目,也了解了辅助功能的使用,感觉这个还是可以做很多东西的,上面已经贴出了核心代码,要源码的可以点击。抢红包源码
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。