Android开启HTTP服务

需求:通过手机给设备升级固件,设备有WIFI

方案:升级包放到APP可以访问的目录,手机开热点并启动一个HTTP服务,设备连接手机热点,另外,设备端开启一个 telnet 服务,手机通过 telnet 登录到设备系统(Android系统热点的默认IP地址是192.168.43.1,APP可以遍历192.168.43这个IP段的IP以及固定的端口),通过指令下载固件,完成升级。

1. 用到的第三方库

Groovy 复制代码
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

2. 添加权限

XML 复制代码
    <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" />
    <!-- If your app targets Android 13 (API level 33)
          or higher, you must declare the NEARBY_WIFI_DEVICES permission. -->
    <!-- If your app derives location information from
         Wi-Fi APIs, don't include the "usesPermissionFlags"
         attribute. -->
    <uses-permission
        android:name="android.permission.NEARBY_WIFI_DEVICES"
        android:usesPermissionFlags="neverForLocation"
        tools:targetApi="s" />
    <!-- If any feature in your app relies on
         precise location information, don't include the
         "maxSdkVersion" attribute. -->
    <!--  Android12获取SSID需要 ACCESS_FINE_LOCATION 权限  -->
    <uses-permission
        android:name="android.permission.ACCESS_FINE_LOCATION"
        android:maxSdkVersion="32" />
    <uses-permission
        android:name="android.permission.READ_EXTERNAL_STORAGE"
        android:maxSdkVersion="32" />

3. 开启热点

APUtil.kt

Kotlin 复制代码
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)
            }
        }
    }
}

4. 开启HTTP服务

使用 nanohttpd 实现

HttpFileServer.kt

Kotlin 复制代码
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 实现

注意:使用的是sparkjava,而不是大数据分析的那个Spark!

Kotlin 复制代码
// 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
}

5. 使用 telnet

WifiUtil.kt

telnet 主要用到了 WifiUtil 的一个属性:network

Kotlin 复制代码
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)}"
    }
}

TelnetUtil.kt

Kotlin 复制代码
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()
        }
    }
}

6. 解压缩文件

参考这篇文章:Android解压 zip rar 7z 文件

相关推荐
拭心2 小时前
Google 提供的 Android 端上大模型组件:MediaPipe LLM 介绍
android
njnu@liyong4 小时前
图解HTTP-HTTP报文
网络协议·计算机网络·http
ZachOn1y5 小时前
计算机网络:应用层 —— 应用层概述
计算机网络·http·https·应用层·dns
带电的小王5 小时前
WhisperKit: Android 端测试 Whisper -- Android手机(Qualcomm GPU)部署音频大模型
android·智能手机·whisper·qualcomm
梦想平凡5 小时前
PHP 微信棋牌开发全解析:高级教程
android·数据库·oracle
元争栈道5 小时前
webview和H5来实现的android短视频(短剧)音视频播放依赖控件
android·音视频
kaixin_learn_qt_ing6 小时前
了解RPC
网络·网络协议·rpc
阿甘知识库6 小时前
宝塔面板跨服务器数据同步教程:双机备份零停机
android·运维·服务器·备份·同步·宝塔面板·建站
元争栈道7 小时前
webview+H5来实现的android短视频(短剧)音视频播放依赖控件资源
android·音视频
MuYe7 小时前
Android Hook - 动态加载so库
android