Nougat通知栏图块一键登录校园网完整开发笔记

开发初衷
在校园中每次使用校园网都需要手动登录一次,有时甚至不会自动跳转登录页面,造成一些不便。所以打算编写一款个人使用在这个前提下,则不必考虑诸多额外因素,比如系统兼容、帐号密码设置、人性化提示等)的小工具,实现一键登录。


实现方式
笔者的设备系统是 Android Nougat / 7.1.2,所以可充分利用 Nougat 的新特性,把这个小工具做成通知栏快捷设置图块(Quick Settings Tile)。这样,在连接到校园网 WiFi 后,下拉通知,点击图块即可实现登录。

成果截图:

Nougat通知栏图块一键登录校园网完整开发笔记_第1张图片
CdutNet Tile

关于这个新特性,在一些国产安卓系统上大概是不存在的,它们的通知栏被深度定制过,相关 API 被阉割。

知识点

  • Chrome 开发者工具分析登录接口
  • Chrome 接口调试工具 Postman 测试接口
  • 新的开发语言之 Kotlin 而非 Java
  • Android 网络开发基础(原生HttpURLConnectionAPI)
  • Nougat 新特性之快捷设置图块(Quick Settings Tile)API
  • 矢量图标(Vector Drawable)的选取

一、分析登录接口

Nougat通知栏图块一键登录校园网完整开发笔记_第2张图片
校园网登录页面
Nougat通知栏图块一键登录校园网完整开发笔记_第3张图片
登录成功后页面内容

笔者使用熟悉的 Chrome 浏览器及其开发者工具来分析。

Nougat通知栏图块一键登录校园网完整开发笔记_第4张图片
登录请求

提示:在登录成功后会跳转另外的站点,而这会导致开发者工具里记录的请求日志消失。解决方法是在开发者工具的工具栏里勾选 ☑️Preserve log 选项进行长连接记录日志。

从截获的请求数据中可以梳理出以下要点:

登录接口URL http://172.20.255.252/
请求方式 POST
请求头部(Headers)数据 初步判断可忽略(结果证明确如此)
表单数据 DDDDD为用户名
upass为密码(显然,经过加密,初步判断为 MD5 加密)
其余字段R1R2para0MKKey初步判断为定值

接下来,查看网页源码搞清楚表单数据的生成规则(尤其是密码)。

定位到

,找到在 POST 之前处理参数的函数ee()

Nougat通知栏图块一键登录校园网完整开发笔记_第5张图片
网页源码

ee()函数中找到密码加密的关键代码。不难看出,加密方式为加盐值(salt)的 MD5 加密。pid值(常量,值为1) + 明文密码 + calg值(常量,值为12345678)经过 MD5 加密,然后加密值 + pid值 + calg值 就是最后的密文值了。

Nougat通知栏图块一键登录校园网完整开发笔记_第6张图片
网页源码

使用 Postman (一个 Chrome 应用,用于接口调试)进行测试。

Nougat通知栏图块一键登录校园网完整开发笔记_第7张图片
Postman 测试

测试成功。至此,笔者已经搞清楚登录接口的调用方法。

二、实现 Android APP

笔者用 Visio 画了功能流程图:

Nougat通知栏图块一键登录校园网完整开发笔记_第8张图片
流程图

本例将选用 Kotlin 来进行整个开发。Kotlin 是一门开发 Android 的新兴语言,最近在 Google I/O 2017 大会上被推为 Android 开发官方语言。因此有必要熟悉并掌握它。去了解 Kotlin

首先,笔者跟随 Kotlin 官网提供的教程一步步在 Android Studio 中完成 Kotlin 项目的搭建。

Nougat通知栏图块一键登录校园网完整开发笔记_第9张图片
项目结构

本着偷懒的精神,笔者在以下两个站点选择了一个合适的图块图标:

  • Google 家的 Material icons - Material Design
  • Austin Andrews 个人维护的 Material Design Icons
Nougat通知栏图块一键登录校园网完整开发笔记_第10张图片
选择图标

笔者在上文提到这款 APP 为自用,所以兼容SDK版本直接上跳到24(Nougat,7.0)

Nougat通知栏图块一键登录校园网完整开发笔记_第11张图片
配置

AndroidManifest 关键配置:







    
        
    

编写工具方法:

package com.by_syk.onetapcdutnet.util

import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkInfo

object Net {
    /**
     * 判断 WiFi 连接
     */
    fun isWiFiConnected(context: Context): Boolean {
        val manager: ConnectivityManager? = context.getSystemService(Context.CONNECTIVITY_SERVICE)
                as ConnectivityManager
        val networkInfo: NetworkInfo? = manager?.activeNetworkInfo
        return networkInfo != null && networkInfo.isAvailable
                && networkInfo.type == ConnectivityManager.TYPE_WIFI
    }
}
package com.by_syk.onetapcdutnet.util

import java.security.MessageDigest

object MD5 {
    /**
     * MD5 加密并返回十六进制结果
     */
    fun md5(text: String): String {
        val messageDigest = MessageDigest.getInstance("MD5")
        messageDigest.update(text.toByteArray())
        val bytes = messageDigest.digest()
        return parseHex(bytes)
    }

    private fun parseHex(bytes: ByteArray): String {
        val sb = StringBuilder()
        for (b in bytes) {
            val tmp = (b.toInt() and 0xff).toString(16)
            if (tmp.length == 1) {
                sb.append("0")
            }
            sb.append(tmp)
        }
        return sb.toString()
    }
}
package com.by_syk.onetapcdutnet.util

