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/DISABLEDupdateToggleUi;中间态(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 三层防抖 + 一层冗余

  1. 300ms 触摸拦截:onBindViewHooter 给 switch 设 onTouchListener,interval < DOUBLE_CLICK_INTERVAL(300L) 直接消费(MiCarNewSwitchPreference.kt:35, 251-258)。
  2. 业务防双击:wifiSwitchClickTimestamp.isQuickDoubleClick(...)(WifiSwitchController.kt:39,44,107-111)命中则 setChecked(!it) 回滚。同一时间戳对象被列表条目点击复用(MiCarBaseWifiEntryController.kt:209-212)——开关和条目互斥防连点。
  3. loading 互斥:loading 中任何点击回滚。
  4. 冗余守卫 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.onCreatePreferencescontroller.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);XML connection_settings_fragment.xml:40-48,点击带 android.settings.WIFI_SETTINGS intent 跳 WLAN 列表页。
  • 映射表(:82-102,子线程读 + UI 线程写):
条件文案
wifi 关wifi_state_off_summary = “已关闭”
开 + Passpoint/OSU APpasspointProviderFriendlyName
开 + 有效 ssidssid 原文(getRealSsid :117-123 剥首尾双引号)
开但拿不到 ssidwifi_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.handleOnStartentry.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.isNeedAddInListHotspotUtils.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 的认证失败分支。