辽宁大学疫情自动打卡

276 阅读4分钟

最近看有大佬通过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。

总结

最后看下效果。

效果展示
只需要在通知栏有此通知就代表可以了。

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