
简介面向 Android 开发者的 WiFi 二维码自动连接实现资料包围绕扫码识别、配置解析、系统 WiFi API 调用与权限处理四大环节展示从摄像头取流到自动连网的完整实现路径。除讲解 ZXing 二维码扫描的接入方式、WifiConfiguration 对象的构造细节外还针对 Android 6.0 以上 WRITE_SETTINGS 动态申请、SSID 双引号包裹、预共享密钥类型设置等常见坑点给出可直接套用的代码片段。压缩包共 141 个文件大小 2.16MB以 Java 源码、class 字节码、XML 布局、PNG 图片为主另含 jar 依赖库及可直接安装的 APK 成品便于读者边读源码边验证效果。资料包目录结构清晰关键类与布局文件一一对应二次开发时能快速定位修改点。资源已有 2011 人学习下载适合具备一定 Android 基础、希望快速实现扫码连 WiFi 功能的开发者参考使用也可作为相关课程设计的落地素材。 这段时间在做一个Android工具类应用其中有个功能是“扫描WiFi二维码自动连接”。听起来不就是调个扫码库、解析一下字符串、再调一下WifiManager嘛结果真做起来才发现光是Android版本碎片化这一关就够喝一壶的。从Android 6的运行时权限到Android 10对WiFi API的颠覆性调整再到Android 13的通知权限限制每一点都能让你在真机上翻车。这篇文章就完整记录一下我的设计和实现过程包括方案选型、踩坑记录、兼容性处理思路还有一些常规文档里不会写的细节。做成一个可以直接照抄的工程实践笔记给同样在做类似功能的朋友一个参考。1. 项目整体设计与思路拆解1.1 核心需求解析这个功能的核心流程并不复杂扫码 - 解析WiFi信息 - 发起连接 - 连接成功回调。但拆开来看每一环都藏着不少分支。二维码内容是什么格式不是所有二维码都是WiFi配置码可能是普通文本、网址甚至是一段JSON需要先识别再过滤。解析出来之后怎么连接调用WifiManager.addNetwork还是WifiNetworkSuggestion不同安卓版本API差异巨大。连接成功后如何反馈是弹Toast、静默重连、还是跳转系统WiFi设置页这涉及应用场景的预期行为。当前设备是否已连接其他WiFi要不要主动断开当前连接这是策略问题不是技术问题。第一版需求提到的应用场景是访客网络接入和门店扫码连WiFi。也就是说用户扫码之后期望的是“不需要手动输入密码、不需要跳转设置页、尽可能全自动完成连接”。这个预期对API版本适配提出了比较高的要求。1.2 方案选型为什么是ZXing 原生WiFi API二维码识别这块主流方案有ZXing、ZBar、ML Kit、OpenCV自绘等。我最终选择了ZXing原因有三点第一ZXing是纯Java/Kotlin实现体积可控不需要依赖Google Play Services国内环境友好第二它在低分辨率、暗光、倾斜场景下的识别率实测不错对扫码场景足够第三它的核心库是core模块我只需要QRCodeReader来解析Bitmap不需要引入整个Barcode Scanner应用。WiFi连接部分我没有选第三方封装库而是直接用Android原生API。市面上封装的库大多只是对addNetwork的简单包装对Android 10的新特性支持普遍滞后。与其依赖维护不活跃的库不如自己写好状态机管理。1.3 版本碎片化绕不开的兼容性设计这是整个项目里最坑的地方。Android 10API 29之前WifiManager.addNetworkenableNetwork是通用做法一次调用就能让系统记住并连接指定WiFi。Android 10之后addNetwork被标记为deprecated而且部分厂商ROM直接禁用了它的效果你调用不报错但系统就是不连。Android 10引入了WifiNetworkSuggestion通过WifiManager.addNetworkSuggestions提交网络建议系统会在适当时候自动连接。这套API从设计理念上更安全但它的连接是“异步且不一定立即生效”的完全不像老API那样指哪打哪。再加上Android 12API 31对suggestions的连接回调方式做了调整Android 13API 33又要求连接WiFi必须申请NEARBY_WIFI_DEVICES权限。所以兼容层必须按三套逻辑来写系统版本连接方案定位权限要求备注Android 8.0 - 9.0addNetwork enableNetwork需要传统方案稳定但已被废弃Android 10 - 11WifiNetworkSuggestion需要系统建议机制回调体验一般Android 12 - 13WifiNetworkSuggestion 新权限模型需要Android 13需NEARBY_WIFI_DEVICES2. 核心细节解析与关键技术点2.1 WiFi二维码格式先读懂协议再动手WiFi二维码的标准格式实际上是公开的就是那种“WIFI:T:WPA;S:mynetwork;P:mypass;;”的字符串。在动手写解析代码前有必要完整理解这个格式。T认证类型取值有WEP、WPA、WPA2、WPA3、nopass开放网络。SSSID网络名称。P密码。H是否隐藏SSIDtrue或false可选项。I身份标识用于企业级802.1x网络普通场景很少见。解析时最容易翻车的是特殊字符。如果SSID或密码里包含;、:、\、,等符号标准规定需要用反斜杠转义。但实际扫码时很多路由器生成的二维码根本不做转义导致直接按;切分字符串会出错。我这里用的解析方案是逐个字段扫描而不是split(;)遇到\就跳过下一个字符这样才能兼容不规范来源的二维码。另外中文字符的SSID在实际解析中也经常会出问题。二维码内容本身是UTF-8编码但部分老设备生成的二维码会用GBK编码。稳妥做法是尝试用UTF-8解析失败后再尝试GBK兜底。注意解析二维码时一定要先判断是不是WIFI:开头不是就直接提示“不是WiFi二维码”避免把普通文本或网址内容拿去解析造成误报。2.2 权限体系从Android 6到Android 13的变化WiFi相关的权限是碎片化重灾区。直接列一下实测结论Android 6.0 - 8.0需要ACCESS_FINE_LOCATION或ACCESS_COARSE_LOCATION才能调用WiFi相关的扫描和连接API这是Google为了防隐私追踪强行加的限制。你只是连个WiFi系统却认为是隐私操作。Android 8.0 - 9.0需要ACCESS_LOCATION_EXTRA_COMMANDS这属于特殊权限一般应用拿不到不过实测addNetwork流程不强制要求。Android 10 - 12ACCESS_FINE_LOCATION依然是硬门槛。这里有个经典坑——用户如果只授予模糊定位权限getScanResults()会返回空列表。必须主动判断checkSelfPermission拿到的到底是精确还是模糊权限。Android 13新增了NEARBY_WIFI_DEVICES附近设备权限而且这个权限属于neverForLocation类型好在你不需要同时申请位置权限来用它。但要注意的是如果你的应用targetSdk升到33旧的位置权限声明方式在新版本上会失效两个权限体系互相交织容易出现在不同手机上表现不一致的情况。我的处理方式是做一个权限入口页一次性把该申请的权限列出并说明每项权限的作用。让用户在开始扫码前就完成授权后续流程就会顺畅很多。2.3 连接APIaddNetwork已死NetworkSuggestion当立在Android 10及以上版本强烈建议不要再走addNetwork老路。虽然有些国产ROM上它还能用但Google在新版本上已经做了限制部分设备会静默失败且没有任何异常抛出非常难排查。WifiNetworkSuggestion的使用逻辑是这样的你把待连接网络的SSID和密码封装成WifiNetworkSuggestion对象通过addNetworkSuggestions提交给系统。系统会在合适的时机进行评估如果该网络信号够好、优先级够高就会自动连接。连接状态通过WifiManager.NETWORK_STATE_CHANGED_ACTION广播来感知。需要特别说明的是suggestion机制不是“立即连接”它的连接时机由系统决定。实测中提交建议后通常5秒内会开始连接但如果是信号较弱或者多个WiFi源并存的环境这个时间会拉长。好在现在的Android系统对suggestion的响应速度已经比刚推出时快多了。Android 12之后还可以给suggestion设置优先级。WifiNetworkSuggestion.Builder提供了setPriority方法数值越大优先级越高。如果你在同一个区域维护了多个WiFi热点这个接口很实用可以用来实现“优先连主网络主网络挂了再连备用”的策略。3. 实操过程与核心代码实现3.1 工程配置与依赖引入我用的开发环境是Android Studio Koala版本Gradle 8.6Kotlin 1.9.22minSdk 26targetSdk 33。在build.gradle里加入ZXing依赖dependencies { // ZXing核心库只需core模块不需要引入整个扫码App implementation com.google.zxing:core:3.5.3 }如果你不想自己处理相机预览、对焦、光线检测等逻辑也可以直接用zxing:android-embedded这个封装库它自带扫码界面接入更快。但如果你想做深度定制比如界面风格统一、扫码框样式调整、支持从相册选图识别那还是只引核心库自己控制整个流程更划算。我这个项目因为要同时支持扫码枪扫的二维码和手机扫码所以选择了只引core模块。3.2 扫描模块与二维码解析扫码预览部分我用的是Camera2 API配一个自绘的扫码框View。整个预览和实时识别流程逻辑如下class QrCodeAnalyzer(private val onResult: (String) - Unit) : ImageAnalysis.Analyzer { private val reader QRCodeReader() override fun analyze(image: ImageProxy) { val bitmap image.toBitmap() // 需要把YUV_420_888转成RGB位图 val source RGBLuminanceSource(bitmap.width, bitmap.height, getPixels(bitmap)) val binaryBitmap BinaryBitmap(HybridBinarizer(source)) return try { val result reader.decode(binaryBitmap) onResult(result.text) } catch (e: Exception) { // 识别失败不需要处理下一帧继续 } finally { image.close() } } }这里有个性能细节RGBLuminanceSource内部会把Bitmap像素转成亮度数组如果每帧都做一次全量转换高分辨率下CPU开销会非常大。我的做法是把分析帧的尺寸压到720p以下并且ImageAnalysis的STRATEGY_KEEP_ONLY_LATEST模式保证同一时间只处理最新帧丢弃积压旧帧。实测在720p下ZXing识别速度完全够用基本感觉不到延迟。解码成功后会拿到字符串比如WIFI:T:WPA;S:MyHomeWiFi;P:abcd1234;;接下来进入解析器。简单版解析可以用正则但考虑到上面说的转义问题我建议自己写扫描器data class WifiQrInfo( val authType: String, val ssid: String, val password: String, val isHidden: Boolean false ) fun parseWifiQrCode(content: String): WifiQrInfo? { if (!content.startsWith(WIFI:)) return null var authType nopass var ssid var password var hidden false var i 5 while (i content.length) { if (i 1 content.length) break val key content[i] val separator content[i 1] if (separator ! :) { i; continue } i 2 val valueBuilder StringBuilder() while (i content.length content[i] ! ;) { if (content[i] \\ i 1 content.length) { // 处理转义字符 valueBuilder.append(content[i 1]) i 2 } else { valueBuilder.append(content[i]) i } } i // 跳过结尾的分号 when (key) { T - authType valueBuilder.toString() S - ssid valueBuilder.toString() P - password valueBuilder.toString() H - hidden valueBuilder.toString().toBoolean() } } return WifiQrInfo(authType, ssid, password, hidden) }这个解析器有几个关键点一是手工遍历而不是split(;)能正确处理包含分号的SSID二是遇到\符号会跳过转义直接读取下一个字符三是对格式不完整的二维码也不会闪退最多返回null交给上层提示用户。3.3 自动连接逻辑与多版本兼容连接逻辑是核心中的核心。我封装了一个WifiConnector对外只暴露一个方法suspend fun connect(ssid: String, password: String, authType: String): Boolean内部根据Build.VERSION.SDK_INT决定走哪条路if (Build.VERSION.SDK_INT Build.VERSION_CODES.Q) { connectWithSuggestion(ssid, password) } else { connectWithAddNetwork(ssid, password, authType) }老版本方案private fun connectWithAddNetwork(ssid: String, password: String, authType: String): Boolean { val wifiManager applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager // 先移除同SSID的旧配置避免重复添加 val existingNetworks wifiManager.configuredNetworks existingNetworks?.filter { it.SSID \$ssid\ }?.forEach { wifiManager.removeNetwork(it.networkId) } val config WifiConfiguration().apply { this.SSID \$ssid\ when (authType) { WPA, WPA2, WPA3 - { this.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK) this.preSharedKey \$password\ } WEP - { this.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE) this.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN) this.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.SHARED) this.wepKeys[0] \$password\ this.wepTxKeyIndex 0 } nopass - { this.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE) } } } val networkId wifiManager.addNetwork(config) if (networkId -1) return false return wifiManager.enableNetwork(networkId, true) }这里有一段很重要的代码连接前先移除同SSID的旧配置。如果不移除重复添加会让系统记住多个同名校验配置部分ROM会随机选择导致“明明是正确密码但连不上”的诡异问题。Android 10方案private fun connectWithSuggestion(ssid: String, password: String): Boolean { val wifiManager applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager val suggestion WifiNetworkSuggestion.Builder() .setSsid(ssid) .setWpa2Passphrase(password) .build() val result wifiManager.addNetworkSuggestions(listOf(suggestion)) return result WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS }suggestion方案有个坑同一个应用对同一个SSID重复提交建议第二次提交会失败。所以每次连接前应该先调用removeNetworkSuggestions清掉旧建议再添加新建议wifiManager.removeNetworkSuggestions(listOf(suggestion)) // 参数可为空列表 wifiManager.addNetworkSuggestions(listOf(suggestion))另一个坑是WifiNetworkSuggestion类的构造方法里setWpa2Passphrase和setWpa3Passphrase不能同时调用系统会认为参数非法。所以对WPA3网络要用setWpa3Passphrase而不是继续用wpa2的。3.4 连接状态监听连接状态我用了BroadcastReceiver监听WifiManager.NETWORK_STATE_CHANGED_ACTIONprivate val networkStateReceiver object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val wifiInfo intent.getParcelableExtra(WifiManager.EXTRA_WIFI_INFO) val networkInfo intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO) if (networkInfo?.isConnected true wifiInfo?.ssid?.trim() targetSsid) { // 连接成功 onConnected?.invoke() } } }这里有个特别容易踩的坑wifiInfo.ssid并不是任何时候都带双引号。在Android 8到10之间部分ROM返回的SSID是带引号的部分不带。所以比较时一定要.replace(\, )之后再比或者按我上面写的那样用trim()。否则明明已经连上了你的应用却死活认为没连上。再补充一个细节要确保BroadcastReceiver注册的时机正确。我是在onCreate里动态注册onDestroy里注销。不要用静态注册因为WiFi状态广播接收非常频繁很容易在外层被系统拦截或延迟派发。4. 常见问题与排查技巧实录4.1 问题速查表这里把整个开发过程中遇到的高频问题汇总一下按问题现象、原因、解法三列整理问题现象根本原因解决方案扫码后提示“不是WiFi二维码”SSID中包含特殊字符解析器没有做转义处理换成逐字符扫描解析不要用split分割Android 10真机上连接不生效addNetwork被废弃部分ROM返回成功但系统不执行切换为WifiNetworkSuggestion权限已授予但扫描结果为空只授予了模糊定位权限或没有动态申请精确定位判断ACCESS_FINE_LOCATION是否精确授权必要时引导用户去设置页开启精确位置Suggestion重复提交导致连接失败同SSID的suggestion已存在再次add返回ALREADY_EXISTS调用前先removeNetworkSuggestions清空旧数据明明连上了但回调没触发SSID字符串带不带引号不一致统一用replace(\, )后再比较手机息屏后连接中断厂商省电策略杀掉了前台服务和广播接收引导用户加白名单或者提升应用为前台服务Android 13上无法发起连接缺少NEARBY_WIFI_DEVICES权限targetSdk 33及以上必须动态申请附近设备权限WPA3热点连接失败用setWpa2Passphrase设置了wpa3网络判断认证类型后使用setWpa3Passphrase4.2 几个容易忽略的隐藏细节第一Android 12开始WifiManager.getConnectionInfo()返回的SSID和BSSID被脱敏了非系统应用拿到的值可能是unknown ssid。这导致“连接成功后读取当前WiFi名称”这个操作在所有Android 12设备上都会失效。替代方案是通过NetworkCallback监听onAvailable和onCapabilitiesChanged在回调里拿WifiInfo但同样有脱敏限制。如果你的应用需要展示当前连接的是哪个WiFi最好在发起连接前就保存好目标SSID连接成功回调时直接用自己保存的值而不是尝试从系统读取。第二Android 13的NEARBY_WIFI_DEVICES权限和位置权限是互斥的。如果你在manifest里同时声明了ACCESS_FINE_LOCATION和NEARBY_WIFI_DEVICES系统会弹两个权限请求用户如果只授权了其中一个部分API会工作、部分不会。我这里采用的策略是targetSdk 33及以上时只申请NEARBY_WIFI_DEVICES不再申请位置权限因为suggestion方案本身不需要扫描周边热点只需要发起连接。第三如果应用需要在后台自动重连WiFi只是用BroadcastReceiver注册监听是不够的很多国产ROM会直接杀掉后台应用。实测下来把连接逻辑放进前台服务配合FOREGROUND_SERVICE类型才是稳妥方案。Android 14上还需要声明前台服务类型否则运行时直接崩。第四每个WiFi二维码解析后最好记录一下来源。比如是来自相机扫码的、相册图片解析的、还是文本粘贴的。如果是相册解析系统相册返回的图片可能经过了Exif旋转处理需要先读取Orientation信息修正图片方向再送入解码器否则竖排二维码在部分设备上识别率骤降。第五别在onResume里注册WiFi状态监听。我曾经遇到过这样的bug扫码页跳转到系统WiFi设置页授权回来onResume重新注册了接收器导致原来那一个没有注销两个接收器同时触发回调界面状态被来回切换。统一做法是在onCreate注册onDestroy注销不要依赖onResume/onPause来做注册注销。5. 一些经验沉淀与可扩展思路如果你和我一样只需要支持Android 10以上的设备那就干脆别再兼容老API了。addNetwork这条老路在国产ROM上行为不统一每家的实现都有各自的小九九你永远不知道哪个厂商会在哪个版本悄悄改掉行为。写兼容层本来就是为了减小维护成本如果维护本身成了成本那就该做减法了。从我实测的角度看市面上的主流设备小米、华为、OPPO、vivo、荣耀、三星针对WifiNetworkSuggestion的适配已经比较成熟Android 11以上的设备基本都能在提交建议后5秒内自动连接。真正让人头疼的还是那些小众品牌或者深度定制的系统遇到过一次某品牌手机对suggestion完全没有响应的情况最后只能降级跳转系统WiFi设置页让用户自己手动连接。另外补充一个可扩展方向如果要做批量导入WiFi配置比如IT管理员给上百台设备统一配置可以考虑解析一份含有多个WiFi二维码的图片集或者读取配置文件通过suggestion机制一次提交多个网络建议。系统会根据信号强度和优先级自动选择最合适的网络这比逐条调用addNetwork要优雅得多。实测连续提交十几个suggestion不会有性能问题但要注意每个SSID不要重复提交即可。这个功能做完之后我自己最大的体会是扫码解析这块其实没有任何难度ZXing足够成熟难点全在Android系统API的碎片化适配和权限逻辑的细节处理上。希望这篇文章能帮你少走一些弯路。如果你在适配过程中遇到我文中没提到的奇葩问题欢迎在评论区贴出来我这边有复现环境的话可以帮忙一起排查。本文还有配套的精品资源点击获取