☰
Android原生WiFi二维码一键连接方案
2026/10/21 12:47:58 网站建设 项目流程

简介:本资源是一份面向Android开发者的实战型工具包,聚焦于“扫描WiFi二维码自动连接”这一高频场景,适用于中初级开发者快速掌握二维码解析、WiFi配置管理与系统权限适配等核心能力。压缩包共141个文件,包含20个Java源码(如MainActivity、CaptureActivityHandler、CameraManager等关键类)、41个编译后class文件、25个UI资源png、13个配置xml及6个依赖jar,完整覆盖从扫码识别、JSON格式WiFi信息解析(SSID/密码/加密类型),到构建WifiConfiguration、调用WifiManager完成连接的全流程实现;另有APK安装包与resources.ap_等构建产物,便于直接运行验证。资源大小2.16MB,结构紧凑,无冗余文件,适合集成学习与二次开发。目前已有2014人学习下载,提供可运行Demo、清晰的权限申请逻辑(含API 23+动态适配)及典型错误处理思路,是理解Android底层WiFi连接机制的优质实践样本。

1. 扫码连 WiFi 不再是“玄学”:一个真正在 Android 设备上跑通的零配置接入方案

你有没有遇到过这样的场景:在会议室投影仪旁,同事递来一张印着 WiFi 二维码的卡片,你掏出手机一扫——结果跳转到浏览器、弹出“无法识别”、或者干脆连上却没网?不是手机不行,是绝大多数扫码工具根本没走 Android 的WifiManager真实连接链路,只是把 SSID 和密码“展示给你看”,让你手动填。而这个项目,就是为了解决这个血泪问题:它用原生 Android API 实现了从「摄像头捕获二维码」→「解析出标准 WiFi 配置字符串(WIFI:S:xxx;T:WPA;P:yyy;;)」→「调用系统级addNetwork()+enableNetwork()」的完整闭环。不依赖任何第三方 SDK,不绕过系统权限模型,不伪造用户点击,所有操作都在前台 Activity 内完成,适配 Android 10~14(含隐私沙盒限制下的NEARBY_WIFI_DEVICES权限处理)。适合嵌入到企业内网自助终端、IoT 设备配网 App、或作为学校/展会现场的免输入 WiFi 接入模块。如果你正被“扫码后连不上”、“Android 12+ 权限拒绝后无提示”、“WPA3 二维码解析失败”卡住,这份代码就是你该立刻下载跑起来的最小可验证体。


2. 项目结构与技术栈:轻量但不妥协的 Android 原生实现

这个项目不是 Demo,而是一个可直接集成进生产 App 的功能模块。它没有引入 ZXing 的庞大 jar 包,而是用 CameraX + ML Kit Barcode Scanning(精简版)做实时扫码;没有用反射黑盒调用隐藏 API,所有 WiFi 连接逻辑都基于WifiManager官方公开接口;更关键的是,它把 Android 各版本的权限演进、网络配置格式兼容、以及连接状态回调的异步陷阱,全部收口在WifiQrConnector这个核心类里。整个工程只有 3 个 Java/Kotlin 文件 + 2 个 XML 布局,无 Gradle 插件依赖,Gradle 版本锁定在 8.2+,AGP 8.3+,目标 SDK 为 34(Android 14),但向下兼容至 API 29(Android 10)。它不追求“一行代码接入”,而是把每一步的 why 和 how 暴露出来——比如为什么startActivityForResult()在 Android 11+ 必须废弃?为什么WifiConfiguration的allowedKeyManagement字段必须按位或设置?这些细节,才是你在自己项目里复现时真正会翻车的地方。

2.1 目录结构与文件职责说明

项目采用扁平化结构,避免过度分包带来的理解成本:

app/ ├── src/main/ │ ├── java/com/example/wifiqr/ │ │ ├── MainActivity.kt # 入口 Activity:申请权限 + 启动扫码界面 │ │ ├── QrScannerActivity.kt # 核心扫码页:CameraX 预览 + ML Kit 解析 + 结果回调 │ │ └── WifiQrConnector.kt # 关键业务类:解析 WiFi QR 字符串 + 构建 WifiConfiguration + 触发连接 │ ├── res/ │ │ ├── layout/activity_qr_scanner.xml # 全屏扫码 UI(带取景框、闪光灯开关) │ │ └── values/strings.xml # 权限提示文案、错误信息等 │ └── AndroidManifest.xml # 声明 CAMERA、INTERNET、ACCESS_FINE_LOCATION(仅 Android < 12)、NEARBY_WIFI_DEVICES(Android 12+)

