前段时间
李跳跳
事件沸沸扬扬,想必大家都有耳闻,一个无毒无广告的帮助用户自动跳过广告的绿色软件竟然收到来自大厂的律师函,说是导致他们的广告收益大幅下降。我TM真想邦邦两拳!真是动了某些人的蛋糕就要赶尽杀绝的节奏。所以写这篇文章算是致敬吧!
背景
-
我们都知道现在随便打开一个APP,启动页都是充斥着各种各样的广告,一般都是要等三到五秒钟才会自己关闭,或者用户手动点击跳过按钮直接进入首页,其实这件事在以前更为泛滥,已经到了无法忍受的地步,不知道是哪位变态产品想出来的
摇一摇
进入广告,问题的关键在于TMD的设置的传感器参数非常灵敏,手机稍微动一下就触发打开广告的操作了,简直没人性
。 -
其实网上已经有很多人写过类似的文章了,那么为什么我还要重复写一遍呐,虽然实现的整体思路大家都基本一致,但是具体的实现细节或多或少的有些不一样的,我也是抱着学习的态度去实现一遍,算是是对之前写的自动化系列文章的一种延续吧。
实现思路
-
所有的实现方案都是基于
Android无障碍
模式做的,加上我们之前已经对微信的各种自动化方案已经很熟悉了(不了解的可以先阅读一下玩转Android自动化系列专栏),所以这里实现起来也就顺手拈来了。 -
我们要想自动点击
跳过
按钮就要找到触发的时机,一般APP启动的时候都会先进入启动页广告,然后在进入首页,所以我们的问题就变成怎么判断APP启动。 -
我们知道在
onAccessibilityEvent
方法的回调中是可以看到当前event的一些具体信息的,比如包名,类名等。大致内容如下:
css
EventType: TYPE_WINDOW_STATE_CHANGED; EventTime: 333079282; PackageName: com.lygttpod.android.auto;WindowChangeTypes: [] [ ClassName: com.lygttpod.android.auto.MainActivity; Text: [Android自动化];
我们只需要找到需要的包名,然后去遍历当前页面的元素,找到带有
跳过
或者关闭
字样的节点然后触发点击事件即可。
经过我们的大量尝试后会发现其实只需要监听typeWindowStateChanged
就可以了,这样不仅可以减少频繁触发的次数也会过滤掉很多无用的event
- 怎么根据当前事件的包名去找到我们需要的呐,这里我们可以定义一个配置文件
fuck_ad_app_config.json
,文件中添加我们需要跳过广告的APP信息,然后在服务链接的时候解析文件取出来数据进行匹配就ok了。
json
{
"fuckAd":{
"apps":[
{
"appName":"XX财富",
"packageName":"com.eastmoney.android.berlin",
"launcher":"com.eastmoney.android.berlin.activity.MainActivity",
"adNodes":[
{
"action":"跳过"
}
]
}
]
}
}
- 看到了包名和启动页就相当于检测到APP启动了,然后用我们之前写好的方法遍历找到指定节点触发点击就好了。
kotlin
private fun loadAdConfig(): FuckAdApps? {
return try {
val json = AppContext.loadAsset("fuck_ad_app_config.json")
if (json.isNullOrEmpty()) {
null
} else {
val data: FuckAdApps =
Gson().fromJson(json, object : TypeToken<FuckAdApps>() {}.type)
data
}
} catch (e: Exception) {
e.printStackTrace()
null
}
}
- 拿到配置文件中的数据后就判断触发event的应用是否在配置文件中,是的话就触发点击跳过操作,大致代码如下
kotlin
fun fuckAD(event: AccessibilityEvent) {
fuckADTaskScope.launch {
val packageName = event.packageName.default()
val className = event.className.default()
val adApp =
fuckAdApps?.fuckAd?.apps?.find { it.packageName == packageName && it.launcher == className }
adApp?.let {
Log.d("FuckADTask", "打开了【${it.appName}】的【${it.launcher}】")
skipAd(it)
}
}
}
private suspend fun skipAd(adApp: AdApp) {
val acc = FuckADAccessibility.fuckADAccessibility ?: return
val actions = adApp.adNode.action.split(",")
val skipResult = actions.find { acc.clickByText(it) } != null
if (skipResult) {
withContext(Dispatchers.Main) {
Log.d("FuckADTask", "自动跳过广告啦")
}
}
}
- 这样我们就实现了自动点击【跳过】的功能了,但是这样写的会有很大的局限性,就是我们必须先获取到所有需要跳过广告的APP的包名和启动页类名,这是一件非常麻烦的事情,当然我也尝试这么做了,首先我下载了应用市场top50的APP,一个一个的测试,在打印的log中寻找对应的信息,在我尝试找了二十多个APP的时候我放弃了,因为我发现这是一件非常笨重的做法,即使我把下载的这50个APP都配置好了,也不能保证别人能下载和我一模一样的APP啊,市场上APP那么多怎么可能搞得完。
优化
- 既然每个用户手机中安装的APP都不一样,我们能不能先获取手机中已安装的APP呐,然后再对其处理不就好了么,这样就可以针对当前手机设备中的APP进行操作了,不仅减少了非自己安装的APP遍历过程,也可以覆盖到每个用户的设备。而且Android也提供的有相应的API去获取应用列表。于是就有了如下代码
kotlin
fun Context.queryAllInstallApp(): MutableList<AdApp> {
val packageManager = this.packageManager
// 创建一个 Intent,用于查询所有启动的应用程序
val intent = Intent(Intent.ACTION_MAIN, null)
intent.addCategory(Intent.CATEGORY_LAUNCHER)
// 使用 queryIntentActivities 获取所有匹配的应用列表
val appInfoList =
packageManager.queryIntentActivities(intent, 0)
.filterNot { isSystemApp(it) || isSelf(it) }
.map {
val appName = it.loadLabel(packageManager).toString()
val packageName = it.activityInfo.packageName
val className = it.activityInfo.name
AdApp(appName, packageName, className)
}
.toMutableList()
return appInfoList
}
- 这样就可以全面覆盖到每个用户安装的APP了,但是在测试中会发现还会有点问题,就是手机系统中内置的APP也会在里边,有很多其实我们是不需要去监听的,于是我们就把系统内置APP给过滤掉,顺手再把自己的APP也过滤了。当然其实想要做的更好就在添加一个配置菜单,让用户自己去选择需要过滤的APP也是一种不错的选择(后边有需要再加吧),这里我就先自动过滤系统内置了
kotlin
// 检查应用是否为系统应用
private fun isSystemApp(resolveInfo: ResolveInfo): Boolean {
val regex = "com\.android\.[^.]+" // 匹配系统应用 com.android. 后面跟着任意不包含 . 的字符
val result = resolveInfo.activityInfo.packageName.matches(Regex(regex))
Log.d("isSystemApp", "isSystemApp: ${resolveInfo.activityInfo.packageName} : $result")
return result
}
- 这样我们就真正实现了自动跳过自己设备安装的非系统APP的启动页广告了,让我们变一下看一下效果吧。
makefile
打开了【知乎】的【com.zhihu.android.app.ui.activity.LauncherActivity】
自动跳过【知乎】的广告啦
打开了【知乎】的【com.zhihu.android.app.ui.activity.LauncherActivity】
isSkipped: 5秒内已经成功跳过一次广告,无需重复检测
打开了【网易云音乐】的【com.netease.cloudmusic.activity.IconChangeDefaultAlias】
自动跳过【网易云音乐】的广告啦
进阶
在我自己使用的过程中遇到误判的情况,比如某个页面的文本中带有跳过的字眼也会触发,所以我们还需要对误判的情况处理一下。这里我分了几种情况。
1、action的长度
- 广告页的跳过有一定的规则,一般都会有字数限制,比如
跳过
、关闭
、跳过(5)
、5s | 跳过
等等,我们这里可以添加一个最大字数长度限制的判断,大部分字数都在五个字以内,所谓我们设置默认最大长度是5,个别APP字数长的可以单独在进行设置。
2、action所在类的类型
- 广告页的跳过一般都是文本消息,如果如你在
EditText
中输入了跳过
两个字,按照原来的判断逻辑也会触发我们的判断,所以我们只需要TextView类型的文本。当然还有极个别APP的广告跳过两个字是图片,真TM想方设法的搞事哦,遇到这种情况可以单独设置id也能做兼容,有的APP是没有text值的,只有id(12306APP
就是只有id),如果连ID都TM没有那就直接向工信部举报吧(哈哈,开玩笑。。。)。
3、action是否在列表内的内容
- 大部分情况下广告页的内容不会是在列表中展示的,为了避免某些内容中的有跳过字眼触发误判,还可以加上判断是否在ListView或RecyclerView中等等,这样就避免了大部分内容区域带有关键字眼导致的误判
经过上面这些判断后就会大大降低的误触的概率,当然有遇到特殊的情况可以继续加判断
scss
val skipResult = acc.clickByCustomRule {
if (id.isNotBlank()) {
it.viewIdResourceName == id
} else {
if (it.isTextView()) {
val text = it.text.default()
text.length <= adApp.actionMaxLength()
&& actions.find { action -> text.contains(action) } != null
&& it.inListView().not()
} else {
false
}
}
}
if (skipResult) {
withContext(Dispatchers.Main) {
Log.d("FuckADTask", "自动跳过【${adApp.appName}】的广告啦")
}
}
- 当然也可以针对每个APP单独设置自己的规则,设置内容如下
- 至此自动跳过启动页广告的自动化工具算是完成了,中间其实有很多优化的细节就不在文章中展开讲解了,需要详细了解的可以阅读一下源码
适配
【铁路12306】
获取到的text是空,所以就设置ID为com.MobileTicket:id/tv_skip
进行适配,className = android.widget.TextView → id = com.MobileTicket:id/tv_skip → isClickable = true
ini
+--- className = android.widget.FrameLayout
| --- className = android.widget.LinearLayout
| --- className = android.widget.FrameLayout → id = android:id/content
| --- className = android.widget.LinearLayout → id = com.MobileTicket:id/ll_splash_ad_container
| +--- className = android.widget.FrameLayout → id = com.MobileTicket:id/fl_adContent_container
| | +--- className = android.widget.ImageView → id = com.MobileTicket:id/img_adContent → description = 广告 → isClickable = true
| | +--- className = android.widget.FrameLayout → id = com.MobileTicket:id/fl_skip_wrong → isClickable = true
| | | --- className = android.widget.TextView → id = com.MobileTicket:id/tv_skip → isClickable = true
| | --- className = android.view.ViewGroup
| | +--- className = android.widget.ImageView → id = com.MobileTicket:id/lottie_scroll_view
| | --- className = android.view.ViewGroup → id = com.MobileTicket:id/cl_button_container → isClickable = true
| | +--- className = android.widget.ImageView → id = com.MobileTicket:id/lottie_button_view
| | +--- className = android.widget.TextView → text = 上滑或点击跳转至详情页 → id = com.MobileTicket:id/tv_button_text
| | --- className = android.widget.ImageView → id = com.MobileTicket:id/iv_right_arrow
|
【中国联通】
获取到的文本是5s | 跳过
,action长度是7,所以设置actionMaxLength为7
即可
ini
+--- className = android.widget.FrameLayout
| --- className = android.widget.LinearLayout
| --- className = android.widget.FrameLayout
| --- className = android.widget.LinearLayout → id = com.sinovatech.unicom.ui:id/action_bar_root
| --- className = android.widget.FrameLayout → id = android:id/content
| --- className = android.widget.RelativeLayout
| +--- className = android.widget.FrameLayout → id = com.sinovatech.unicom.ui:id/splash_container2
| | --- className = android.widget.RelativeLayout
| | +--- className = android.widget.ImageView → id = com.sinovatech.unicom.ui:id/welcome_bg
| | +--- className = android.widget.TextView → text = 5s | 跳过 → id = com.sinovatech.unicom.ui:id/welcome_advertise_close → isClickable = true
| | --- className = android.widget.ImageView → id = com.sinovatech.unicom.ui:id/adv_bottom_detail → isClickable = true
| --- className = android.widget.ImageView → id = com.sinovatech.unicom.ui:id/welcome_bottom_logo
在使用过程中如有发现其他特殊的APP欢迎提issues或者打在评论区,我们一起去维护,造福更多人。
最后
-
在具体的代码实现中已经做了很多优化工作,不会影响APP正常使用,也不会导致耗电增多或导致系统卡顿之类的问题,完全可以放心使用。
-
为了防止我们的广告服务被系统杀死,可以设置我们的APP为系统白名单或者允许后台运行等一系列保活方法让它多活一会吧。
感兴趣的可以下载demo体验一下,在阅读源码过程遇到任何问题欢迎提Issues,如果对你有帮助,希望动动你的发财小手点个赞呗
如果你会文中的实现方法还不太了解,建议先看看一下前几期的玩转Android自动化系列文章介绍,相信一定会对你有所帮助的。