Android 辅助服务实战-游戏点击器

背景:

前几年我一直在玩一款氪金养成类手游<<末日危机>>,每天都有任务需要完成,那时游戏里面还没有一键收菜,我去年(接手了公会里面不少弃坑号,越玩越是耗时间)就想做了一些辅助软件减轻我的负担,先是在PC做了个自动点击完成关卡战斗,后面觉得天天开虚拟机太麻烦,不如直接在手机上完成。

思路:

Android系统有提供一个无障碍功能:AccessbilityService,在Android6.0之前能访问第三方应用的当前布局和控件结点,现在Android手机系统等级动辄10以上,已经不能用这种直接获取控件的方式,要使用截图再进行图片识别目标位置,再进行坐标位置上的点击。

首先手机上需要开启辅助服务,该服务在手机截图API实现要求手机版本至少要Android10。具体代码如下

private fun isAccessibilitySettingsOn(mContext: Context): Boolean {var accessibilityEnabled = 0// TestService为对应的服务val service = packageName + "/" + AccessbilityServiceImp::class.java.canonicalNameLog.i(TAG, "service:$service")try {accessibilityEnabled = Settings.Secure.getInt(mContext.applicationContext.contentResolver,Settings.Secure.ACCESSIBILITY_ENABLED)Log.v(TAG, "accessibilityEnabled = $accessibilityEnabled")} catch (e: SettingNotFoundException) {Log.e(TAG, "Error finding setting, default accessibility to not found: " + e.message)}val mStringColonSplitter = SimpleStringSplitter(':')if (accessibilityEnabled == 1) {Log.v(TAG, "***ACCESSIBILITY IS ENABLED*** -----------------")val settingValue = Settings.Secure.getString(mContext.applicationContext.contentResolver,Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES)if (settingValue != null) {mStringColonSplitter.setString(settingValue)while (mStringColonSplitter.hasNext()) {val accessibilityService = mStringColonSplitter.next()Log.v(TAG,"-------------- > accessibilityService :: $accessibilityService $service")if (accessibilityService.equals(service, ignoreCase = true)) {Log.v(TAG,"We've found the correct setting - accessibility is switched on!")return true}}}} else {Log.v(TAG, "***ACCESSIBILITY IS DISABLED***")}return false
}

具体实现:

开启了辅助服务后,我们需要一个媒介和该服务进行交互,并且不能影响后续的截图效果,一种能在屏幕上进行收缩的工具栏。

我采用系统消息通知栏+广播来解决这个问题,它会和辅助服务进行交互:

object GameNotification {private var manager: NotificationManager? = nullprivate var remoteViews: RemoteViews? = nullvar notify: Notification? = nullfun createNotificaton(context: Context) {manager = context.getSystemService(AccessibilityService.NOTIFICATION_SERVICE) as NotificationManager// 设置通知栏的图片文字remoteViews = RemoteViews(context.packageName,R.layout.custom_notice)remoteViews!!.setImageViewResource(R.id.widget_play, R.drawable.ic_baseline_play_arrow_24)remoteViews!!.setImageViewResource(R.id.widget_stop, R.drawable.ic_baseline_stop_24)val builder = NotificationCompat.Builder(context)val intent = Intent(context, MainActivity::class.java)// 点击跳转到主界面val intent_go = PendingIntent.getActivity(context, 0, intent,PendingIntent.FLAG_UPDATE_CURRENT)remoteViews!!.setOnClickPendingIntent(R.id.notice, intent_go)// 设置开始按键val start = Intent()start.action = Command.ACTION_STARTval intent_start = PendingIntent.getBroadcast(context, 0, start,PendingIntent.FLAG_UPDATE_CURRENT)remoteViews!!.setOnClickPendingIntent(R.id.widget_play, intent_start)// 设置停止按键val stop = Intent()stop.action = Command.ACTION_STOPval intent_stop = PendingIntent.getBroadcast(context, 1, stop,PendingIntent.FLAG_UPDATE_CURRENT)remoteViews!!.setOnClickPendingIntent(R.id.widget_stop, intent_stop)builder.setSmallIcon(R.drawable.ic_launcher_background) // 设置顶部图标if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {val mChannel = NotificationChannel("123", "147", NotificationManager.IMPORTANCE_LOW)manager!!.createNotificationChannel(mChannel)notify = Notification.Builder(context).setChannelId("123")//                    .setContentTitle("5 new messages")//                    .setContentText("hahaha").setContent(remoteViews).setSmallIcon(R.mipmap.ic_launcher).build()} else {notify = builder.build()notify!!.contentView = remoteViews // 设置下拉图标notify!!.bigContentView = remoteViews // 防止显示不完全,需要添加apisupportnotify!!.flags = Notification.FLAG_ONGOING_EVENT}manager!!.notify(100, notify)}}

通过广播进行消息传输,告诉辅助服务可以执行或者停止当前选择的任务。

识别图片:

屏幕截图获取代码片段:

执行截图:

@RequiresApi(Build.VERSION_CODES.R)
fun startSnapShoot() {accessbilityServiceImp!!.mHandler.postDelayed({accessbilityServiceImp!!.takeScreenshot(Display.DEFAULT_DISPLAY,accessbilityServiceImp!!.mainExecutor,mCallBack)},1000)
}

接收截图数据:

