最近在使用心悦俱乐部这个APP,里面有个代币叫G分,可以换游戏道具,但需要每天领取,比较繁琐。于是索性做一个自动领取G分的辅助,姑且叫它G分助手吧。
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配置文件如下:
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): 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 需求分析
拟定功能有三点:
签到
领取月卡积分
喂猫
那么,我们的辅助功能应该是这样的:
每日定时打开心悦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...
}
}
首先检查一下今日任务是否已经执行完毕了,如果是的话就什么都不做。因为不能让辅助妨碍了用户的正常操作,所以在完成了任务之后就不需要再进行操作了。
按照4.2.1中的流程依次执行任务。
执行完毕之后,将结果保存在本地。
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分了。