object C {
    // 校园网登录地址
    val CAMPUS_NET_URL = "http://172.20.255.252"
    // 登录用户名
    val USER_NAME = "xxx"
    // 登录密码
    val PWD = "xxx"
}

接口调用核心类:

package com.by_syk.onetapcdutnet.util

import java.io.BufferedReader
import java.io.DataOutputStream
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL

object CdutNet {
    /**
     * 检测校园网连接状态
     * 
     * @return null - 未连接到校园网
     *         false - 已连接校园网但未登录
     *         true - 已登录校园网
     */
    fun check(): Boolean? {
        var bufferedReader: BufferedReader? = null
        try {
            val huc = URL(C.CAMPUS_NET_URL).openConnection() as HttpURLConnection
            huc.requestMethod = "GET"
            huc.connectTimeout = 3000
            huc.readTimeout = 3000
            val inputStream = huc.inputStream
            bufferedReader = BufferedReader(InputStreamReader(inputStream, "GBK"))
            val sbContent = StringBuilder()
            var buffer = bufferedReader.readLine()
            while (buffer != null) {
                sbContent.append(buffer)
                buffer = bufferedReader.readLine()
            }
            return sbContent.contains("已使用时间")
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            if (bufferedReader != null) {
                bufferedReader.close()
            }
        }
        return null
    }

    /**
     * 登录校园网
     * 
     * @param userName 登录帐号
     * @param pwd 登录密码
     * @return true - 登录成功
     *         false - 登录失败
     */
    fun login(userName: String, pwd: String): Boolean {
        try {
            val huc = URL(C.CAMPUS_NET_URL).openConnection() as HttpURLConnection
            huc.requestMethod = "POST"
            huc.connectTimeout = 4000
            huc.readTimeout = 4000
            huc.doOutput = true

            val PID = "1"
            val CALG = "12345678"
            var enPwd = MD5.md5("$PID$pwd$CALG")
            enPwd = "$enPwd$CALG$PID"
            val paras = "DDDDD=$userName&upass=$enPwd&R1=0&R2=1¶=00&0MKKey=123456"
            val dos = DataOutputStream(huc.outputStream)
            dos.write(paras.toByteArray())
            dos.flush()
            dos.close()

            val inputStream = huc.inputStream
            val bufferedReader = BufferedReader(InputStreamReader(inputStream, "GBK"))
            val sbContent = StringBuilder()
            var buffer = bufferedReader.readLine()
            while (buffer != null) {
                sbContent.append(buffer)
                buffer = bufferedReader.readLine()
            }
            bufferedReader.close()

            return sbContent.contains("登录成功窗")
        } catch (e: Exception) {
            e.printStackTrace()
        }
        return false
    }
}

核心快捷设置图块 Service:

package com.by_syk.onetapcdutnet.service

import android.os.AsyncTask
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import com.by_syk.onetapcdutnet.R
import com.by_syk.onetapcdutnet.util.C
import com.by_syk.onetapcdutnet.util.CdutNet
import com.by_syk.onetapcdutnet.util.Net

class QSTileService : TileService() {
    /**
     * 图块可见回调
     */
    override fun onStartListening() {
        super.onStartListening()

        if (Net.isWiFiConnected(this)) {
            updateStatus(R.string.tile_label)
        } else { // 未连接 WiFi 则将图块置为不可用状态
            updateStatus(R.string.tile_status_no_net_conn, false)
        }
    }

    /**
     * 图块点击回调
     */
    override fun onClick() {
        super.onClick()

        Task().execute()
    }

    /**
     * 更新图块状态
     *
     * @param labelId 文字ID
     * @param enable 状态
     */
    fun updateStatus(labelId: Int?, enable: Boolean = true) {
        if (labelId != null) {
            qsTile.label = getString(labelId)
        }
        if (enable) {
            qsTile.state = Tile.STATE_ACTIVE
        } else {
            qsTile.state = Tile.STATE_UNAVAILABLE
        }
        qsTile.updateTile()
    }

    /**
     * 核心异步任务,检查、登录校园网
     */
    inner class Task : AsyncTask() {
        override fun doInBackground(vararg params: String?): Boolean {
            publishProgress(R.string.tile_status_check)
            val res = CdutNet.check() // 检查校园网连接
            if (res == null) { // 未连接校园网,结束
                publishProgress(R.string.tile_status_not)
                return false
            } else if (!res) { // 未登录校园网,进行登录操作
                publishProgress(R.string.tile_status_loggin)
                if (!CdutNet.login(C.USER_NAME, C.PWD)) { // 登录失败,结束
                    publishProgress(R.string.tile_status_failed)
                    return false
                }
            }
            // 至此,表示已成功登录
            publishProgress(R.string.tile_status_ok)
            return true
        }

        override fun onProgressUpdate(vararg values: Int?) {
            super.onProgressUpdate(*values)

            updateStatus(values[0])
        }
    }
}
Nougat通知栏图块一键登录校园网完整开发笔记_第12张图片
一键登录GIF演示

三、写在最后

完整代码请移步 GitHub 查看:OneTapCdutNet - GitHub

你可能感兴趣的:(Nougat通知栏图块一键登录校园网完整开发笔记)