 @RequiresApi(Build.VERSION_CODES.R)var mCallBack:AccessibilityService.TakeScreenshotCallback = object :AccessibilityService.TakeScreenshotCallback{override fun onSuccess(screenshotResult: AccessibilityService.ScreenshotResult) {val nodeInfo = accessbilityServiceImp?.rootInActiveWindowval result = nodeInfo?.let { accessbilityServiceImp?.findNodeInfosByText(it,"任务") }Log.e("AccessbilityServiceImp","onSuccess = "+screenshotResult+",result = "+result)var timespec = 500LaccessbilityServiceImp!!.timespec = 1000Lval hardwareBuffer = screenshotResult.hardwareBufferval colorSpace = screenshotResult.colorSpaceif (hardwareBuffer.width > 0 && hardwareBuffer.height > 0 && colorSpace != null) {val bitmap = Bitmap.wrapHardwareBuffer(hardwareBuffer, colorSpace)...}}}...

把获取的数据转换成bitmap,后面使用opencv进行图片识别。

竞技场挑战任务:

竞技场页面获取当前在场队伍的战力值:

 fun detectNumberRect(bitmap: Bitmap, list: List<Mat>): List<ComplyPlayInfo> {val bitmapNew: Bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true)var resultPlays :List<ComplyPlayInfo>val dectectedPlays = ArrayList<ComplyPlayInfo>()var myPlayInfo = ComplyPlayInfo(0,0)//opencv初始化if (!OpenCVLoader.initDebug()) {Log.d("--------opencv--------", "OpenCVLoader error")}//Mat変換val src = Mat(bitmapNew.height, bitmapNew.width, CvType.CV_8UC4)Utils.bitmapToMat(bitmapNew, src)//获取战力值所在的坐标位置集合val rects = detectPowerNumberRect(bitmap)rects.mapIndexed {index,it->val mRgb = Mat(src, it)val b = Bitmap.createBitmap(mRgb!!.cols(), mRgb.rows(),Bitmap.Config.ARGB_8888)Utils.matToBitmap(mRgb, b)val dst = Mat.zeros(Size(src.width().toDouble(), src.height().toDouble()), CvType.CV_8UC3)//绘制轮廓 (Yellow)var color = Scalar(255.0, 255.0, 0.0)//灰色化Imgproc.cvtColor(mRgb, mRgb, Imgproc.COLOR_RGB2GRAY)bitwise_not(mRgb, mRgb);//二値化Imgproc.threshold(mRgb, mRgb, 0.0, 250.0,Imgproc.THRESH_BINARY or Imgproc.THRESH_OTSU)Utils.matToBitmap(mRgb, b)//比特反转val hierarchy = Mat.zeros(Size(0.0, 0.0), CvType.CV_8UC1)// 輪郭抽出val contours: List<MatOfPoint> = ArrayList()Imgproc.findContours(mRgb,contours,hierarchy,Imgproc.RETR_TREE,Imgproc.CHAIN_APPROX_SIMPLE)Imgproc.drawContours(dst, contours, -1, color, 1)var box: Rect?var boxs = ArrayList<Rect>()for (i in contours.indices) {val ptmat: MatOfPoint = contours[i]// 轮廓的重心的绘制 (Red)color = Scalar(255.0, 0.0, 0.0)val ptmat2 = MatOfPoint2f(*ptmat.toArray())val bbox = Imgproc.minAreaRect(ptmat2)box = bbox.boundingRect()if (box.width < box.height && box.height < 50 && box.height > 25&& box.y < 10) {Imgproc.circle(dst, bbox.center, 5, color, -1)// 周围轮廓四角形绘制 (Green)color = Scalar(0.0, 255.0, 0.0)Imgproc.rectangle(dst, box.tl(), box.br(), color, 2)boxs.add(box)}}boxs.sortBy {it.x}val result = ArrayList<NumberInfo>()var num = 0Lboxs.mapIndexed { index, it ->if (it.x+it.width>mRgb.width()){it.width = mRgb.width()-it.x}val tmp = Mat(mRgb, it)val tmpBitmap = Bitmap.createBitmap(tmp!!.cols(), tmp.rows(),Bitmap.Config.ARGB_8888)Utils.matToBitmap(tmp, tmpBitmap)list.mapIndexed { index, it ->val result_cols: Int = abs(tmp.cols() - it.cols()) + 1val result_rows: Int = abs(tmp.rows() - it.rows()) + 1val res = Mat(result_rows, result_cols, CvType.CV_32FC1)//归一化var item = itval bitmap2 = Bitmap.createBitmap(it!!.cols(), it!!.rows(),Bitmap.Config.ARGB_8888)Utils.matToBitmap(it, bitmap2)Imgproc.resize(it, it, Size(tmp.width().toDouble(), tmp.height().toDouble()))Imgproc.matchTemplate(tmp, item, res, Imgproc.TM_SQDIFF)val bitmap = Bitmap.createBitmap(tmp!!.cols(), tmp!!.rows(),Bitmap.Config.ARGB_8888)Utils.matToBitmap(tmp, bitmap)val bitmap1 = Bitmap.createBitmap(item!!.cols(), item!!.rows(),Bitmap.Config.ARGB_8888)Utils.matToBitmap(item, bitmap1)//获得最可能点,MinMaxLocResult是其数据格式,包括了最大、最小点的位置x、yval mlr = Core.minMaxLoc(res)result.add(NumberInfo(mlr.minVal, index))Log.e("AccessbilityServiceImp", "mlr result minVal="+ mlr.minVal + ",maxVal=" + mlr.maxVal + ",minLoc=" + mlr.minLoc + ",maxLoc=" + mlr.maxLoc)}result.sortBy {it.minValue}num += result[0].index * 10.0.pow(boxs.size.toDouble() - 1 - index).toLong()result.clear()}if (index == 0){myPlayInfo = ComplyPlayInfo(num,index)}else{dectectedPlays.add(ComplyPlayInfo(num,index))}Log.e("AccessbilityServiceImp", "num result =" + num)}resultPlays = dectectedPlays.filter { myPlayInfo.strength > it.strength }Log.e("AccessbilityServiceImp", "resultPlays =" + resultPlays)return resultPlays}

调用 Imgproc.matchTemplate(),使用数字模板对比,相似度高的就认为是该阿拉伯数字,逐个数字叠加后获得战力值,返回战力值比自己队伍低的集合,上层获取到该值后挑选还能挑战的队伍进行战斗,(相同的队伍当天只能挑战它8次)。

任务栏刷任务:

在任务栏进行刷任务操作需要进行的步骤:识别当前视图所有的任务的星数(1到7星)->挑选符合条件的任务刷新->向右滑动任务->重新识别还没有操作过的任务->刷新完后点击一键领取已经完成的任务。

识别当前任务的星数代码片段:

