03 · App 侧源码精讲
代码基线:fuben4 dev
b006d1144。本篇逐段拆 App 侧三个关键文件。
文件地图
| 文件 | 行数 | 角色 |
|---|---|---|
settingsPage/micarConnectionSettings/src/main/java/com/android/car/settings/miauto/telecomm/TeleCommunicationSelectController.kt | 246 | 业务控制器(唯一) |
base/settingsBaseUi/src/main/java/com/android/car/settings/miauto/preferences/BaseTabPreferenceController.kt | 97 | Tab 控制器基类 |
base/settingsBaseUi/src/main/java/com/android/car/settings/miauto/preferences/NewTabPreference.kt | 268 | Tab 控件本体 |
settingsPage/micarConnectionSettings/src/{xcddif,f3dif,dcddif}/java/com/android/car/settings/mi/collinear/satellite/TelephonyUtil.kt | 201/201/152 | 平台反射封装(三 flavor) |
1. TeleCommunicationSelectController 逐段拆
1.1 常量与双卡前提(:30-39)
const val VEHICLE_MOBILE_NETWORK_SELECTION = "VEHICLE_MOBILE_NETWORK_SELECTION"
const val NETWORK_UNKNOWN = -1
// 背景 xcd p2.1 xcd板子同时有两个e-sim卡,卡1——电信,卡2——移动
const val NETWORK_CTCC = 0 // 中国电信
const val NETWORK_CMCC = 1 // 中国移动
const val NETWORK_AUTO = 2⚠️ 三 Tab 硬编码,无 SIM 在位门控——台架单卡时「中国移动」照样渲染可点 (N801PROB-138847 的 App 侧放大因素,改进项=按活跃 sub 数门控)。
1.2 Tab 数据构建(:80-97)
private val mSelectionTabData: Array<Tab<*>> by lazy {
arrayOf(
Tab<Int>(getString(...vehicle_network_CT)).setData(NETWORK_CTCC).apply {
mIsSelected = mMobileNetworkSelection == this.data
}.setAllowClickWhenDisabled(true),
...CMCC...,
...smart_mode...
)
}lazy构建,选中态初值来自成员mMobileNetworkSelection(此时还是 UNKNOWN, 真实选中靠 onResume 校准)setAllowClickWhenDisabled(true)= 置灰时点击仍路由到onDisabledTabClicked(弹提示框),而不是静默吞掉
1.3 onResume 校准(:99-125)
双读策略:先读 App 缓存立即画 UI → 异步读平台权威值 → 不一致就更新缓存+刷 UI。
mMobileNetworkSelection = Settings.Global.getInt(cr, VEHICLE_MOBILE_NETWORK_SELECTION, UNKNOWN)
if (mMobileNetworkSelection != UNKNOWN) refreshMobileNetSelection(it)
ThreadPoolUtils.ASYNC.execute {
val netIndex = getCurrentMobileNet() // 平台 getUserDefaultChannel
if (netIndex != mMobileNetworkSelection) { // 不一致才动作
mMobileNetworkSelection = netIndex
setSelectionToCache()
mUIHandler.post { refreshMobileNetSelection(netIndex) }
}
}:119 注释原文:「when this meet recyclerview execute computing layout, crash will happen」 ——所以刷新必须 post 到主线程下一帧,这是修过的 crash,别图省事直接调。
1.4 点击切换下发(:143-183)
override fun onSelectTabChanged(tab) {
super.onSelectTabChanged(tab) // 父类:乐观把选中态切过去(:63-67)
refreshSubTitle(tab) // summary 换文案
onTelephonySIMCardSelectChanged(tab)
}
private fun onTelephonySIMCardSelectChanged(selectedTab) {
ThreadPoolUtils.ASYNC.execute {
val result = TelephonyUtil.setDefaultChannel(channel) // 反射→平台(同步 binder)
if (result) {
mMobileNetworkSelection = ...; setSelectionToCache()
} else {
mUIHandler.post { refreshMobileNetSelection(mMobileNetworkSelection) } // 回滚 UI
}
}
}设计要点:
- 乐观更新:UI 先变,binder 慢慢走。好处是手感快;代价是平台「受理但执行失败」时 UI 会停留在”选了但没切”的状态(契约缺陷,见 04 篇)
- false 才回滚:
result为 false 的场景=平台校验失败(非法值/重复值)或反射异常 (恒 false)。⚠️ 执行层失败不会传到这里(受理层恒 true) - 线程模型:binder 调用在
ThreadPoolUtils.ASYNC,UI 刷回必须mUIHandler.post
1.5 ECALL 监听(:44-68, 204-243)
- :44-47 注释记录了一个 fwk 层坑:多个 TelephonyManager 不能共享同一个
TelephonyCallback 对象——binder 到达 fwk 后对象标识不同,解注册时只能释放最后一个,
累积导致 callback 超限。所以
mTelephonyCallbacks是ConcurrentHashMap<TelephonyManager, TelephonyCallback>一对一映射 updateEnableState()(:56)遍历所有 SIM,任何一路 OFFHOOK/RINGING 就整体禁用- 配对函数
registerCallback(:215)/unregisterCallback(:233),生命周期在 onCreate/onDestroy;注册本身 try-catch 全包,单 sub 失败不影响其他
2. Tab 框架(BaseTabPreferenceController + NewTabPreference)
2.1 基类骨架(BaseTabPreferenceController.kt)
- :37 抽象
buildTabData()——子类提供 Tab 数组;:40buildButtonData()可空, 本模块没用它(纯 Tab,无右侧按钮) - :46-61
onCreateInternal里preference.setTabCallback(this)+initUIData() - :63-67
onSelectTabChanged默认实现=单选互斥(mIsSelected = it == tab) - :74-91 可见即可说描述生成(01 篇第 6 节)
2.2 NewTabPreference 的绑定缓存(NewTabPreference.kt:187-211)
bindTabView 用 containerView.tag != hashCode() 判断”这个 widgetFrame 是不是被
RecyclerView 回收给了别的 Preference”:
- 不同 →
inflateWidgetLayoutResource整器重铺 - 相同 → 只走 binder 增量刷新 Tab 元素
改 Tab 数据后必须走 setData()(:236)——它会置 mIsNeedUpdate = true 并
notifyDataChanged();直接改 mTabData 字段不会触发重铺,UI 就不动。
2.3 点击路由(:60-72)
见 02 篇第 2 节流程图。三个回调的区分是本框架的核心约定:
onSelectTabChanged(正常切换)/onSelectedTabClicked(重复点选中项)/
onDisabledTabClicked(置灰点击,需 allowClickWhenDisabled=true)。
3. TelephonyUtil 反射封装(xcddif/f3dif 版)
3.1 为什么用反射
android.telephony.TelephonyManagerEx 是平台隐藏 API(在 pangu/yu7 的
miui/frameworks/telephony/common/base/java/android/telephony/),不进 SDK,
App 编译期不可见 → 只能 Class.forName + getMethod 反射。
3.2 与本模块相关的两个方法(:120-172)
fun getDefaultChannel(): Int // → TelephonyManagerEx.getUserDefaultChannel()
fun setDefaultChannel(channel): Boolean // → TelephonyManagerEx.setUserDefaultChannel(int)失败语义:
getDefaultChannel任何异常 → 返回ERROR_DEFAULT = -1setDefaultChannel任何异常 → 返回ERROR_SET_CHANNEL = false- 全程无 checked exception,所有异常被 catch + MLog.e 吞掉——排查时先看日志里
有没有
ClassNotFoundException/NoSuchMethodException(ROM 平台 API 缺失/改名)
3.3 常量表(:174-201)
| 常量 | 值 | 说明 |
|---|---|---|
CLASS_TELEPHONY_MANAGER_EX | android.telephony.TelephonyManagerEx | 平台扩展类 |
METHOD_GET_DEFAULT | getDefault | 取默认实例 |
METHOD_GET_USER_DEFAULT_CHANNEL | getUserDefaultChannel | 读偏好 |
METHOD_SET_SDEFAULT_CHANNEL | setUserDefaultChannel | 写偏好 |
SLOT_ONE / SLOT_TWO / SLOT_DEFAULT | 0 / 1 / 2 | 卫星默认卡=卡 2(n801) |
ERROR_DEFAULT | -1 | 通用失败 |
ERROR_SET_CHANNEL | false | 设通道失败 |
同文件还有卫星三方法(isSupportSatelliteByDevice:35、setSatelliteEnable:66、
isSatelliteEnable:95),属「移动网络→卫星」入口,与本控件共用类但逻辑独立。
3.4 dcddif 空桩(对照)
// dcd没有使用,但是为了编译通过需要有对应方法
fun getDefaultChannel(): Int { return ERROR_DEFAULT } // 恒 -1
fun setDefaultChannel(channel: Int): Boolean { ... } // 恒 ERROR_SET_CHANNEL后果:DCD 上如果误把这个分组放出来,Tab 会全部未选中、点击静默回滚。 连接页在 DCD 隐藏该分组(见 01 篇),两层防护。
4. 改代码 checklist(常见需求落点)
| 需求 | 改哪里 |
|---|---|
| 改文案 | values-zh-rCN/strings.xml + values/strings.xml(注意多语) |
| 加/减 Tab | Controller :80-97 数组 + 常量 + summary 分支(:150-162) |
| 加 SIM 在位门控(138847 改进项) | Controller mSelectionTabData 构建处,按 SubscriptionManager.getActiveSubscriptionInfoList() 数量过滤 |
| 切换失败 toast/反馈 | onTelephonySIMCardSelectChanged 的 false 分支(:175-180) |
| 换平台接口 | TelephonyUtil 常量区 + 对应方法,三 flavor 同步改 |
| 看日志 | adb shell setprop log.tag.TeleCommunicationSelectController D(默认 MLog.d 在 release 不可见,I 级可见) |