Android辅助工具,G分助手的实现 - 心悦俱乐部app自动签到、领G分

最近在使用心悦俱乐部这个APP,里面有个代币叫G分,可以换游戏道具,但需要每天领取,比较繁琐。于是索性做一个自动领取G分的辅助,姑且叫它G分助手吧。

这个辅助主要是通过Accessibility Service(辅助功能)实现的,总体思路就是通过AccessibilityService模拟点击来实现自动化。项目地址是https://github.com/LittleFogCat/gpointhelper

1. 查看包名和当前Activity

首先使用adb shell连接上手机。在启动应用之后,输入dumpsys activity activities命令查看当前的Activity。

image.png

image.png

可以看到,包名是com.tencent.tgclub,欢迎页是WelcomeActivity,主页面是MainActivity

2. 查看当前应用布局,View的id等

在Android sdk目录下,有一个tools文件夹。这之中有一个monitor工具,也就是之前的DDMS。连接手机到电脑之后,通过monitor即可看到当前应用界面的布局了。

  • 点击dump view hierarchy


    点击dump
  • 当前应用布局


    当前应用布局.png

通过monitor工具,我们就可以获取到想要点击View的id,从而为实现模拟点击做好准备。

3. AccessibilityService的配置

Accessibility Service的教程网上一搜一大把,很简单,这里就不赘述了。

AccessibilityService的xml配置文件如下:

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged|typeViewClicked"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:accessibilityFlags="flagReportViewIds|flagRequestEnhancedWebAccessibility|flagRetrieveInteractiveWindows"
    android:canRequestEnhancedWebAccessibility="true"
    android:canRetrieveWindowContent="true"
    android:canPerformGestures="true"
    android:description="@string/app_name"
    android:notificationTimeout="10"
    android:packageNames="com.tencent.tgclub" />

其中特别要指出的是,flagRequestEnhancedWebAccessibility这一项,是为了操作WebView中的内容的。最坑的地方在于,在api 26中这个flag就被废弃了,而且我并没有找到替代方法。也就是说,在Android O以后的手机很可能就不能用这个方式了,而且竟然没有可以替代的方式!(只能用Android 7及以前的手机暂时苟一下)

image.png

4. 实现代码

4.1 判断是否开启AccessibilityService的权限

/**
 * 检测是否本应用辅助功能开启
 */
fun isAccessibilitySettingsOn(mContext: Context, clazz: Class<out AccessibilityService>): Boolean {
    var accessibilityEnabled = 0
    val service = mContext.packageName + "/" + clazz.canonicalName
    try {
        accessibilityEnabled = Settings.Secure.getInt(mContext.applicationContext.contentResolver,
                Settings.Secure.ACCESSIBILITY_ENABLED)
    } catch (e: Exception) {
        e.printStackTrace()
    }

    val mStringColonSplitter = TextUtils.SimpleStringSplitter(':')
    if (accessibilityEnabled == 1) {
        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()
                if (accessibilityService.equals(service, ignoreCase = true)) {
                    return true
                }
            }
        }
    }
    return false
}
    /**
     * 检查是否开启辅助功能,没有开启就跳转到设置页面
     */
    private fun checkAccessibilityOn() {
        if (!isAccessibilitySettingsOn(this, GPointService::class.java)) {
            mDialog = makeDialog(this,
                    "需要打开辅助功能",
                    "点击确定,在设置中找到\"G分助手\",打开辅助功能",
                    "确定",
                    DialogInterface.OnClickListener { _, _ -> startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)) },
                    false)
            mDialog!!.show()
        }
    }

4.2 AccessibilityService的实现

4.2.1 需求分析

拟定功能有三点:

  1. 签到
  2. 领取月卡积分
  3. 喂猫

那么,我们的辅助功能应该是这样的:
每日定时打开心悦app -> 检查今日是否完成以上任务 -(如果没有)-> 执行任务 -> 检查是否成功 [ -> 上报]

4.2.2 思路

在AccessibilityService的onAccessibilityEvent回调方法中,可以接收到在xml中指定app的事件。

    /**
     * 检测到事件。
     */
    override fun onAccessibilityEvent(event: AccessibilityEvent) {
        Log.v(TAG, "onAccessibilityEvent: " + AccessibilityEvent.eventTypeToString(event.eventType))
        if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
            // window状态改变(切换窗口、显示隐藏、对话框等)
            // doSth...
        } else if(event.eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
            // 当前window内容改变
            // doSth...
        }
    }
  1. 首先检查一下今日任务是否已经执行完毕了,如果是的话就什么都不做。因为不能让辅助妨碍了用户的正常操作,所以在完成了任务之后就不需要再进行操作了。

  2. 按照4.2.1中的流程依次执行任务。

  3. 执行完毕之后,将结果保存在本地。