    private fun cutStarsPictrue(bitmap: Bitmap, list: ArrayList<Rect>): List<TaskInfo> {val bitmapNew: Bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true)//opencv初始化if (!OpenCVLoader.initDebug()) {Log.d("--------opencv--------", "OpenCVLoader error")}//Mat变换val mRgb = Mat(bitmapNew.height, bitmapNew.width, CvType.CV_8UC4)Utils.bitmapToMat(bitmapNew, mRgb)var starsNum = ArrayList<TaskInfo>()list.map {val rect = Rect(it.x + 15, it.y + 8, it.width - 35, it.height - 18)val src = Mat(mRgb, rect)val b = Bitmap.createBitmap(src!!.cols(), src.rows(),Bitmap.Config.ARGB_8888)Utils.matToBitmap(src, b)val num = returnStarsNum(b)starsNum.add(TaskInfo(rect,num))Log.e("AccessbilityServiceImp", " num =" + num)}return starsNum}

识别当前任务的内容代码片段:(主要是识别当前任务是否符合条件,三星任务只需要英雄碎片,4星任务抛弃竞技场挑战券,4星以上任务全部完成)

fun cutTaskContent(bitmap: Bitmap, list: List<TaskInfo>): List<TaskInfo> {val bitmapNew: Bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true)//opencv初始化if (!OpenCVLoader.initDebug()) {Log.d("--------opencv--------", "OpenCVLoader error")}//Mat変換val mRgb = Mat(bitmapNew.height, bitmapNew.width, CvType.CV_8UC4)Utils.bitmapToMat(bitmapNew, mRgb)var starsNum = ArrayList<TaskInfo>()var rect:Rectlist.map {//如果是3星任务截图区域if (it.stars < 4){rect = Rect(it.rect.x + bitmapNew.width/23, it.rect.y - 3 + bitmapNew.height/32*9, it.rect.width/3 , it.rect.height/2 )}else {//如果是4-5星任务截图区域rect = Rect(it.rect.x + bitmapNew.width / 60,it.rect.y + bitmapNew.height / 33 * 6,it.rect.width / 5,it.rect.width / 3)}val src = Mat(mRgb, rect)val b = Bitmap.createBitmap(src!!.cols(), src.rows(),Bitmap.Config.ARGB_8888)Utils.matToBitmap(src, b)val num = returnTaskStarsNum(b)/*如果是3星任务返回5是英雄碎片如果是4,5星任务返回5是竞技场挑战券*/if (it.stars < 4) {if (num >= 4) {starsNum.add(it)}}else{if (num in 5..9) {starsNum.add(it)}}Log.e("AccessbilityServiceImp", " starsNum =" + starsNum)}return starsNum
}

指令执行:

复杂的点击和滑动指令操作需要一个执行者来完成:Handler

val mHandler:TaskHandler = TaskHandler()class TaskHandler:Handler(Looper.getMainLooper()){override fun handleMessage(msg: Message) {val callback = msg.obj as ()->Unitcallback.invoke()}}

用消息把不同的指令传给Handler去处理,可以延时执行,也可以随时取消没有完成的指令任务。

