30 · WLAN 页面与导航架构
核心文件:
view/WifiSettingsActivity.kt、view/MiCarWifiListFragment.kt、view/MiCarWifiEntryDetailFragment.kt、view/WlanProvisionDialogActivity.kt+MiCarWifiProvisionFragment.kt、networkrequest/NetworkRequestDialog*.java(共 6 文件)、f3dif/MiCarWifiCollinear.kt代码基线:fuben_micarsettings @ dev,f3 flavor;file:line 相对仓库根 关联:全系列的地基篇——后面 31~34 篇的所有 Controller/Fragment 都在本篇的骨架上活动
0. 名词预热(小白版)
- 独立弹窗 Activity(DialogActivity):车机设置的二级页不是 push Fragment 到设置主页,而是单独起一个”长得像弹窗的 Activity”(主题定了 842×864 尺寸),有自己的任务栈。
- taskAffinity:任务栈”亲和度”。
":WifiSettings"(冒号开头)= 私有任务栈,与设置主页不同栈,互不干扰 back 键。 - singleTask / singleInstance:全系统只允许一个实例,重复拉起走
onNewIntent不新建。 - PreferenceScreen / Controller:页面 = XML 偏好项列表 + 一组 Controller(每项一个,取数/刷新/响应点击);
settings:controller="类全名"写在 XML 里由框架反射实例化。 - Provision(开机引导):首开/恢复出厂向导,此时
Settings.Global.DEVICE_PROVISIONED=0。
1. WLAN 页面地图(总览)
flowchart TD HOME["设置主页左侧菜单·连接<br/>miauto_top_level_menu_fragment.xml:81"] --> CONN["ConnectionSettingsTopFragment<br/>connection_settings_fragment.xml"] CONN -->|"蓝牙 :30-37"| BT["BluetoothSettingsActivity<br/>taskAffinity=:BluetoothSettings"] CONN -->|"WLAN :40-48 隐式intent<br/>android.settings.WIFI_SETTINGS"| WIFI["WifiSettingsActivity<br/>taskAffinity=:WifiSettings (842×864弹窗)"] CONN -->|"热点 :51-57"| HS["HotspotSettingActivity"] WIFI -->|"getInitialFragment()"| LIST["MiCarWifiListFragment<br/>(开关+已保存组+可用组)"] LIST -->|"详情按钮(isSaved才有)"| DET["MiCarWifiEntryDetailFragment<br/>(自动连接开关+修改密码/删除)"] WIFI -.->|"未引导且refer≠连接页<br/>WifiSettingsActivity.kt:93-110"| PROV["WlanProvisionDialogActivity<br/>singleInstance 全屏"] PROV --> PF["MiCarWifiProvisionFragment<br/>(单列表,复用controller)"] EXT1["外部 Provision App"] -.->|com.android.car.settings.provision.WIFI_SETTINGS| PROV EXT2["系统 WiFi 服务(第三方App要网)"] -.->|com.android.settings.wifi.action.NETWORK_REQUEST| NR["NetworkRequestDialogActivity<br/>(30s超时,单SSID/列表/错误三形态)"]
要点:WLAN 入口不在蓝牙主页里——蓝牙页与 WLAN 页是平级的两个独立弹窗 Activity,由同一个”连接主页”(住在设置主 Activity 里)分别用隐式 intent 拉起。共用 micar_bluetooth_nopaired_device 布局是资源复用,不是导航嵌套。
2. WifiSettingsActivity:入口承接者
2.1 Manifest(manifest:148-165)
<activity android:name="...WifiSettingsActivity"
android:exported="true" android:launchMode="singleTask"
android:taskAffinity=":WifiSettings" android:theme="@style/MiCarWifiDialog">
<intent-filter android:priority="100">
<action android:name="android.net.wifi.PICK_WIFI_NETWORK"/>
<action android:name="android.settings.WIFI_SETTINGS"/>
<action android:name="android.settings.panel.action.WIFI"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
...
</activity>- exported + 3 系统 action + priority=100(高于 AOSP Settings 同名 filter):任何 App/语音/系统发这三个 action 都解析到它。
- theme=MiCarWifiDialog(styles.xml:36-38)→ parent
MiCarCarSettingsDialog(base/settingsBaseUi themes.xml:48-56),弹窗尺寸 842×864。 windowSoftInputMode=adjustPan(软键盘平移不 resize,配合密码框);meta-data:SINGLE_PANE=true、distractionOptimized=true(行驶中可操作)、disableTaskSnapshot=true(不留快照残影)。
2.2 仓内唯一拉起点:连接主页 XML 内嵌 intent
connection_settings_fragment.xml:40-48:
<com.android.car.settings.miauto.preferences.BasicPreference
android:key="@string/pk_connection_wifi_entry" android:title="@string/wifi_entry_title"
settings:controller="...WifiConnectionStateController">
<intent android:action="android.settings.WIFI_SETTINGS">
<extra android:name="refer" android:value="settings_connection_wifi"/>
</intent>
</com.android.car.settings.BasicPreference>不是代码 startActivity,而是 Preference 内嵌 <intent>,点击由偏好框架隐式发出。refer 用于区分”从连接页进”(Provision 分流要用)。
2.3 Activity 自身职责(WifiSettingsActivity.kt)
- onCreate(:89-121):①未引导且
refer≠SETTINGS_CONNECTION_WIFI→ 转跳 WlanProvisionDialogActivity 并 finish(:93-110);②注册连接失败广播micar.intent.action.WIFI_CONNECTION_FAILURE(:42-56),失败且 SSID 匹配时重弹密码框(详见 33-连接流程 §5)。 - getInitialFragment(:159-161) =
MiCarWifiListFragment();基类DialogBaseCarSettingsActivity#onCreate:180-182→launchIfDifferent。 - onNewIntent(:123-131):singleTask 二次拉起时
popBackStackImmediate(null, POP_BACK_STACK_INCLUSIVE)清栈回列表页(在详情页时也会被拉回)。 - onDestroy(:133-145):反注册广播、dismiss 密码弹窗、清
tryConnectWifiEntry。
2.4 布局是怎么进窗口的:四层注入链(小白重点)
行号本轮在 fuben10_micarsettings @
rls-xcd-u-ee-2603复核;基类结构与 dev 一致(对照 §8 链①的行号可互证)。
新人最常问:“WifiSettingsActivity 全文没有 setContentView,界面哪来的?”答案:布局不是一处设的,是四层接力,每层只管一件事:
第①层 窗口形态(theme) Manifest:148 android:theme="@style/MiCarWifiDialog"
MiCarWifiDialog(styles.xml:36) → MiCarCarSettingsDialog(base themes.xml:49)
→ Theme.MiCar.DayNight.DialogActivity(micarx 库)
决定:842×864 弹窗尺寸(dialogActivityLayoutWidth/Height 属性)、对话框窗口外观
│
第②层 内容容器(setContentView) DialogBaseCarSettingsActivity#onCreate:170
setContentView(R.layout.car_setting_dialog_activity)
⚠️ 该布局全文只有一个空 FrameLayout @+id/dialog_container,连标题栏都没有!
标题栏/关闭按钮由 micarx 库的 BaseDialogActivity 按主题做窗口装饰,不在仓内布局里
│
第③层 页面内容(Fragment 事务) 同一个 onCreate:182
getInitialFragment()(=MiCarWifiListFragment) → launchIfDifferent → launchFragmentInternal(:331)
FragmentTransaction.add(R.id.dialog_container, fragment) + 淡入动画
(二级页 FragmentPageMark.Replace 走 replace + 右进左出动画;一级页走 add + fade_in)
│
第④层 Fragment 根布局(theme 属性,非手写) 继承链 PreferenceFragmentCompat
主题属性 preferenceFragmentCompatStyle=@style/PreferenceFragmentDialogActivity(styles.xml:344)
→ android:layout=@layout/car_ui_preference_fragment_dialog_auto(CarUI 偏好页骨架)
Fragment#onCreateView 再往 android.R.id.list_container 手动 inflate
micar_bluetooth_nopaired_device(WLAN 关闭提示条,图标换 micar_ic_wifi_off_indicator)三个反直觉点(排障必知):
- 骨架与内容分属不同层:标题”WLAN”+关闭键来自第①层窗口装饰、列表骨架来自第④层静态布局,都不依赖 WiFi 数据,所以 0.5s 就能画出来;真正等数据的是第④层里的 Preference 条目。这就是”骨架秒出、内容空窗数秒”类问题(如 MIICOSBUG-2759,见 40-排障速查)的结构根源——看性能问题先分清卡在那一层。
- 找标题栏别在仓里 grep 布局:它是 micarx
BaseDialogActivity的窗口装饰,想改标题栏行为看DialogBaseCarSettingsActivity#updateTitleBar/getMiCarToolbar(:213/:254)这些转发方法。 - 页面切换动画分两套:
launchFragmentInternal按FragmentPageMark接口分流——实现SecondLevelAnim的二级页走右进左出,一级页只 fade_in;Replace接口决定事务用replace还是add(都在 dialog_container 里叠,back 栈逐层弹)。
3. MiCarWifiListFragment:列表页装配
3.1 继承链与 Controller 装配
MiCarWifiListFragment → DialogSettingsFragment → TopLevelSettingsFragment → BaseXmlParserSettingsFragment。Controller 写在 XML 里反射创建(settings:controller,PreferenceControllerListHelper.java:94):
| 顺序 | 节点(miauto_wifi_list_fragment.xml) | key | controller |
|---|---|---|---|
| 1 | MiCarNewSwitchPreference(:7-11) | pk_wifi_settings_entry | WifiSwitchController(总开关) |
| 2 | SpacePreference 8dp(:13-14) | — | — |
| 3 | MiCarPreferenceCategory(:16-19) | pk_wifi_current_conn | MiCarSavedWifiListController(已保存/当前连接组) |
| 4 | SpacePreference 10dp(:21-22) | — | — |
| 5 | CarUIPreferenceCategory(:24-30) | pk_wifi_available_conn,dependKey=总开关 | MiCarAvailableWifiListController(可用组,带头部刷新动画) |
Fragment 代码里显式 use(...) 只有两处(语音态广播,MiCarWifiListFragment.kt:117-132)。
3.2 生命周期逐段
- onCreate(:47-76):①设
PreferenceComparisonCallback(:49-73,同类+title/summary 相同即判同,减少 RecyclerView 重绑——这是列表”看起来不闪”的另一半,配合 32-列表刷新);②MiWifiTrackerUtils(this.lifecycle)建 tracker 并 addListener(:74-75)。 - onCreateView(:78-91):inflate
micar_bluetooth_nopaired_device布局做”WLAN 关闭提示”条,图标换micar_ic_wifi_off_indicator(:87-89)。 - onViewCreated(:93-102):挂 viewpage(可见即可说)可用性监听。
- onResume(:37-43):标题栏”WLAN”+关闭图标(点击=finish 整个弹窗)。
- onNewIntent(:140-149):对可用列表 controller
initExtraSsid()+refreshUi()(外部 intent 指定 SSID 自动点击,extrakey_chosen_wifientry_key)。 - onDestroy(:134-138):removeListener +
mWifiTrackerTools?.destroy()(quit 后台线程)。
4. MiCarWifiEntryDetailFragment:详情页
参数不走 Bundle,走全局单例 + setter 双保险:
MiCarSavedWifiListController#initWifiEntryPreference:52-61:MiCarWifiUtils.detailWifiEntry = it+setDetailWifiEntryData(it)+launchFragment。onCreate(:45-57)先从单例恢复(旋转/重建场景),SSID 空 →goBack()。
展示内容(极简,两件事):
- 唯一 XML 偏好项:自动连接开关(
pk_wifi_detail_priority_toggle→ MiCarWifiAutoConnectController,见 31-开关篇 §4); - 底部双按钮(onCreateView :76-96 inflate ViewStub +
car_ui_two_buttons_preference):- 修改密码(zh:111):开放网禁用(:104),点击弹 WifiPasswordDialog;
- 删除网络(zh:112)→
confirmDeleteAp()(:145-169)二次确认 →forget(null)(见 34-忘记网络)。
- 没有代理/IP/网关高级设置页;信号/锁图标只在列表条目上。
- 没有独立 tracker:直接持列表页的同一 entry 引用(状态更新依赖列表页 tracker 活着,见 21-架构篇 §7)。
5. Provision(开机引导)链
触发两路:
- 仓内转发:WifiSettingsActivity#onCreate:93-110 ——
!isDeviceProvisioned && refer≠SETTINGS_CONNECTION_WIFI(DevelopmentSettingsUtil.java:70-73 =Settings.Global.DEVICE_PROVISIONED != 0)时转跳; - 外部直拉:manifest:185-197 独立 action
com.android.car.settings.provision.WIFI_SETTINGS(发送方是外置 Provision App[待验证:不在本仓库]),launchMode=singleInstance,ProvisionWlan全屏主题。
与正常页的关系:同一个 UI 家族、不同的壳——Fragment/Controller/密码弹窗全部复用;差异表:
| 维度 | Provision(引导页) | 正常列表 |
|---|---|---|
| 数据构成 | 单列表:connected+全部 entry 混排(MiCarProvisionWifiListController.kt:47-60) | 双组:saved/available |
| 条目样式 | MiCarProvisionWifiEntryPreference(专用布局,隐藏详情按钮) | MiCar(Maved)WifiEntryPreference |
| 空防护 | 无 guardTransientEmpty(:38-45,空波直接清) | 500ms 防护 |
| 连接态外传 | onHandleCmd(HAS_CONNECTED_AP) 驱动”继续”按钮(MiCarWifiProvisionFragment.kt:77-92) | 无 |
| 弹窗形态 | 全屏 + 带取消钮(:62-72) | 非全屏无取消 |
| tracker | 独立一份(MiCarWifiProvisionFragment.kt:46-47) | 列表页自建 |
交互:标题”连接 WLAN”+hideSkipButton()(WlanProvisionDialogActivity.kt:106-108);底部”继续”点完 finishWithResult(RESULT_CANCELED) 回传引导流程;返回键同。
6. NetworkRequest 弹窗族(第三方 App 请求连 WiFi)
场景:第三方 App 调 ConnectivityManager.requestNetwork()(带 WifiNetworkSpecifier)申请 WiFi(如投屏、助手类指定 SSID)时,系统弹此框让用户代选网——App 自己没权限改 WiFi 配置。
- 谁拉起:系统 WiFi 服务端(framework/packages/modules/Wifi)。证据:action
com.android.settings.wifi.action.NETWORK_REQUEST沿用 AOSP 同名 +android:permission="NETWORK_SETTINGS"(signature|privileged,普通 App 无法直接 start)[发送方类在框架,待验证]。 - 形态三选一(NetworkRequestDialogActivity.java:57-70 读
EXTRA_IS_SPECIFIED_SSID):单 SSID 确认框 / 列表选择框 / 错误框(超时 30sDELAY_TIME_STOP_SCAN_MS:29 / 请求方 ABORT)。 - 生命周期:onResume
registerNetworkRequestMatchCallback(:73-81),onPause 反注册(:84-92);onMatch(List<ScanResult>)(:175-197)填 UI,标题带请求方 App 名(EXTRA_APP_NAME)。 - 连接路径不走 WifiManager.connect:
onNetworkSelected(:221-230)调NetworkRequestUserSelectionCallback.select(config)交回框架;结果onUserSelectionConnectSuccess/Failure(:200-215)→ Toast + finish。 - config 构造
NetworkRequestWifiConfigUtils.getWifiConfig(:11-35):按 ScanResult.capabilities 启发式(SAE/PSK/EAP/WEP/open),不含密码(specifier 流程密码由框架/对端处理)。 - 连上后在 WLAN 列表的呈现:
NetworkRequestEntry(仅出现在 mActiveWifiEntries)。
7. MiCarWifiCollinear.kt(f3dif 独有):flavor 接缝
fun createWifiPickerTracker(...): WifiPickerTracker {
return WifiPickerTracker(lifecycle, context, wifiManager, connectivityManager,
mainHandler, workerHandler, clock, maxScanAgeMillis, scanIntervalMillis, listener)
}- 共享代码
MiWifiTrackerUtils调它(MiWifiTrackerUtils.kt:65-74),三个 flavor 各提供同名顶层函数:f3dif 8 参;dcddif 多传 NetworkScoreManager 共 9 参;xcddif 同 8 参。 - 原因:WifiTrackerLib 三 flavor 各一整份源码,构造签名不同。“Collinear(共线)“是本仓库 flavor 钩子后缀惯例(同类:
MiBluetoothCollinear.kt)。
8. 关键调用链汇总
① 正常进入:
设置主页菜单"连接"(miauto_top_level_menu_fragment.xml:81)
→ ConnectionSettingsTopFragment#onCreate → connection_settings_fragment.xml
→ 点 WLAN 条目(隐式 intent android.settings.WIFI_SETTINGS + refer)
→ WifiSettingsActivity(manifest:148) onCreate
→(未引导且 refer≠连接页 → WlanProvisionDialogActivity)
→ DialogBaseCarSettingsActivity#onCreate:180 launchIfDifferent
→ MiCarWifiListFragment
② tracker 创建:
MiCarWifiListFragment#onCreate:74 → MiWifiTrackerUtils:init:65
→ MiCarWifiCollinear#createWifiPickerTracker:17(f3dif) → WifiPickerTracker
③ 进详情:
MiCarSavedWifiListController#initWifiEntryPreference:54(onDetailBtnClick)
→ MiCarWifiUtils.detailWifiEntry=entry + launchFragment → MiCarWifiEntryDetailFragment
④ 第三方请求连网:
系统 WiFi 服务发 com.android.settings.wifi.action.NETWORK_REQUEST
→ NetworkRequestDialogActivity#onCreate:57 →(单SSID框/列表框)
→ onMatch:175 → 用户选择 onNetworkSelected:221 → NetworkRequestUserSelectionCallback.select
→ onUserSelectionConnectSuccess:200 → Toast+finish