坑娃-防沉迷App
猿爸:屁孩,已经看了很长xx了,该让眼睛休息下了。
屁孩:不情不愿的关了,去玩别的。
很和谐是不是?当你不在家的时候,屁孩就没这么乖了,是不是,认同不?
这时就需要有一个强势的“人”来阻止他,这个可别奢望老人来阻止他,于是只能让“机器人”来搞了。
于是搞了个App,名字叫坑娃神器(不知道若干年后,屁孩看到这个文章心里是什么滋味。。。),功能比较简单,就是监控指定app的活动状态,如果连续运行一段时间,坑娃神器就会自动从后台切到前台进行倒计时,倒计时结束之后可以继续娱乐。
这个App目前只是处于原始社会版,希望有能力的猿爸们加入,我们一起将其完善,帮助下一代养成一个好的自律能力,此App已在github上开源,欢迎贡献,项目地址https://github.com/hunshenshi/timeup.git
调研–准备工作
找了下目前市面已有的工具和一些手机自带的健康使用的功能,不太满足我的需求,
1、大多数都是限制每天累计使用时长,而我希望每使用一段时间就休息一会,而不关注累计时长
2、由于市面上都是一些面向广大用户的产品,达到时长之后会有提示,而不会有更激烈的行为。而我的需求强硬,再强硬(反正用户只有家长的娃)
我想要的功能也比较简单,就是当检测到前台视频类的软件连续运行一段时间之后,依然在运行,就将其切换到后台,并将倒计时页面切换至前台,等待倒计时结束。随后再次进行下一轮监控,不监控累计时长,只监控连续使用时长,并且强制切换App。
没有现成的轮子那就自己造一个吧,梳理下会用到的技术点:
1、App保活,因为需要坑娃神器长期存活于后台进行监控
2、如何拿到正在运行的app
3、定时执行,需要坑娃神器定期检测前台运行的app是什么
4、后台唤醒,需要坑娃神器从后台唤醒到前台进行倒计时
看了些相关的文章,感觉功能大都可以实现,只是安卓的版本太多了,各种功能在各个版本中实现方式又不一样,在这特别同情搞安卓的朋友,真不容易呀。。。
第一个Android Application
决定要搞了,那就先从Hello World开始吧,首先下载个Android Studio,然后根据向导创建一个android应用,这里语言我选的是java,一路下来就能运行一个demo app了。具体步骤如下:
新建project之后,进入选择模版页面,这里我选的是basic,下面还有很多模版可以选
选择下一步之后,填写project相关的信息,比如project名字,需要的语言和sdk
点击Finish就结束了,等待IDE加载程序就行了。
这个过程可能会比较慢,主要是gradle-5.6.4-all.zip下载慢,你可以使用迅雷下载,然后放到指定的目录就OK了。
App保活
App保活这个话题,不搜不知道,一搜是打开眼界呀,充分展现了群众的智慧呀。什么双Activity相互唤醒、播放无声音频各种奇淫巧技。
不过我这里就自己用,这么多的奇淫巧技我是用不上了(就算需要用上我也没有那么高深的功力),规规矩矩的申请权限吧。
常驻后台需要申请电池白名单权限,网上有很多代码示例,相关代码如下:
1 | Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); |
获取正在运行的App
如何通过代码拿到正在前台展示的app,也是困难重重,对于一无所知的我依然是先网上搜一堆demo,然后测试。
搜素的结果大体分为3种,主要分布在不同的sdk版本中,已经过时的有getRunningTasks
和getRunningAppProcesses
,在新版sdk中都普遍使用UsageStatsManager
,使用UsageStatsManager
时需要申请权限,而且通过它获取app的方法网上也有好几种,我对比了几种使用的是queryUsageStats
方法,还可以使用queryEvents
方法,不过我测试发现queryEvents
状态变更没有queryUsageStats
及时。(不要问我为什么测试了这么多方法。。)
整体思路是通过queryUsageStats
查询一段时间内的App使用信息,然后对App的最后更新时间进行排序,时间最大的就是正在前台展示的App。代码示例如下:
1 | // 获取UsageStats集合 |
定时周期执行任务
由于这只是个坑娃的App,所以这里直接简单粗暴的使用定时任务来check前台App是否为所要监控的App,而没有使用各种Trigger。
Android中的定时任务主要是Timer
和AlarmManager
,推荐使用AlarmManager
,而且不需要申请权限。
AlarmManager
结合Service
使用,在高版本的sdk中,AlarmManager
没有重复执行的功能,需要在Service
再次调用AlarmManager
从而起到重复执行任务的功能。而且在高版中为了节省电量也进行了很多优化,要想精准出发定时器需要使用setExactAndAllowWhileIdle
方法,具体代码如下:
1 | AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); |
注意两次调用定时器的不同,在CheckService
中调用时Trigger为SystemClock.elapsedRealtime() + TIME_INTERVAL
,代表延后TIME_INTERVAL
之后触发。
后台唤醒
后台唤醒是指正在后台运行的App由于某种原因被切换到前台。
如果是在低版本的sdk中可以使用getRunningTasks
,拿到对应package的task信息,其中包括taskId,然后调用moveTaskToFront
方法将其任务切换到前台,但是getRunningTasks
已被标注为过时的,所以在上面获取前台App时也没用该方法。
这里使用了新建Activity的方法,具体代码如下:
1 | public void toRunningForeground(String packageNameTarget) { |
踩坑实录
主要功能开发完成,并在IDE中正常运行,下面就要实机测试了,前方冰冷的洪水迎面拍来!!!
将App安装到平板上之后,进行测试发现应该切换倒计时页面时并没有正常切换,于是下面就开始了取经之后:
- sdk版本不兼容
因为Android sdk各版本有可能不兼容,所以首先怀疑是不是sdk的问题,于是在IDE中设置了对应版本,发现确实没有生效,于是搜索对应sdk后台切换前台的相关问题,发现可能是权限问题,sdk 29之后限制了App的相应权限,需要申请浮窗权限,申请代码如下:
1 | public void requestOverlayPermission(Context context) { |
获取当前App
申请相应权限之后,在IDE中可以正常运行,再次部署到平板上进行测试,发现依然没有生效,继续排查。
在IDE中多次排查时偶然发现获取当前App时,有次拿到了com.xx.android.launcher
,以为是获取前台App不准确原因造成的,于是就用了好多种方法进行测试,发现始终未生效。但是在真机测试的过程中,发现一个规律,只要插上usb,App就运行正常,在IDE中查看App在平板上的运行日志也正常,可是拔掉usb就不生效,也看不到当时的日志,于是就想打印些关键信息在平板上查看进行排查问题。真机上查看运行日志
看一些日志教程好复杂,专门搞一下真的不值得呀,于是想到是不是可以把关系信息写到一个可读的文件里,于是将其写入,代码如下:1
2
3
4
5public static void writeFile(Context context, String filename, String filecontent) throws IOException {
FileOutputStream output = context.openFileOutput(filename, Context.MODE_APPEND);
output.write(filecontent.getBytes());
output.close();
}使用时只传入文件名就行,默认写到
/data/data/app/com.xx/files
目录下,去平板上找这个目录,却没有找到。。。不过连上usb可以在IDE的Device File Explorer
中找到,于是再次的测试流程就是拔掉usb进行测试,然后连上usb进行文件查看运行信息。怀疑AlarmManger在后台不生效
查看App写入文件的信息之后,发现定时器只是第一次启动了,随后的定时器并未触发。
在使用AlarmManger
时也没有要求申请什么权限,搜索之后也没有找到任何线索,于是资源一些专业人士,朋友说可能是定时器触发间隔时间太短,系统给优化了,并附上了源码中的方法注释,可信度很高呀,我也很开心,要搞定了,可是修改安装之后依然是暴击呀。
此时真的陷入了死胡同,没有任何线索,一连好几天没有任何进展,偶然在手机上查看应用权限的时候,想到是不是平板自动启停的原因,于是将App的自动管理切换为手动管理,再次进行测试,生效了。
太艰辛了,为了坑个娃太不容易了。。。
这个App目前只是处于原始社会版,希望有能力的猿爸们加入,我们一起将其完善,帮助下一代养成一个好的自律能力,此App已在github上开源,欢迎贡献,项目地址https://github.com/hunshenshi/timeup.git
后记
其实写这个App并不完全是为了完全阻止他玩平板,只是为了培养他解决问题,善于思考的能力,让他知道如何去达到自己的目的。因为这个App本身比较简单,可能只能阻止他几天,然后他会发现这个App的逻辑漏洞,然后我再想办法把这个漏洞堵上,继续等待他发现新的漏洞,希望在这个过程中能培养他的好奇欲。