提示:WifiQrConnector.kt是唯一需要你重点关注和可能二次开发的文件。其他文件属于“胶水层”,负责把用户操作、系统能力、业务逻辑粘合起来。不要试图在QrScannerActivity里写连接逻辑——那是典型的职责错位,后续升级权限模型时你会改到崩溃。

2.2 技术选型依据:为什么不用 ZXing?为什么坚持用 CameraX?

很多开发者第一反应是“用 ZXing + Intent 扫码”,但这条路在 Android 10+ 已经走不通:

  • ZXing 的IntentIntegrator会唤起外部扫码 App(如微信、支付宝),它们返回的只是纯文本,无法获取原始二维码的编码格式元数据,而 WiFi 二维码必须严格区分WIFI:S:...;T:WEP;...和WIFI:S:...;T:WPA;...,类型错误会导致WifiConfiguration构建失败;
  • 更致命的是,外部 App 返回的文本不带上下文,你无法判断它是用户手输的、还是扫码来的——这在需要做防误触校验(比如要求 SSID 长度 ≥ 3)时是黑匣子。

所以本项目选择CameraX + ML Kit Barcode Scanning(Lite 版):

  • CameraX 封装了各厂商相机 HAL 差异,避免Camera1/Camera2双重维护;
  • ML Kit 的BarcodeScanner支持FORMAT_QR_CODE且返回Barcode.rawValue(原始字节流解码后的 String)+Barcode.format(明确标识为 QR_CODE),确保你能拿到未经篡改的、带格式标签的原始内容;
  • 使用play-services-mlkit-barcode-scanning:18.3.0(Lite 版,APK 增量仅 ~800KB),而非 Full 版(~5MB),兼顾体积与能力。

2.3 权限模型演进:从 ACCESS_COARSE_LOCATION 到 NEARBY_WIFI_DEVICES

Android 对 WiFi 扫描和连接的权限管控逐年收紧,本项目做了三段式兼容:

Android 版本所需权限说明
≤ Android 11 (API 30)ACCESS_FINE_LOCATION或ACCESS_COARSE_LOCATION用于扫描周围 WiFi 列表(本项目未用此能力,但系统连接流程内部可能触发)
Android 12 (API 31)NEARBY_WIFI_DEVICES(运行时申请)强制要求,用于调用WifiManager.startScan()和addNetwork();声明在AndroidManifest.xml中,否则addNetwork()直接抛SecurityException
Android 13+ (API 33)NEARBY_WIFI_DEVICES+POST_NOTIFICATIONS(仅当需后台连接提示)本项目默认在前台连接,故POST_NOTIFICATIONS非必需

关键代码在MainActivity.kt中的权限申请逻辑:

// Android 12+ 必须显式申请 NEARBY_WIFI_DEVICES if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.NEARBY_WIFI_DEVICES) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions( this, arrayOf(Manifest.permission.NEARBY_WIFI_DEVICES), REQUEST_CODE_NEARBY_WIFI_PERMISSION ) } else { startQrScanner() } } else { // Android 11 及以下,LOCATION 权限已足够 if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions( this, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), REQUEST_CODE_LOCATION_PERMISSION ) } else { startQrScanner() } }

注意:NEARBY_WIFI_DEVICES是普通权限(normal permission),但需在AndroidManifest.xml中声明,且用户可在设置中单独关闭。如果用户拒绝,WifiManager.addNetwork()会静默失败(返回 -1),不会抛异常——这是你必须主动检查返回值的原因,下文会详解。


3. 核心模块拆解:WifiQrConnector如何把一串字符串变成已连接状态

WifiQrConnector是整个项目的灵魂。它不处理摄像头、不画 UI、不申请权限,只做一件事:把符合 WiFi 二维码规范的字符串,转换成系统可执行的网络配置,并驱动连接流程。它的设计原则是“单职责、可测试、易替换”。你可以把它抽成独立 module,也可以直接 copy 到你现有项目里。下面逐段拆解其核心逻辑。

3.1 WiFi 二维码字符串解析:不只是正则匹配

WiFi 二维码遵循 WiFi Alliance 官方规范 ,格式为:
WIFI:S:<SSID>;T:<WEP|WPA|WPA2|WPA3>;P:<PASSWORD>;H:<true|false>;;

