需求:通过手机给设备升级固件,设备有WIFI
方案:升级包放到APP可以访问的目录,手机开热点并启动一个HTTP服务,设备连接手机热点,另外,设备端开启一个 telnet 服务,手机通过 telnet 登录到设备系统(Android系统热点的默认IP地址是192.168.43.1,APP可以遍历192.168.43这个IP段的IP以及固定的端口),通过指令下载固件,完成升级。
- implementation 'commons-net:commons-net:3.9.0' // telnet
- implementation 'org.nanohttpd:nanohttpd:2.3.1' // NanoHttpd
- implementation 'org.apache.commons:commons-compress:1.23.0'//解压缩文件
- implementation 'com.github.junrar:junrar:7.5.4'//解压rar
- implementation 'org.tukaani:xz:1.9'//解压.7z文件需要
- implementation 'com.sparkjava:spark-core:2.9.4'// sparkjava
- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
- <uses-permission android:name="android.permission.INTERNET" />
- <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
- <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
- <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
- <uses-permission android:name="android.permission.WRITE_SETTINGS" />
- <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
-
-
- <uses-permission
- android:name="android.permission.NEARBY_WIFI_DEVICES"
- android:usesPermissionFlags="neverForLocation"
- tools:targetApi="s" />
-
-
- <uses-permission
- android:name="android.permission.ACCESS_FINE_LOCATION"
- android:maxSdkVersion="32" />
- <uses-permission
- android:name="android.permission.READ_EXTERNAL_STORAGE"
- android:maxSdkVersion="32" />
- import android.Manifest
- import android.content.ActivityNotFoundException
- import android.content.ComponentName
- import android.content.Context
- import android.content.Intent
- import android.content.pm.PackageManager
- import android.net.wifi.SoftApConfiguration
- import android.net.wifi.WifiManager
- import android.os.Build
- import android.util.Log
- import androidx.annotation.RequiresApi
- import androidx.core.app.ActivityCompat
- import java.lang.reflect.Field
- import java.lang.reflect.Method
-
-
- class APUtil(private val context: Context) {
- val wifiManager = context.getSystemService(Context.WIFI_SERVICE) as WifiManager
-
-
- private fun securityTypeString(securityType: Int): String {
- return when (securityType) {
- SoftApConfiguration.SECURITY_TYPE_OPEN -> "OPEN"
- SoftApConfiguration.SECURITY_TYPE_WPA2_PSK -> "WPA2_PSK"
- SoftApConfiguration.SECURITY_TYPE_WPA3_OWE -> "WPA3_OWE"
- SoftApConfiguration.SECURITY_TYPE_WPA3_SAE -> "WPA3_SAE"
- SoftApConfiguration.SECURITY_TYPE_WPA3_OWE_TRANSITION -> "WPA3_OWE_TRANSITION"
- SoftApConfiguration.SECURITY_TYPE_WPA3_SAE_TRANSITION -> "WPA3_SAE_TRANSITION"
- else -> "Unknown security type: $securityType"
- }
- }
-
- @RequiresApi(Build.VERSION_CODES.O)
- private val localOnlyHotspotCallback = object : WifiManager.LocalOnlyHotspotCallback() {
- override fun onStarted(reservation: WifiManager.LocalOnlyHotspotReservation?) {
- super.onStarted(reservation)
- if (Build.VERSION.SDK_INT >= 30) {
- reservation?.softApConfiguration?.let { config ->
- Log.i(TAG, "-------------------------- softApConfiguration --------------------------")
- if (Build.VERSION.SDK_INT >= 33) {
- Log.i(TAG, "热点名称: ${config.wifiSsid}")
- } else {
- Log.i(TAG, "热点名称: ${config.ssid}")
- }
- Log.i(TAG, "热点密码: ${config.passphrase}")
- Log.i(TAG, "securityType=${securityTypeString(config.securityType)}")
- Log.i(TAG, "mac=${config.bssid}")
- }
- } else {
- reservation?.wifiConfiguration?.let { config ->
- Log.i(TAG, "-------------------------- wifiConfiguration --------------------------")
- Log.i(TAG, "热点名称: ${config.SSID}")
- Log.i(TAG, "热点密码: ${config.preSharedKey}")
- Log.i(TAG, "mac=${config.BSSID}")
- Log.i(TAG, "status=${config.status}")
- config.httpProxy?.let { httpProxy ->
- Log.i(TAG, "http://${httpProxy.host}:${httpProxy.port}")
- }
- }
- }
- }
-
- override fun onStopped() {
- super.onStopped()
- Log.e(TAG, "onStopped()")
- }
-
- override fun onFailed(reason: Int) {
- super.onFailed(reason)
- Log.e(TAG, "onFailed() - reason=$reason")
- }
- }
-
-
- fun startHotspot() {
- if (Build.VERSION.SDK_INT >= 26) {
- if (ActivityCompat.checkSelfPermission(
- context,
- Manifest.permission.ACCESS_FINE_LOCATION
- ) != PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission(
- context,
- Manifest.permission.NEARBY_WIFI_DEVICES
- ) != PackageManager.PERMISSION_GRANTED
- ) {
- return
- }
- // 官方文档
- // https://developer.android.google.cn/reference/android/net/wifi/WifiManager#startLocalOnlyHotspot(android.net.wifi.WifiManager.LocalOnlyHotspotCallback,%20android.os.Handler)
- wifiManager.startLocalOnlyHotspot(localOnlyHotspotCallback, null)
- }
- }
-
-
- fun printHotSpotState() {
- Log.i(TAG, "热点是否已经打开:方式1 ${isHotSpotApOpen(context)} 方式2 ${isHotSpotApOpen2(context)}")
- Log.i(TAG, "手机型号:${Build.MANUFACTURER} ${Build.MODEL}")
- }
-
-
- companion object {
-
- private const val TAG = "APUtil"
-
- //获取热点是否打开方式1
- fun isHotSpotApOpen(context: Context): Boolean {
- var isAPEnable = false
- try {
- val wifiManager = context.getSystemService(Context.WIFI_SERVICE) as WifiManager
- val method: Method = wifiManager.javaClass.getDeclaredMethod("getWifiApState")
- val state = method.invoke(wifiManager) as Int
- val field: Field = wifiManager.javaClass.getDeclaredField("WIFI_AP_STATE_ENABLED")
- val value = field.get(wifiManager) as Int
- isAPEnable = state == value
- Log.i(TAG, "state=$state, value=$value")
- } catch (e: Exception) {
- e.printStackTrace()
- }
- return isAPEnable
- }
-
-
- //获取热点是否打开方式2
- fun isHotSpotApOpen2(context: Context): Boolean {
- var isAPEnable = false
- try {
- val wifiManager = context.getSystemService(Context.WIFI_SERVICE) as WifiManager
- val method: Method = wifiManager.javaClass.getDeclaredMethod("isWifiApEnabled")
- method.isAccessible = true
- isAPEnable = method.invoke(wifiManager) as Boolean
- } catch (e: Exception) {
- e.printStackTrace()
- }
- return isAPEnable
- }
-
- /**
- * 打开设置热点的页面
- */
- fun openSettings(context: Context) {
- val intent = Intent()
- intent.addCategory(Intent.CATEGORY_DEFAULT)
- intent.action = "android.intent.action.MAIN"
- val cn = ComponentName(
- "com.android.settings",
- "com.android.settings.Settings\$WirelessSettingsActivity"
- )
- intent.component = cn
- try {
- context.startActivity(intent)
- } catch (ex: ActivityNotFoundException) {
- intent.component = ComponentName(
- "com.android.settings",
- "com.android.settings.Settings\$TetherSettingsActivity"
- )
- context.startActivity(intent)
- }
- }
- }
- }
HttpFileServer.kt
- import android.content.Context
- import android.util.Log
- import fi.iki.elonen.NanoHTTPD
- import org.json.JSONObject
- import java.io.FileInputStream
- import java.io.FileNotFoundException
- import java.io.IOException
- import java.util.stream.Collectors
-
- /**
- * 手机开热点时使用的IP
- */
- const val SERVER_IP = "192.168.43.1"
- const val SERVER_PORT = 8080
-
- private const val TAG = "HttpFileServer"
-
- // HFS
- class HttpFileServer(context: Context, ipAddress: String) : NanoHTTPD(ipAddress, SERVER_PORT) {
-
- init {
- rootDir = context.getExternalFilesDir("OTA")!!.absolutePath
- Log.i(TAG, "rootDir=$rootDir")
- }
-
- override fun serve(session: IHTTPSession): Response? {
- val params = session.parameters
- Log.e(TAG, String.format("uri=%s", session.uri))
- for (entry in params.entries) {
- Log.e(TAG, String.format("%s=%s", entry.key, entry.value.stream().collect(Collectors.joining(", "))))
- }
- return responseFile(session)
- }
-
- private fun responseFile(session: IHTTPSession): Response? {
- //目前使用的是 http://192.168.43.1:8080/filename
- val uri = session.uri
- val filename = uri.substring(uri.lastIndexOf('/') + 1)
- return try {
- //文件输入流
- val fis = FileInputStream("$rootDir/${filename}")
- newFixedLengthResponse(Response.Status.OK, "application/octet-stream", fis, fis.available().toLong())
- } catch (e: FileNotFoundException) {
- Log.e(TAG, "$filename 文件不存在!", e)
- val jsonObj = JSONObject()
- jsonObj.put("message", "$filename 文件不存在!")
- Log.e(TAG, jsonObj.toString(2))
- newFixedLengthResponse(Response.Status.NOT_FOUND, "application/json", jsonObj.toString(2))
- } catch (e: IOException) {
- e.printStackTrace()
- newFixedLengthResponse(session.uri + " 异常 " + e)
- }
- }
-
- companion object {
- var rootDir: String = ""
- }
- }
注意:使用的是sparkjava,而不是大数据分析的那个Spark!
- // http://192.168.43.1:8080/download?filename=dog.jpg
- Spark.port(8080)
- Spark.get("download") { req, res ->
- val filename = req.queryParams("filename")
- println("thread ${Thread.currentThread().id} - $filename")
-
- res.status(200)
- res.header("Content-Type", "application/octet-stream")
- res.header("Content-disposition", "attachment; filename=$filename")
-
- context.assets.open(filename).use { istream ->
- istream.copyTo(res.raw().outputStream)
- }
- res
- }
telnet 主要用到了 WifiUtil 的一个属性:network
- import android.content.Context
- import android.net.ConnectivityManager
- import android.net.Network
- import android.net.NetworkCapabilities
- import android.net.NetworkRequest
- import android.net.wifi.WifiInfo
- import android.net.wifi.WifiManager
- import android.net.wifi.WifiNetworkSpecifier
- import android.os.Build
- import android.os.PatternMatcher
- import android.util.Log
- import androidx.annotation.RequiresApi
- import site.feiyuliuxing.wifitest.toIPAddress
-
- class WifiUtil(context: Context) {
- private val mNetworkCallback: ConnectivityManager.NetworkCallback
- private val mConnectivityManager: ConnectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
- private val wifiManager: WifiManager = context.getSystemService(Context.WIFI_SERVICE) as WifiManager
- private var mNetwork: Network? = null
-
- //Android系统热点的默认IP地址是192.168.43.1
- var hostIP = "192.168.43.1"
- private set
- val network: Network?
- get() = mNetwork
- val targetIP: String
- get() {
- val arr = hostIP.split(".").toMutableList()
- arr[arr.lastIndex] = "1"
- return arr.joinToString(".")
- }
-
- init {
- if (Build.VERSION.SDK_INT < 29) {
- //Android10以下系统
- mNetworkCallback = object : ConnectivityManager.NetworkCallback() {
- override fun onAvailable(network: Network) {
- callbackAvailable(network)
- }
-
- override fun onLost(network: Network) {
- callbackLost(network)
- }
- }
- } else if (Build.VERSION.SDK_INT < 31) {
- //Android10 Android11
- mNetworkCallback = object : ConnectivityManager.NetworkCallback() {
- override fun onAvailable(network: Network) {
- callbackAvailable(network)
- }
-
- override fun onLost(network: Network) {
- callbackLost(network)
- }
-
- @RequiresApi(Build.VERSION_CODES.Q)
- override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
- // Android10才支持该回调方法
- callbackCapabilitiesChanged(network, networkCapabilities)
- }
- }
- } else {
- //Android12获取 SSID 需要 FLAG_INCLUDE_LOCATION_INFO 并获得 ACCESS_FINE_LOCATION 权限
- mNetworkCallback = object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
- override fun onAvailable(network: Network) {
- callbackAvailable(network)
- }
-
- override fun onLost(network: Network) {
- callbackLost(network)
- }
-
- @RequiresApi(Build.VERSION_CODES.Q)
- override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
- // Android10才支持该回调方法
- callbackCapabilitiesChanged(network, networkCapabilities)
- }
- }
- }
- }
-
- private fun callbackAvailable(network: Network) {
- mNetwork = network
- hostIP = getIP(network)
- }
-
- private fun callbackLost(network: Network) {
- mNetwork = null
- }
-
- @RequiresApi(Build.VERSION_CODES.Q)
- private fun callbackCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
- val wifiInfo = networkCapabilities.transportInfo as WifiInfo?
- if (wifiInfo != null) {
- println("onCapabilitiesChanged() - SSID=${wifiInfo.ssid}, IP=${wifiInfo.ipAddress.toIP()}")
- }
- }
-
- private val networkRequest: NetworkRequest
- get() = NetworkRequest.Builder()
- .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
- .removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
- .apply {
- if (Build.VERSION.SDK_INT >= 29) {
- val wifiNetworkSpecifier = WifiNetworkSpecifier.Builder()
- // .setBand(ScanResult.WIFI_BAND_24_GHZ)//Android12 但是设置这个参数后无法连接
- .setSsidPattern(PatternMatcher("ZS[0-9a-fA-F]{8}", PatternMatcher.PATTERN_ADVANCED_GLOB))
- // .setSsid("ZS3755a0e2")
- .setWpa2Passphrase("12345678")
- .build()
-
- setNetworkSpecifier(wifiNetworkSpecifier)
- }
- }
- .build()
-
- fun requestConnectWIFI() {
- mConnectivityManager.requestNetwork(networkRequest, mNetworkCallback)
- }
-
- fun close() {
- mConnectivityManager.unregisterNetworkCallback(mNetworkCallback)
- }
-
- //无需运行时申请权限
- fun getGatewayIP(): String {
- if (Build.VERSION.SDK_INT < 32) {
- /*This method was deprecated in API level 31.
- Use the methods on LinkProperties which can be obtained either via
- NetworkCallback#onLinkPropertiesChanged(Network, LinkProperties) or
- ConnectivityManager#getLinkProperties(Network).*/
- val dhcpInfo = wifiManager.dhcpInfo
-
- // 网关IP地址是一个整数,需要转换为可读的IP地址格式
- val gatewayIpAddress: String = dhcpInfo.gateway.toIPAddress()
- println("Gateway IP: $gatewayIpAddress")
- return gatewayIpAddress
- } else {
- var addr = ""
- mConnectivityManager.activeNetwork?.let { network ->
- mConnectivityManager.getLinkProperties(network)?.let { linkProperties ->
- linkProperties.dhcpServerAddress?.let { inet4Address ->
- addr = inet4Address.hostAddress ?: ""
- }
- }
- }
- println("网关IP: $addr")
- return addr
- }
- }
-
- private fun getIP(network: Network): String {
- mConnectivityManager.getLinkProperties(network)?.linkAddresses?.let { linkAddresses ->
- linkAddresses.forEach { linkAddress ->
- // hostAddress: the IP address string in textual presentation.
- linkAddress.address.hostAddress?.let { hostAddress ->
- if (hostAddress.isValidIP) {
- println("手机IP:${hostAddress}")
- return hostAddress
- }
- }
- }
- }
- return ""
- }
-
- private val ipRegex = """\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}""".toRegex()
-
- private val String.isValidIP: Boolean
- get() = ipRegex.matches(this)
-
- private fun Int.toIP(): String {
- return "${this.and(0xff)}" +
- ".${this.shr(8).and(0xff)}" +
- ".${this.shr(16).and(0xff)}" +
- ".${this.shr(24).and(0xff)}"
- }
- }
- import android.net.Network
- import org.apache.commons.net.telnet.EchoOptionHandler
- import org.apache.commons.net.telnet.SuppressGAOptionHandler
- import org.apache.commons.net.telnet.TelnetClient
- import org.apache.commons.net.telnet.TerminalTypeOptionHandler
-
- class TelnetUtil {
- private val telnetClient = TelnetClient()
-
- private fun connectServer(ip: String, network: Network?): Boolean {
- try {
- if (network != null) {
- telnetClient.setSocketFactory(network.socketFactory)
- }
-
- /*val ttopt = TerminalTypeOptionHandler("VT220", false, false, true, false)
- val echoopt = EchoOptionHandler(true, false, true, false)
- val gaopt = SuppressGAOptionHandler(true, true, true, true)
- telnetClient.addOptionHandler(ttopt)
- telnetClient.addOptionHandler(echoopt)
- telnetClient.addOptionHandler(gaopt)*/
-
- telnetClient.connect(ip, 23)
- if (telnetClient.isConnected) {
- println("端口可用 $ip")
- return true
- }
-
- } catch (ex: Exception) {
- ex.printStackTrace()
- }
- return false
- }
-
- fun readUntil(match: String, timeout: Long = 10_000): Boolean {
- val sb = StringBuilder()
- val startTime = System.currentTimeMillis()
- while (true) {
- if ((System.currentTimeMillis() - startTime) >= timeout) {
- //超时
- break
- }
- var len = telnetClient.inputStream.available()
- while (len > 0) {
- val ch = telnetClient.inputStream.read()
- if (ch != -1) {
- sb.append(ch.toChar())
- print(ch.toChar())
- if (ch == 0x0D || ch == 0x0A) {
- System.out.flush()
- }
- if (sb.contains(match)) {
- println(sb.toString())
- return true
- }
- }
- len--
- }
- }
- println(sb.toString())
- return false
- }
-
- fun send(msg: String) {
- telnetClient.outputStream.apply {
- write(msg.toByteArray(Charsets.UTF_8))
- flush()
- }
- }
- }
参考这篇文章:Android解压 zip rar 7z 文件