4.2.3 设计

思路上捋清楚了,接下来就是具体是设计了。

1. 任务设计

任务的设计主要分为两个方面。第一,数据结构;第二,存储方式。
如何得知当日是否已经执行完毕?执行任务之后如何存储?
最简单可行的想法就是使用SharedPreferences来进行记录,读取之后保存在AccessibilityService中。

    private lateinit var mSharedPreferences: SharedPreferences

    override fun onServiceConnected() {
        Log.d(TAG, "onServiceConnected: GPointService")
        mSharedPreferences = getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
    }

另一方面,由于任务状态包括了签到、领取月卡积分、喂猫三项,任务重复性高,如果全都写在AccessibilityService中,冗余度很高,所以单独抽象一个Task基类出来。这样做的好处不仅在于可以减少重复代码,而且如果以后有新的任务,直接继承这个父类即可,利于扩展:


uml1

最后,创建一个TaskManager的单例类,统一管理任务,将职责从AccessibilityService中分割出来。

uml3

这里任务的设计暂告一段落了。

2. 任务执行

在任务执行之后,还需要保存任务的完成状态。对于每日更新的任务来说,每天的任务完成之后,需要将任务标记为已完成,并且在第二天的0点(或其他时候)重新变成未完成。

这个花了我很多时间,主要是比较各种方式的优劣。对于定/延时任务,一般来说有这几种方法:

  • Timer
  • Handler
  • AlarmManager

鉴于任务间隔时间很长,所以这里采用了AlarmManager作为定时任务的方法。
另外,考虑到不同的任务可能会有不同的执行时间和间隔,那么就没法统一执行时间了。这个问题其实还是很棘手的,虽然有不同的解决方案,但是我最后也没有找到一个比较完美的。

最终方案

采用过期时间mExpireTime。对于每个Task来说,执行完毕任务之后,设定一个任务过期时间。任务过期之前为保护期,在保护期内,任务不会再次运行。超过过期时间的,或者没有设置过期时间的,视为过期任务,则执行。同时将isTaskDone()方法修改为shouldRunTask()方法,使其更符合实际逻辑。

每次AccessibilityService的onAccessibilityEvent回调均会调用TaskManager.checkAndRunTasks()方法来检查所有任务是否过期,对于未过期的任务则跳过,只执行已过期的任务。在checkAndRunTasks方法中,会调用每个Task的shouldRunTask方法,检查是否应该运行。

对于TaskManager,简化了外部接口,使得任务的执行更加便捷且清晰;同时当一个任务执行时,其他任务禁止执行,避免互相干扰。
另外对Task类也进行了优化,删去了过度设计的部分。

umllast

4.3 最终代码

虽然因为手里没有Android 7测试样机,具体功能的实现没有办法继续写下去了,不过大体框架已经完成,剩下的内容就是往里面写各个任务的业务逻辑了,甚至根据需求可以添加其他任务。
Github地址是https://github.com/LittleFogCat/gpointhelper,有兴趣的可以自己改着玩。(不过我相信没有人会坚持看到这里。)
事实上,写到这里,我已经不想领G分了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,377评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,390评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,967评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,344评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,441评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,492评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,497评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,274评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,732评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,008评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,184评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,837评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,520评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,156评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,407评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,056评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,074评论 2 352

推荐阅读更多精彩内容

  • 微信红包自打出世以来就极其受欢迎,抢红包插件可谓红极一时.今天,我们重新谈谈抢红包插件的哪些事儿.本质上,抢红包插...
    于加泽阅读 1,858评论 0 6
  • 又来到了一个老生常谈的问题,应用层软件开发的程序员要不要了解和深入学习操作系统呢? 今天就这个问题开始,来谈谈操...
    tangsl阅读 4,114评论 0 23
  • 文/tangsl(简书作者) 原文链接:http://www.jianshu.com/p/2b993a4b913e...
    西葫芦炒胖子阅读 3,754评论 0 5
  • 口是伤人斧,言是割心刀,出言三思,责人有度。一个人的宽容,来自一颗善待他人的心。一个人的涵养,来自一颗客观的心。一...
    隔壁小志龙阅读 225评论 0 0
  • 今天处理了一起学生投诉,跟随学生来的“执法人员”态度特别恶劣,言语粗俗,导致了我也被对方的情绪带动,做出了一些消极...
    泥巴叔叔阅读 102评论 0 1