    //滑动fun slidingScreen(scrollData: ScrollData, delayTime: Long){val callback = {if (height != 0f) {if (width != 0f) {ActionDeal(width/2,height/2,scrollData,62)}}}timespec = delayTimemHandler.postDelayed(callback,-1,delayTime)}//点击fun clickScreen(x:Float,y: Float){val callback = {ActionDeal(x,y,null,62)}mHandler.postDelayed(callback,-1,timespec)timespec+=1000Log.e("AccessbilityServiceImp","timespec = "+timespec)}//点击(带时间差)fun clickScreen(x:Float,y: Float,delayTime: Long){val callback = {if (height != 0f) {if (width != 0f) {ActionDeal(x,y,null,62)}}}mHandler.postDelayed(callback,62,delayTime)}

不用指令之间的等待时间各不相同,需要给根据实际的时间差来完成一连串指令。

创建任务实体类:

data class TaskInfo(val callback: () -> Unit, val timespec: Long, val id:Int)

创建任务集合:

var taskList = ArrayList<TaskInfo>()

Handler执行所有的指令任务:

taskList.add(TaskInfo({localBroadcastReceiver.startSnapShoot()},timespec,48))taskList.map {Log.e("AccessbilityServiceImp","timespec = "+it.timespec)mHandler.postDelayed(it.callback,it.timespec)
}

传送门:项目源码地址
创造不易,欢迎点赞starred me

Android 辅助服务实战-游戏点击器相关推荐

  1. android动态评论功能,Android辅助权限实战之微信自动评论与点赞

    Android辅助权限实战之微信自动评论与点赞 当我们把辅助权限玩的比较熟悉 的时候,就可以释放我们的双手做一些有趣的事情了,例如之前网上流传的微信自动抢红包插件,就是使用的这个服务,不过我们今儿讲的 ...

