30 · WLAN 页面与导航架构

核心文件:view/WifiSettingsActivity.ktview/MiCarWifiListFragment.ktview/MiCarWifiEntryDetailFragment.ktview/WlanProvisionDialogActivity.kt+MiCarWifiProvisionFragment.ktnetworkrequest/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=truedistractionOptimized=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-182launchIfDifferent
  • 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)

三个反直觉点(排障必知):

  1. 骨架与内容分属不同层:标题”WLAN”+关闭键来自第①层窗口装饰、列表骨架来自第④层静态布局,都不依赖 WiFi 数据,所以 0.5s 就能画出来;真正等数据的是第④层里的 Preference 条目。这就是”骨架秒出、内容空窗数秒”类问题(如 MIICOSBUG-2759,见 40-排障速查)的结构根源——看性能问题先分清卡在那一层。
  2. 找标题栏别在仓里 grep 布局:它是 micarx BaseDialogActivity 的窗口装饰,想改标题栏行为看 DialogBaseCarSettingsActivity#updateTitleBar/getMiCarToolbar(:213/:254)这些转发方法。
  3. 页面切换动画分两套:launchFragmentInternalFragmentPageMark 接口分流——实现 SecondLevelAnim 的二级页走右进左出,一级页只 fade_in;Replace 接口决定事务用 replace 还是 add(都在 dialog_container 里叠,back 栈逐层弹)。

3. MiCarWifiListFragment:列表页装配

3.1 继承链与 Controller 装配

MiCarWifiListFragmentDialogSettingsFragmentTopLevelSettingsFragmentBaseXmlParserSettingsFragment。Controller 写在 XML 里反射创建(settings:controller,PreferenceControllerListHelper.java:94):

顺序节点(miauto_wifi_list_fragment.xml)keycontroller
1MiCarNewSwitchPreference(:7-11)pk_wifi_settings_entryWifiSwitchController(总开关)
2SpacePreference 8dp(:13-14)
3MiCarPreferenceCategory(:16-19)pk_wifi_current_connMiCarSavedWifiListController(已保存/当前连接组)
4SpacePreference 10dp(:21-22)
5CarUIPreferenceCategory(: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 自动点击,extra key_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()

展示内容(极简,两件事):

  1. 唯一 XML 偏好项:自动连接开关(pk_wifi_detail_priority_toggle → MiCarWifiAutoConnectController,见 31-开关篇 §4);
  2. 底部双按钮(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(开机引导)链

触发两路:

  1. 仓内转发:WifiSettingsActivity#onCreate:93-110 —— !isDeviceProvisioned && refer≠SETTINGS_CONNECTION_WIFI(DevelopmentSettingsUtil.java:70-73 = Settings.Global.DEVICE_PROVISIONED != 0)时转跳;
  2. 外部直拉: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 确认框 / 列表选择框 / 错误框(超时 30s DELAY_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