31 · WLAN 开关与自动连接
核心文件:
controller/WifiSwitchController.kt(总开关,122 行)、controller/WifiConnectionStateController.kt(连接主页入口文案,128 行)、controller/MiCarWifiAutoConnectController.kt(详情页自动连接开关,59 行)、MiCarWifiUtils.kt(工具门面,178 行)、WifiExt.kt(3 个谓词扩展) 代码基线:/home/zbc/car/fuben_micarsettings/MiCarSettings@ dev,f3 flavor(app 层跨 flavor 共享;file:line 均相对仓库根) 关联:10-基础概念、21-WifiTrackerLib 架构(开关回调用到的 tracker 链路在彼处展开)
0. 全景:三个”开关”各管一摊
小白最容易混的三个东西,先分清:
| 名字 | 长在哪 | 管什么 | 类 |
|---|---|---|---|
| WLAN 总开关 | WLAN 列表页第一行 | 整个 WiFi STA 的开/关(setWifiEnabled) | WifiSwitchController |
| 连接主页 WLAN 入口右侧文案 | 连接主页”WLAN”条目右侧 | 只读显示(已关闭/已开启/SSID),点击跳列表页 | WifiConnectionStateController |
| 自动连接开关 | 某网络详情页 | 该网络的 auto-join(allowAutojoin) | MiCarWifiAutoConnectController |
没有的东西(全仓库已核实):没有”开机自动连已保存网络”的 App 层逻辑(那是 framework WifiService 的 auto-join 评分体系[待验证:framework 侧细节]),没有”信号最优切换”,没有”开热点自动关 WLAN”联动。
1. WLAN 总开关:WifiSwitchController
1.1 控件与 XML
- 控件是
MiCarNewSwitchPreference(base/settingsBaseUi/.../preferences/MiCarNewSwitchPreference.kt:31),不是蓝牙那套 MiCarConnectSwitch。 - XML:
settingsPage/micarConnectionSettings/src/main/res/xml/miauto_wifi_list_fragment.xml:7-11,key=pk_wifi_settings_entry,title=wifi_entry_title(zh “WLAN”,values-zh-rCN:6)。
1.2 读:一次性同步 + 之后纯回调驱动
onCreateInternal同步读一次:mIsWifiEnable = MiCarWifiUtils.isWifiEnable()→preference.setChecked(...)(WifiSwitchController.kt:52-53)。isWifiEnable()(MiCarWifiUtils.kt:164-166)=getWifiState() == WIFI_STATE_ENABLED;getWifiState()(:175-177)=mWifiManager.wifiState(同步 binder 读)。- 之后只有稳态回调才翻面:
onWifiStateChanged(state)(:74-83)仅在WIFI_STATE_ENABLED/DISABLED调updateToggleUi;中间态(ENABLING/DISABLING)不动 checked,只setLoading(!isStateReady)——开关在”变”的过程中显示 loading,防止 UI 抖动。 updateToggleUi(:86-94)有同值短路:mIsWifiEnable == isEnabled直接 return(防回跳)。
1.3 写:子线程 binder + loading 互斥
// WifiSwitchController.handlePreferenceChanged(:101-121)
if (newValue != mIsWifiEnable) {
ThreadPoolUtils.ASYNC.execute { MiCarWifiUtils.setWifiEnable(it) } // 子线程 binder
preference.setLoading(true) // 先 loading, Wifi 回调之后取消
}MiCarWifiUtils.setWifiEnable()(MiCarWifiUtils.kt:171-173)= mWifiManager?.setWifiEnabled(isEnable)——全仓库唯一 setWifiEnabled 调用点(kt/java/XML/反射都查过,§6)。
1.4 完整时序:从手指拨动到开关回位
sequenceDiagram participant U as 用户 participant Sw as MiCarNewSwitchPreference participant Ctrl as WifiSwitchController participant MU as MiCarWifiUtils participant WM as WifiManager(WifiService) participant Trk as WifiPickerTracker participant L as MiWifiTrackerUtils.Listener U->>Sw: onClick Sw->>Sw: 300ms触摸拦截(:251) Sw->>Ctrl: callChangeListener → handlePreferenceChanged:101 Ctrl->>Ctrl: isQuickDoubleClick?(:107) 同值守卫(:112) Ctrl->>MU: (ASYNC) setWifiEnable:171 Ctrl->>Sw: setLoading(true) MU->>WM: setWifiEnabled Note over Sw: 控件乐观翻面(:73-77)<br/>loading 期间再点会被翻回(:66-69) WM-->>Trk: 广播 WIFI_STATE_CHANGED(先 ENABLING 后 ENABLED) Trk->>L: onWifiStateChanged(稳态) L->>Ctrl: onWifiStateChanged:74 Ctrl->>Ctrl: updateToggleUi:86(同值短路) Ctrl->>Sw: setChecked(权威值) + setLoading(false) Ctrl->>Ctrl: updatePreferencesVisibleWhenStatusChange<br/>→ 可用网络分组显隐(§2)
代码调用链(对照上图):
MiCarNewSwitchPreference#onClick:161
→ callChangeListener → WifiSwitchController#handlePreferenceChanged:101
→(ThreadPoolUtils.ASYNC) MiCarWifiUtils#setWifiEnable:171
→ WifiManager.setWifiEnabled ← Binder,真正生效
→(系统广播) WifiPickerTracker#handleWifiStateChangedAction:356-363
→ MiWifiTrackerUtils$mTrackerListener#onWifiStateChanged:45
→ WifiSwitchController#onWifiStateChanged:74
→ updateToggleUi:86 → setChecked + updatePreferencesVisibleWhenStatusChange
(base/settingsBaseUi/.../PreferenceController.java:240)小白关键理解:Preference 层的”权威开关状态”永远等 onWifiStateChanged 稳态回调;Switch 控件本体的即时翻面只是乐观 UI(MiCarNewSwitchPreference.kt:73-77),loading 期间的重复点击会被 isInLoadingStage() 分支翻回(:66-69)。
1.5 三层防抖 + 一层冗余
- 300ms 触摸拦截:
onBindViewHooter给 switch 设 onTouchListener,interval < DOUBLE_CLICK_INTERVAL(300L)直接消费(MiCarNewSwitchPreference.kt:35, 251-258)。 - 业务防双击:
wifiSwitchClickTimestamp.isQuickDoubleClick(...)(WifiSwitchController.kt:39,44,107-111)命中则setChecked(!it)回滚。同一时间戳对象被列表条目点击复用(MiCarBaseWifiEntryController.kt:209-212)——开关和条目互斥防连点。 - loading 互斥:loading 中任何点击回滚。
- 冗余守卫
newValue != mIsWifiEnable(:112)同值不下发。
1.6 availability
getAvailabilityStatus() 无条件 AVAILABLE(:70-72)——无车控信号门控、无用户限制(驾驶行为限制若拦是 UX 基类统一行为,非本控制器逻辑)。
2. 开关 ↔ 可用网络分组的显隐(dependKey 机制)
handlePreferenceChanged 本身不动列表。可用网络分组的显隐走声明式依赖:
- XML 里只有”可用网络”分组声明
settings:dependKey="@string/pk_wifi_settings_entry"(miauto_wifi_list_fragment.xml:24-27); - 反向解析:
BaseXmlParserSettingsFragment.onCreatePreferences→controller.setDependKeyList(...)(base/settingsBaseUi/…/BaseXmlParserSettingsFragment.java:655-657); updatePreferencesVisibleWhenStatusChange(PreferenceController.java:240-263):开→自上而下setVisible(true);关→自下而上收起并listViewScrollUpIfNeed上滚。
列表条目本身的清空走另一条路(§5),两者叠加。
3. 连接主页 WLAN 入口:WifiConnectionStateController
- 不走 tracker,直接注册系统广播
WIFI_STATE_CHANGED_ACTION+NETWORK_STATE_CHANGED_ACTION(:62-65);onCreate 主动刷一次(:58-59,注释:重启后 wifi 未开收不到广播);onResume 再刷(:72-75)。 - 只更新右侧文案
preference.updateRightText(...)(:98);XMLconnection_settings_fragment.xml:40-48,点击带android.settings.WIFI_SETTINGSintent 跳 WLAN 列表页。 - 映射表(:82-102,子线程读 + UI 线程写):
| 条件 | 文案 |
|---|---|
| wifi 关 | wifi_state_off_summary = “已关闭” |
| 开 + Passpoint/OSU AP | passpointProviderFriendlyName |
| 开 + 有效 ssid | ssid 原文(getRealSsid :117-123 剥首尾双引号) |
| 开但拿不到 ssid | wifi_state_on_summary = “已开启” |
4. 详情页「自动连接」开关:MiCarWifiAutoConnectController
- XML:
micar_wifi_entry_detail_fragment.xml:7-11,title zh “自动连接”(values-zh-rCN:108)。 - per-network,不是全局:entry 取自静态单例
MiCarWifiUtils.detailWifiEntry(MiCarWifiUtils.kt:31,由列表页 onDetailBtnClick 赋值)。 - 读:
isAutoJoinEnabled=mTargetWifiConfig.allowAutojoin(StandardWifiEntry.java:806-812)。 - 写链:
MiCarWifiAutoConnectController#handlePreferenceChanged:46
→ StandardWifiEntry#setAutoJoinEnabled:820
→ WifiManager.allowAutojoin(networkId, enabled) ← framework binder- 前提:
canSetAutoJoinEnabled()=isSaved() || isSuggestion()(StandardWifiEntry.java:814-818),不满足时 no-op;entry 为空 return false(:55-57)。 - 联动文案:列表条目 summary 在 auto-join 关闭时追加 “已关闭自动连接”(
Utils.getAutoConnectDescription,WifiTrackerLib f3dif Utils.java:547-556;zh 串wifitrackerlib_auto_connect_disable:21)。
5. 关开关之后:tracker 不销毁,列表四路清空
5.1 tracker 生命周期 = Fragment,不是开关
MiCarWifiListFragment.onCreate 创建 tracker 并 addListener(:74-75),onDestroy 才 removeListener+destroy(quit worker thread,:134-138)。关 WLAN 开关不销毁 tracker,只是扫描停了:BaseWifiTracker.Scanner.onWifiStateChanged(enabled)(BaseWifiTracker.java:1057-1067)wifi 关→stopScanning();shouldScan() = mIsWifiEnabled && mIsStartedState && !mIsScanningDisabled && isInteractive(:1073-1076)。
5.2 清空的四条路(双保险×2)
flowchart TD A[用户关开关] --> B[WifiManager.setWifiEnabled false] B --> C1["①广播 WIFI_STATE_CHANGED<br/>WifiPickerTracker.handleWifiStateChangedAction:356"] C1 --> C2["WIFI_STATE_DISABLED → clearAllWifiEntries:294<br/>(清6个缓存+mNetworkRequestEntry) → updateWifiEntries 通知"] B --> C3["②App门控:MiWifiTrackerUtils.getWifiEntries:105<br/>isWifiEnable()==false 直接返回空list"] C2 --> D["③MiCarBaseWifiEntryController.updateState:103<br/>!isWifiEnable → isVisible=false + removeAll()"] C3 --> D D --> E["④页面tip:MiCarWifiListFragment.onWifiStateChanged:157<br/>DISABLED → 显示wifi-off提示图"] A --> F["⑤dependKey分组折叠(§2)"]
5.3 已知时序坑:瞬态空窗(已修,防复发样板)
MICOCKPIT-146231:从详情页返回时 WifiPickerTracker.handleOnStart 先 entry.clearConnectionInfo 再多波渐进重建(WifiPickerTracker.java:304-354;MIUI 改动把 timeoutScans 置 false :346-349),中间波下发空 list → UI 闪空 63~249ms。
防护 = MiCarBaseWifiEntryController.guardTransientEmpty(:142-164)+ EMPTY_DEFER_MS=500L(:57):
- 空 list 先挂 500ms 延迟复核,期间非空波到达则取消清空;到期仍空才放行真清空;
- Saved/Available 两个子类
updateState(pref, list)首行都调它(MiCarAvailableWifiListController.kt:41-44 / MiCarSavedWifiListController 同位); - 关开关场景显式旁路:
!isWifiEnable()时 guard 直接放行(:155)——关开关是”真清空”,不吃 500ms 延迟。
残留风险[待验证]:开/关中间态只 setLoading 不动 UI,列表最终一致依赖 DISABLED 广播到达;getWifiEntries 每次都 HotspotUtils.refreshHotspotInfo()(反射 getMicarSoftApInfo,MiWifiTrackerUtils.kt:106 + HotspotUtils.kt:112-122)在 SINGLE_ASYNC 线程执行,慢反射理论上可延迟列表刷新。
6. 联动澄清(容易想当然的部分)
- 全仓库
setWifiEnabled唯一调用点 = MiCarWifiUtils.kt:172,唯一上游 = WifiSwitchController.kt:114(ASYNC 线程)。XML/反射无第二处。 - 无任何”开热点关 WLAN”/“WLAN 联动蓝牙”:热点走
MicarWifiManager.startMicarSoftAp/stopMicarSoftAp(HotspotUtils.kt:236-245),只读isWifiApEnabled等,不碰 STA 开关;STA/AP 同 radio 互斥若存在也是 framework 行为[待验证:framework 侧]。 - wifi 模块对蓝牙仅两处”借用”:复用
micar_bluetooth_nopaired_device布局当无设备 tip(MiCarWifiListFragment.kt:85-86)、import 蓝牙包PreferenceAction做刷新动画(MiCarAvailableWifiListController.kt:8, 90-92)。 - 列表会剔除”设备互联热点”:
MiWifiTrackerUtils.isNeedAddInList→HotspotUtils.isHotspot(wifiEntry)(反射getTargetScanResults比对 MicarSoftApInfo 的 BSSID,MiWifiTrackerUtils.kt:122-134 + HotspotUtils.kt:129-147)——车机自己发的热点不出现在 WLAN 列表。
7. WifiExt.kt(23 行,无协程无 Flow)
三个同步谓词(WifiEntry → Boolean):isConnected()(:8-10)、isConnecting()(:14-16)、isOpenNetwork()(:21-23,= SECURITY_NONE || SECURITY_OWE)。消费点:条目高亮/背景(MiCarWifiEntryPreference.kt:101,178)、锁图标显隐(:89)。
8. 信号/安全/状态文案住在哪(重要澄清)
MiCarWifiUtils 里没有 getSignalString/getSecurityString——信号和安全/状态文案全部下沉在 WifiTrackerLib(f3dif):
- 信号:图标档位而非文字。
WifiEntry.getLevel()(WifiEntry.java:382-386)= 优先mWifiInfoLevel否则mScanResultLevel,域 0~4,WIFI_LEVEL_UNREACHABLE=-1(:143-145)。App 侧MiCarWifiEntryPreference.getWifiEntryIcon()(:161-172):已连接无 internet 固定无网 X 档(ICON_NO_NET_INDEX=5);否则level.coerceIn(0,4)设 LevelListDrawable(UNREACHABLE 降 0 格防图标清空,:165-166 注释)。 - 安全类型:
StandardWifiEntry.getSecurityString(:829-832)→Utils.getSecurityString(Utils.java:1359+);zh 映射(EAP→“WPA/WPA2-EAP”、SAE→“WPA3”、PSK→“WPA/WPA2”、OWE→“Enhanced Open”、WEP、Suite-B-192、空→“无”)在 WifiTrackerLib f3dif res values-zh-rCN:57-75。 - 条目副标题:
MiCarWifiEntryPreference.onUpdated()→wifiEntry.getSummary(false)(:187-192)→StandardWifiEntry.getSummary(StandardWifiEntry.java:276-346,” / ” 拼 StringJoiner):CONNECTED 走Utils.getConnectedDescription(Utils.java:278+:已连接/网络连接受限/正在检查互联网访问权限…/无法访问互联网…);CONNECTING 走Utils.getConnectingDescription(:370-384)查 12 态表(arrays.xml:24-36:正在扫描…/正在连接…/正在验证身份…/正在获取 IP 地址…/已暂停/正在断开连接…/已断开连接/失败/已屏蔽/连接状况不佳,暂时停用);DISCONNECTED 走Utils.getDisconnectedDescription(:386+:已保存/请检查密码是否正确,然后重试/IP 配置失败/连接失败/已停用…)。尾缀:auto-join 关→“已关闭自动连接”(:21)、计费→“按流量计费”(:38)。
状态↔文案全表是连接篇(33)与排障篇(40)的基础,3405 案例(错误密码提示缺失)的判定字段就出自
getDisconnectedDescription的认证失败分支。