  2. android 辅助服务 简书,Android AccessibilityService使用

    测试demo主要使用了Android 的无障碍辅助服务(AccessibilityService),主要注意是 1. 开启服务,绑定目标app的监听(demo中根据包命绑定了体积计算的app),需要在 ...

  3. 注意android辅助服务事件不能用于保存

    本来希望把来自辅助服务的事件,像epoll那样暂存在队列进行调度,或者做成事件堆栈,从而将辅助服务事件加入到容器.但是一直不能达到预期的后果.最后才发现一个坑人的事实,辅助服务事件被释放(或者说重置) ...

  4. 使用android studio时酷狗音乐,17 Android Studio开发实战:音乐播放器——浪花音乐...

    手机上的多媒体内容讲究声情并茂.悦目且悦耳,这样才能让用户的感官得到最大享受.影视播放器由于存在视频自身的画面,反而限制了开发者的施展空间:而音乐播放器允许定制播放画面,开发者有足够空间施展拳脚.本节 ...

  5. Android辅助权限实战之微信自动评论与点赞

    当我们把辅助权限玩的比较熟悉 的时候,就可以释放我们的双手做一些有趣的事情了,例如之前网上流传的微信自动抢红包插件,就是使用的这个服务,不过我们今儿讲的是微信自动评论与点赞功能(抢红包的文章网上已经有 ...

  6. Android辅助工具助手-keep 自动点赞器

    0.运行界面 源码下载 https://github.com/sufadi/AccessibilityServiceMonitor 1.需求点赞界面,进行自动点击 注意:要clickable事件为tu ...

  7. android 辅助服务自动右滑,我的手机启用辅助功能后怎么滑动屏幕,是什么盲人的什么功能,怎么取消?...

    满意答案 xt0e 2017.01.11 采纳率:50%    等级:12 已帮助:8005人 看这个,一招学会使用VoiceOver功能. 输入密码:先选择你要输入的数字,然后双击这个字母就可以了. ...

  8. Android辅助服务监听dialog,Android开发中对话框辅助类——DialogHelper

    写在前面: 对话框在平时的开发工作中使用率很频繁,但是很多开发者每次使用都去写一堆代码,如此,不单单效率不高,而且代码也不优...为此,写了简单的封装. 效果如: 关键类DialogHelper.ja ...

  9. Android 中 利用 AccessibilityService 辅助服务 模拟点击事件

    在 Android 中想要执行一些模拟点击操作,在无法修改页面源码的情况下,通常只能使用 adb 和借助辅助功能两种方式. Adb 方式 借助 adb shell 的命令,我们可以使用下面的方式模拟一 ...

最新文章

  1. bzoj 1189 紧急疏散 网络流
  2. canvas动画3:交互
  3. popwindow弹窗
  4. Windows恶搞脚本,太实用了医院WiFi很快
  5. [JNI] 开发基础(6)字符串相关操作
  6. 求素数平均值c语言,C 输入10个正整数到a数组中,求a数组中素数的平均值.
  7. MVP2006成都聚会图片
  8. 人工智能的马克思主义审视
  9. 武汉凭什么被列为国家超大城市?
  10. 显卡+cuda+cudnn+tensorflow安装教程
  11. cad图纸怎么看懂_如何看懂CAD图?
  12. 美国探亲签证面签时一定要用英语吗?
  13. Keil MDK使用第7篇---Go To Definition 和 Go To referebce的区别
  14. C# 20行代码解析KRC歌词内容
  15. 改善技术简历的47条原则
  16. Android学习——5个UI界面设计
  17. 【开源】司马编译器 Smart Compiler 符号表
  18. 特斯拉的市场策略在中国面临“失效”
  19. 【vn.py学习笔记(二)】vn.py底层接口 学习笔记
  20. Garch模型Stata实例

热门文章

  1. Isaac Sim 使用指南(一)
  2. 在Qt中设置程序图标的方法介绍
  3. oracle基本命令
  4. 电脑主板为什么不取消电池?
  5. 吉他笔记 solo 和弦 推弦 音程
  6. ssm水果商城项目遇到的问题和解决
  7. CSS总结---持续更新中 2022.8.4
  8. Extending Air
  9. windows10 更新NVIDIA 显卡驱动
  10. 饿了么交易系统 5 年演化史