最近看有大佬通过python实现了辽大的疫情自动打卡,就想到了前些天自己也写了一个Android版的疫情自动打卡,原理很简单,就是前台服务保活,服务中运行定时器,每天定时将打卡内容post到学校的服务器。先来看一下软件界面。PS:第一次写文章,加上这篇文章本身技术不难,所以内容可能有点草,还请各位大佬轻喷。该文章仅供学习交流使用,请勿用于其他用途。


依照上面的想法,文章大概分为:前台服务、定时器、post数据三个部分。
一、前台服务
在Android O及更高版本的系统上,如果想启动一个前台服务,就必须要在通知栏显示一个通知,即使用startForegroundService()方法启动前台服务,否则5秒后会抛出异常。所以这里我们也必须创建一个通知。
private fun startNotificationForeground() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val Channel = NotificationChannel(
CHANNEL_ID,
"自动打卡服务",
NotificationManager.IMPORTANCE_HIGH
)
Channel.enableLights(true) //设置提示灯
Channel.lightColor = Color.RED //设置提示灯颜色
Channel.setShowBadge(true) //显示logo
Channel.description = "自动打卡服务" //设置描述
Channel.lockscreenVisibility =
Notification.VISIBILITY_PUBLIC //设置锁屏可见 VISIBILITY_PUBLIC=可见
manager?.createNotificationChannel(Channel)
val notification = Notification.Builder(this)
.setChannelId(CHANNEL_ID)
.setAutoCancel(false)
.setContentTitle("自动打卡服务") //标题
.setContentText("下次打卡时间$date") //内容
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.mipmap.ic_launcher) //小图标一定需要设置,否则会报错(如果不设置它启动服务前台化不会报错,但是你会发现这个通知不会启动),如果是普通通知,不设置必然报错
.setLargeIcon(BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher))
.build()
startForeground(
1,
notification
) //服务前台化只能使用startForeground()方法,不能使用 notificationManager.notify(1,notification); 这个只是启动通知使用的,使用这个方法你只需要等待几秒就会发现报错了
}
}
二、定时器
我们当然不需要一个空壳前台服务,启动这个服务的目的就是让定时器保活,所以我们还要在通知栏信息推送的同时启动定时器。
private fun setService() {
val intent = Intent(this, AlarmReceiver::class.java)
intent.action = ALARM_SINGLE_ACTION
val pi = PendingIntent.getBroadcast(this, 1, intent, 0)
val am =
getSystemService(Context.ALARM_SERVICE) as AlarmManager
val nowCalendar = Calendar.getInstance()
val calendar = Calendar.getInstance()
calendar[Calendar.YEAR] = nowCalendar[Calendar.YEAR]
calendar[Calendar.MONTH] = nowCalendar[Calendar.MONTH]
calendar[Calendar.DAY_OF_MONTH] = nowCalendar[Calendar.DAY_OF_MONTH]
calendar[Calendar.HOUR_OF_DAY] = 7
calendar[Calendar.MINUTE] = 0
calendar[Calendar.SECOND] = 0
date = if (nowCalendar.timeInMillis > calendar.timeInMillis) { //切换到下一天
am.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
calendar.timeInMillis + 24 * 60 * 60 * 1000,
pi
)
Date(calendar.timeInMillis + 24 * 60 * 60 * 1000).toString()
} else {
am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, calendar.timeInMillis, pi)
Date(calendar.timeInMillis).toString()
}
}
在这里,我们采用AlarmManager来做定时器,具体使用方法参考其他文章。其中setExactAndAllowWhileIdle()方法第一个参数传入AlarmManager.RTC_WAKEUP,此时第二个参数就要传入毫秒时间。这个毫秒是RTC时间,即从1970年开始计时的。这个我们不管,我们只需要new一个Calendar对象并且设置好下次打卡的时间,然后用Calendar的getTimeInMillis()方法就可得到这个毫秒。
设置好定时器后,我们还需要在在指定时间相应该定时器,这就需要我们写一个接收器。
class AlarmReceiver : BroadcastReceiver(){
val ALARM_SINGLE_ACTION = "com.sunshine.auto_tjxx.ALARM_SINGLE_ACTION"
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == ALARM_SINGLE_ACTION){
val toTjxx = ToTjxx()
toTjxx.toLoad(context!!)
val thread: Thread = object : Thread() {
override fun run() {
super.run()
try {
sleep(1000)
context.stopService(Intent(context, MyService::class.java))
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(Intent(context, MyService::class.java))
}else{
context.startService(Intent(context, MyService::class.java))
}
}catch (e: Exception){}
}
}
thread.start()
}
}
}
接收器接收到指定action后,就回去post已经设置好的参数,并且停止掉当前服务,重新开始新的服务。这个部分是为了可以让第二天继续自动打卡而不需自己重新设置。
三、post数据
经过研究发现,用户登录后网页会返回一个cookie值,然后提交信息时,通过这个cookie值来验证身份。我们通过模拟登录,来获取这个cookie值,拿到这个值后我们post这个cookie值和我们提交的信息,就可以自动打卡成功。
//模拟登录疫情打卡系统
fun toLoad(context: Context){
//通过SharedPreferences储存数据
sp = context.getSharedPreferences("info", Context.MODE_PRIVATE)
val okHttpClient = OkHttpClient()
.newBuilder()
.followRedirects(false)
.build()
val requestBody = FormBody.Builder()
.add("userid",sp.getString("userid", "")!!)
.add("userpwd",sp.getString("userpwd", "")!!)
.build()
val request = Request.Builder()
.url("http://tjxx.lnu.edu.cn/login_do.asp")
.post(requestBody)
.build()
okHttpClient.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.e(TAG,e)
}
override fun onResponse(call: Call, response: Response) {
toLoad2(response.headers("Set-Cookie"))
}
})
}
//post数据
private fun toLoad2(headers: List<String>) {
val okHttpClient = OkHttpClient()
.newBuilder()
.followRedirects(false)
.build()
val requestBody = FormBody.Builder()
.add("xszd",sp.getString("clwz", "")!!)
.add("xxdz",sp.getString("xxwz", "")!!)
.add("csld",sp.getString("ldjl", "")!!)
.add("csldxx",sp.getString("ldjl_text", "")!!)
.add("hbjc",sp.getString("jc", "")!!)
.add("hbjcxx",sp.getString("jc_text", "")!!)
.add("fyzz",sp.getString("stzk", "")!!)
.add("fyzzxx",sp.getString("stzk_text", "")!!)
.add("glzt",sp.getString("glgc", "")!!)
.add("zxzg",sp.getString("zxzg", "")!!)
.add("gdipszd",sp.getString("gddw", "")!!)
.add("bdipszd",sp.getString("bddw", "")!!)
.add("txipszd",sp.getString("txdw", "")!!)
.build()
val request = Request.Builder()
.url("http://tjxx.lnu.edu.cn/inputExt_do.asp")
.header("Cookie",headers.toString())
.post(requestBody)
.build()
okHttpClient.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.e(TAG,e)
}
override fun onResponse(call: Call, response: Response) {
Log.e(TAG,response)
}
})
}
我们通过提交表单信息来提交疫情信息,图中字母缩写可参考网站的打卡信息F12,最近可能有更变。最后三项分别是高德、百度和腾讯的定位信息,可填可不填,我这里是手动填写的,也没什么问题。如果你想自动填写请自己引入相对应的API。
总结
最后看下效果。

行文至此,疫情自动打卡的大概思路就写完了。写的比较草,因为本身也不难,具体还请大家研究,我就不细写了。第一次写文章,还请各位大佬轻喷。最后附上源代码,供大家参考。