注意末尾两个分号;;是必须的,且字段顺序不固定(H可能出现在P前)。很多开源实现用简单正则WIFI:S:(.*?);T:(.*?);P:(.*?);;,这在SSID含分号(如Office;Guest)或PASSWORD含分号时必然崩溃。

本项目采用状态机式解析,安全可靠:

fun parseWifiQrContent(qrContent: String): WifiConfig? { if (!qrContent.startsWith("WIFI:")) return null val fields = qrContent.substring(5).split(";") // 去掉 "WIFI:" 前缀 var ssid: String? = null var type: String? = null var password: String? = null var hidden: Boolean = false for (field in fields) { if (field.isBlank()) continue val parts = field.split(":", limit = 2) // 严格切分一次,避免密码里冒号干扰 if (parts.size != 2) continue val key = parts[0].uppercase(Locale.getDefault()) val value = parts[1] when (key) { "S" -> ssid = value "T" -> type = value.uppercase(Locale.getDefault()) "P" -> password = value "H" -> hidden = value.lowercase(Locale.getDefault()) == "true" } } // 必填字段校验 if (ssid.isNullOrEmpty() || type.isNullOrEmpty()) return null if (type !in listOf("WEP", "WPA", "WPA2", "WPA3")) return null return WifiConfig(ssid, type, password ?: "", hidden) }

逻辑说明:limit = 2是关键,它确保P:my;pass;word被正确解析为value = "my;pass;word",而不是被第一个分号截断。WifiConfig是一个 data class,仅承载数据,不包含任何业务逻辑。

3.2 构建WifiConfiguration:Android 各版本的 keyManagement 适配

WifiConfiguration是 Android 连接 WiFi 的核心载体。不同加密类型对应不同的allowedKeyManagement位标志,且 Android 版本间有差异:

加密类型Android ≤ 11 (allowedKeyManagement)Android 12+ (allowedKeyManagement)说明
WEPKeyMgmt.WEP_PSKKeyMgmt.WEP_PSK无变化
WPA/WPA2KeyMgmt.WPA_PSKKeyMgmt.WPA_PSK无变化
WPA3KeyMgmt.WPA_PSK(不支持)KeyMgmt.SAE(需 API 30+)WPA3-SAE 是 Android 11 引入,但addNetwork()在 12+ 才稳定支持

本项目在WifiQrConnector.kt中做了精准适配:

private fun buildWifiConfig(config: WifiConfig): WifiConfiguration? { val wifiConfig = WifiConfiguration().apply { SSID = "\"" + config.ssid + "\"" // 必须加双引号,否则含空格 SSID 失败 BSSID = null hiddenSSID = config.hidden } when (config.type) { "WEP" -> { wifiConfig.wepKeys[0] = "\"" + config.password + "\"" wifiConfig.wepTxKeyIndex = 0 wifiConfig.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WEP_PSK) wifiConfig.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN) } "WPA", "WPA2" -> { wifiConfig.preSharedKey = "\"" + config.password + "\"" wifiConfig.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK) } "WPA3" -> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { wifiConfig.preSharedKey = "\"" + config.password + "\"" wifiConfig.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.SAE) } else { Log.w(TAG, "WPA3 not supported on API < 30") return null } } else -> return null } return wifiConfig }

参数说明:preSharedKey和wepKeys[0]必须用双引号包裹密码,这是 Android 系统硬性要求。若密码为12345678,传"12345678"会失败,必须传""12345678""(即字符串内含双引号)。这是无数人踩坑的“玄学点”。

3.3 触发连接:addNetwork()+enableNetwork()的原子性保障

很多人以为调用addNetwork()就完事了,其实这是最大误区。addNetwork()只是把配置存入系统数据库,返回一个networkId(int),它不触发连接。必须紧接着调用enableNetwork(networkId, true),并监听WifiManager.NETWORK_STATE_CHANGED_ACTION广播,才能确认是否真正连上。

本项目将这两步封装为原子操作,并加入超时与失败重试:

fun connect(context: Context, config: WifiConfig, callback: (Result<Boolean>) -> Unit) { val wifiManager = context.getSystemService(Context.WIFI_SERVICE) as WifiManager val wifiConfig = buildWifiConfig(config) ?: run { callback(Result.failure(Exception("Invalid WiFi config"))) return } val networkId = wifiManager.addNetwork(wifiConfig) if (networkId == -1) { // addNetwork 失败:常见于权限被拒、配置非法、或系统限制(如企业设备策略) callback(Result.failure(Exception("addNetwork failed, networkId = $networkId"))) return } // 清除其他已启用网络,避免冲突(可选,根据业务需求开启) wifiManager.configuredNetworks.forEach { existing -> if (existing.networkId != networkId && existing.status == WifiConfiguration.Status.ENABLED) { wifiManager.disableNetwork(existing.networkId) } } // 启用新网络 val enableSuccess = wifiManager.enableNetwork(networkId, true) if (!enableSuccess) { callback(Result.failure(Exception("enableNetwork failed"))) return } // 注册一次性广播接收器监听连接结果 val filter = IntentFilter(WifiManager.NETWORK_STATE_CHANGED_ACTION) val receiver = object : BroadcastReceiver() { override fun onReceive(ctx: Context?, intent: Intent?) { val state = intent?.getParcelableExtra<NetworkInfo.State>( WifiManager.EXTRA_NETWORK_INFO ) if (state == NetworkInfo.State.CONNECTED) { callback(Result.success(true)) } else if (state == NetworkInfo.State.DISCONNECTED) { callback(Result.failure(Exception("Connected but then disconnected"))) } } } context.registerReceiver(receiver, filter) // 30秒超时自动清理 Handler(Looper.getMainLooper()).postDelayed({ context.unregisterReceiver(receiver) callback(Result.failure(Exception("Timeout waiting for connection"))) }, 30_000) }

逻辑说明:enableNetwork(networkId, true)的第二个参数true表示“断开当前连接,立即切换到此网络”,这是实现“扫码即连”的关键。若设为false,系统只会尝试连接,但不主动断开旧网络,可能导致连接卡在“获取 IP”阶段。


4. 常见问题排查:那些让你怀疑人生的真实翻车现场

别笑,下面这些全是某开发者在真实项目中连续 debug 17 小时后记下的血泪笔记。它们不是理论假设,而是你明天就可能遇到的“为什么明明扫码成功了却连不上”的具体原因。

4.1 现象:扫码返回WIFI:S:MyNet;T:WPA;P:12345678;;,但addNetwork()返回 -1

原因:Android 12+ 未申请NEARBY_WIFI_DEVICES权限,或用户在设置中手动关闭了该权限。addNetwork()在权限缺失时静默返回 -1,不抛异常,日志里也无 warning。
解决:在调用addNetwork()前,先检查ContextCompat.checkSelfPermission(context, Manifest.permission.NEARBY_WIFI_DEVICES) == PackageManager.PERMISSION_GRANTED。若不满足,弹窗引导用户去设置页开启:“请前往 设置 > 应用 > 你的App > 权限 > 附近设备,开启权限”。

4.2 现象:连接后WifiManager.getConnectionInfo().getSSID()返回"\"MyNet\""(带双引号)

原因:WifiConfiguration.SSID字段在addNetwork()时必须用双引号包裹(如"\"MyNet\""),但getConnectionInfo().getSSID()返回的也是带引号的字符串。很多开发者直接拿这个值去比对if (ssid == "MyNet"),永远为 false。
解决:统一用ssid.replace("\"", "")去除引号后再比较。更好的做法是,在WifiQrConnector中保存原始config.ssid,连接成功后回调时传出去,避免依赖getConnectionInfo()。

4.3 现象:Android 13 设备上,扫码后连接成功,但 2 秒后自动断开

原因:Android 13 引入了更严格的后台网络限制。如果QrScannerActivity在连接过程中被系统回收(如用户按 Home 键),BroadcastReceiver会被销毁,导致连接状态监听中断,系统认为连接异常而主动断开。
解决:在AndroidManifest.xml中为QrScannerActivity添加android:exported="false"和android:launchMode="singleTop",并在onNewIntent()中恢复扫码状态。更重要的是,连接过程必须在前台完成——禁止在 Service 或 WorkManager 中调用enableNetwork()。

4.4 现象:WPA3 二维码扫码后,buildWifiConfig()中KeyMgmt.SAE分支未进入,日志显示 “WPA3 not supported on API < 30”

原因:设备实际是 Android 12(API 31),但项目targetSdkVersion仍为 29(Android 10),导致Build.VERSION.SDK_INT编译时被截断为 29。SAE常量在 API 30+ 才定义,低 targetSdk 下编译不通过。
解决:将app/build.gradle中的targetSdkVersion升级至 31+,并同步更新compileSdkVersion。这是硬性要求,不是可选项。

4.5 现象:扫码成功,connect()回调Result.success(true),但ping www.baidu.com超时

原因:WiFi 连接成功 ≠ 网络可用。可能是路由器未分配 IP(DHCP 故障)、DNS 不可达、或企业网络需 Portal 认证(本项目不处理)。NETWORK_STATE_CHANGED_ACTION只表示链路层连上,不保证应用层可达。
解决:连接成功后,增加网络连通性验证:

fun checkInternetConnectivity(context: Context): Boolean { val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val network = connectivityManager.activeNetwork ?: return false val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) }

NET_CAPABILITY_VALIDATED表示 DNS 可达且能访问互联网,这才是真正的“能上网”。


5. 二次开发指南:如何把它变成你项目的“一键配网”模块

现在你已经跑通了基础流程,下一步是把它真正融入你的产品。我不会教你“怎么改包名”,而是分享三个真实场景下的改造技巧,每个都来自某高校实验室的 IoT 配网项目落地经验。

5.1 场景一:企业内网 WiFi 需要静态 IP 和 DNS(非 DHCP)

很多工业设备配网要求固定 IP(如192.168.1.100/24)、指定网关(192.168.1.1)、DNS(114.114.114.114)。原生WifiConfiguration支持,但需手动设置ipAssignment和linkProperties:

// 在 buildWifiConfig() 中,WPA/WPA2 分支后追加 if (config.staticIp != null) { wifiConfig.ipAssignment = WifiConfiguration.IpAssignment.STATIC val linkProperties = LinkProperties() linkProperties.setIpAddress(InetAddress.getByName(config.staticIp), 24) // /24 子网掩码 linkProperties.setGateway(InetAddress.getByName(config.gateway)) linkProperties.setDnsServer(InetAddress.getByName(config.dns)) wifiConfig.linkProperties = linkProperties }

注意:staticIp、gateway、dns需从二维码扩展字段解析,例如约定WIFI:...;I:192.168.1.100;G:192.168.1.1;D:114.114.114.114;;。你只需在parseWifiQrContent()中增加对I/G/D字段的支持即可。

5.2 场景二:连接失败时,自动 fallback 到热点配网(SoftAP)

当扫描的 WiFi 不存在或密码错误时,与其让用户重扫,不如启动设备的 SoftAP 模式,让手机直连设备热点进行配置。这需要你控制设备端固件,但 App 端只需两步:

  1. 在connect()的失败回调中,启动一个Intent跳转到你自定义的ApConfigActivity;
  2. ApConfigActivity中用WifiManager扫描名为MyDevice_AP的热点,并连接它(此时你已知密码,无需扫码)。

关键代码(在WifiQrConnector.kt失败分支中):

callback(Result.failure(exception)) { // 启动热点配网流程 val intent = Intent(context, ApConfigActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK context.startActivity(intent) }

5.3 场景三:记录连接历史,支持“最近连接的 5 个 WiFi”快捷切换

用户常需在多个 WiFi 间切换(如家、公司、咖啡馆)。你可以在connect()成功后,将config.ssid和时间戳存入SharedPreferences:

private fun saveToHistory(context: Context, ssid: String) { val prefs = context.getSharedPreferences("wifi_history", Context.MODE_PRIVATE) val editor = prefs.edit() val history = prefs.getStringSet("history", mutableSetOf()) ?: mutableSetOf() history.remove(ssid) // 移除旧位置 history.add(ssid) // 添加到末尾 // 只保留最近 5 个 if (history.size > 5) { val list = history.toList() history.clear() history.addAll(list.subList(1, 5)) } editor.putStringSet("history", history) editor.apply() }

然后在MainActivity中提供一个RecyclerView展示history,点击即调用WifiQrConnector.connect()—— 此时你已知 SSID,只需弹窗让用户输入密码,或从历史记录中读取(若之前存过)。

从那以后我每次做 IoT 配网模块,都会在WifiQrConnector里预留staticIp、apFallback、historySave三个 flag 参数,哪怕当前项目用不到。因为我知道,客户第二周就会提“能不能配静态 IP”、“设备连不上时能不能直连配网”、“能不能记住上次连的 WiFi”。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询