03 · App 侧源码精讲

代码基线:fuben4 dev b006d1144。本篇逐段拆 App 侧三个关键文件。

文件地图

文件行数角色
settingsPage/micarConnectionSettings/src/main/java/com/android/car/settings/miauto/telecomm/TeleCommunicationSelectController.kt246业务控制器(唯一)
base/settingsBaseUi/src/main/java/com/android/car/settings/miauto/preferences/BaseTabPreferenceController.kt97Tab 控制器基类
base/settingsBaseUi/src/main/java/com/android/car/settings/miauto/preferences/NewTabPreference.kt268Tab 控件本体
settingsPage/micarConnectionSettings/src/{xcddif,f3dif,dcddif}/java/com/android/car/settings/mi/collinear/satellite/TelephonyUtil.kt201/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 数组;:40 buildButtonData() 可空, 本模块没用它(纯 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 = -1
  • setDefaultChannel 任何异常 → 返回 ERROR_SET_CHANNEL = false
  • 全程无 checked exception,所有异常被 catch + MLog.e 吞掉——排查时先看日志里 有没有 ClassNotFoundException/NoSuchMethodException(ROM 平台 API 缺失/改名)

3.3 常量表(:174-201)

常量值说明
CLASS_TELEPHONY_MANAGER_EXandroid.telephony.TelephonyManagerEx平台扩展类
METHOD_GET_DEFAULTgetDefault取默认实例
METHOD_GET_USER_DEFAULT_CHANNELgetUserDefaultChannel读偏好
METHOD_SET_SDEFAULT_CHANNELsetUserDefaultChannel写偏好
SLOT_ONE / SLOT_TWO / SLOT_DEFAULT0 / 1 / 2卫星默认卡=卡 2(n801)
ERROR_DEFAULT-1通用失败
ERROR_SET_CHANNELfalse设通道失败

同文件还有卫星三方法(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(注意多语)
加/减 TabController :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 级可见)