07 - 多 Flavor 与海外适配
本篇是新人培训文档的第 07 篇。前几篇你已经熟悉了工程结构、PreferenceController 范式、车辆接口;本篇专门解决一个问题:这套代码是怎么「一套源码、三套产物」地编译出国内 dcd / 国内 xcd / 海外 global 三个 APK 的?我新增一个功能,到底该把代码/资源放到哪个目录?
一、本篇概览
MiCarSettings 是一个典型的多平台 + 多地区工程:
- 平台维度(miPlatform):
dcd(国内 DCD 平台)、xcd(国内 XCD 平台)、global(海外,复用 XCD 平台代码)。 - 地区维度:
cn(国内)、global(海外,含欧洲 ECE 认证区域)。
这两条维度交叉,决定了每一行代码、每一个资源文件最终是否会进入某个 APK。掌握本篇后,你就能精准地控制功能可见性。
读完你能掌握
- 看懂
app/build.gradle里sourceSets的叠加规则,能画出三个 Flavor 各自的源集拼图。 - 知道海外版
global与国内版在AndroidManifest.xml上有哪些组件差异,以及为什么。 - 会用四种条件编译/差异化手段:sourceSet 分目录、
globalImplementation依赖、BuildConfig.FLAVOR、DeviceUtil.isOverseasRegion()、settings:hiddenFeaturesXML 标记。 - 搞清海外专属模块
settingsPage/globalOnly的作用、依赖方式与包含的功能。 - 看懂
atlas翻译回填 commit 的格式,知道多语言资源放在哪、怎么回流。 - 新增「仅国内」/「仅海外」功能时,代码、资源、Manifest 三件套分别该放哪里。
二、三个 Flavor 的源集(sourceSet)拼图
2.1 sourceSets 全文(带中文注释)
下面是 app/build.gradle 里 sourceSets 块的原文(精简版),位置在 app/build.gradle:62:
sourceSets {
main { // 公共基础,所有 flavor 都会继承
manifest.srcFile 'src/main/AndroidManifest.xml'
java.srcDirs = ['src/main/java']
res.srcDirs = ['src/main/res']
aidl.srcDirs = ['src/main/aidl']
}
dcd { // 国内 DCD 平台
manifest.srcFile 'src/main/AndroidManifest.xml'
java.srcDirs = ['src/main/java', 'src/dcddif/java']
res.srcDirs = ['src/main/res', 'src/dcddif/res', 'src/region/cn/res']
aidl.srcDirs = ['src/main/aidl', 'src/dcddif/aidl']
}
xcd { // 国内 XCD 平台
manifest.srcFile 'src/main/AndroidManifest.xml'
java.srcDirs = ['src/main/java', 'src/xcddif/java']
res.srcDirs = ['src/main/res', 'src/xcddif/res', 'src/region/cn/res']
aidl.srcDirs = ['src/main/aidl', 'src/xcddif/aidl']
}
global { // 海外(复用 XCD 平台代码 + 海外地区差异)
manifest.srcFile 'src/region/global/AndroidManifest.xml' // 注意:换 Manifest!
java.srcDirs = ['src/main/java', 'src/xcddif/java', 'src/region/global/java']
res.srcDirs = ['src/main/res', 'src/xcddif/res', 'src/region/global/res']
aidl.srcDirs = ['src/main/aidl', 'src/xcddif/aidl']
}
}2.2 三 Flavor 源集对照表
| Flavor | AndroidManifest | java 源集 | res 源集 | aidl 源集 |
|---|---|---|---|---|
| dcd(国内 DCD) | src/main/AndroidManifest.xml | main/java + dcddif/java | main/res + dcddif/res + region/cn/res | main/aidl + dcddif/aidl |
| xcd(国内 XCD) | src/main/AndroidManifest.xml | main/java + xcddif/java | main/res + xcddif/res + region/cn/res | main/aidl + xcddif/aidl |
| global(海外) | src/region/global/AndroidManifest.xml ⚠️ | main/java + xcddif/java + region/global/java | main/res + xcddif/res + region/global/res | main/aidl + xcddif/aidl |
⚠️ 关键差异:
global不再用src/main/AndroidManifest.xml,而是用一份独立的src/region/global/AndroidManifest.xml。其他两个 flavor 都共用 main 的 Manifest。
2.3 源集叠加关系图
flowchart LR MAIN["📦 src/main<br/>(java+res+aidl+manifest)<br/>所有 Flavor 公共基础"] subgraph DCD["dcd flavor (国内 DCD)"] D1["src/dcddif/{java,res,aidl}"] CN1["src/region/cn/res"] end subgraph XCD["xcd flavor (国内 XCD)"] X1["src/xcddif/{java,res,aidl}"] CN2["src/region/cn/res"] end subgraph GLOBAL["global flavor (海外)"] X2["src/xcddif/{java,res,aidl}"] G1["src/region/global/{java,res}"] GM["src/region/global/<br/>AndroidManifest.xml<br/>(替换 main 的!)"] end MAIN --> DCD MAIN --> XCD MAIN --> GLOBAL D1 -.叠加.-> DCD CN1 -.叠加.-> DCD X1 -.叠加.-> XCD CN2 -.叠加.-> XCD X2 -.叠加.-> GLOBAL G1 -.叠加.-> GLOBAL GM -.替换manifest.-> GLOBAL style MAIN fill:#e3f2fd style GM fill:#ffebee style G1 fill:#fff8e1
2.4 现状盘点:dif 目录里到底有什么?
读完 sourceSets 你可能以为 dcddif/、xcddif/ 下有大量 Java 代码。实际不是——目前两个 dif 目录只各自有一个资源文件:
app/src/dcddif/res/raw/service_center_provider_info.json # DCD 平台的 IoT 服务注册信息
app/src/xcddif/res/raw/service_center_provider_info.json # XCD 平台的 IoT 服务注册信息
这是同路径不同实现:两个 flavor 都用 res/raw/service_center_provider_info.json 这个相对路径,但内容会按平台差异分别打包进各自 APK。也就是说,源集叠加不仅用于 Java 类的覆盖,也常用于同名资源的差异化替换。
📌 新手记住
main是公共底座,所有 flavor 必加。dcd/xcd都叠加region/cn/res(国内专属资源);global叠加region/global/{java,res}并换掉 Manifest。dcddif/xcddif当前只有 json 资源,没有 Java;这说明**「平台差异化」目前主要通过资源 + 运行时判断实现,而非大量 Java 类分支**。- 同名文件放在不同源集里,编译时会按 flavor 自动挑选——这是平台差异化的最常见手段。
三、海外版 AndroidManifest 差异
src/region/global/AndroidManifest.xml(1904 行)与 src/main/AndroidManifest.xml(1875 行)有大量差异。下表是核心区别:
3.1 组件差异对照表
| 类别 | 国内版(main) | 海外版(global) | 说明 |
|---|---|---|---|
| 权限定义 | 自定义权限 mi.car.settings.permission.ATMOSPHERE_LIGHT_SYNCHRONIZE | ❌ 移除 | 海外不支持双层氛围灯同步广播 |
| 国内专属弹窗 | EnergyModeForceChargeDialogActivity(强制发电弹窗)EnergyModeEvPriorityDialogActivity(强制纯电弹窗) | ❌ 移除 | 海外无「强制发电/纯电」需求 |
| 国内专属广播 | AtmosphereLightSyncReceiver(双层氛围灯同步) | ❌ 移除 | 同上 |
| 国内专属服务 | SettingMisModulePublishService(驾驶场景接入 MIS) | ❌ 移除 | MIS 是国内车控平台,海外不用 |
| 海外新增 Activity | ❌ | LocationSettingsActivityPrivacySettingsActivityMicrophoneSettingsActivityCameraSettingsActivity | 海外合规要求:独立的位置/隐私/麦克风/摄像头设置入口 |
| 海外新增服务 | ❌ | InitialLockSetupService | 海外开机引导的初始锁屏设置服务 |
| 海外新增锁屏 | ❌ | LockPinVerifyActivity、LockPinSetActivity、ProvisionLockPinSetActivity | 海外 PIN 码设置/验证流程(国内走另一套) |
| Activity 主题 | DarkFullscreenTheme、FullscreenTheme | DarkFullscreenThemeKeepStatusBar、FullscreenThemeKeepStatusBar(带 tools:replace="android:theme") | 海外部分页面需保留状态栏区域 |
3.2 差异组件关系图
flowchart TB subgraph CN["🇨🇳 国内版 main/AndroidManifest.xml"] CN_PERM["自定义权限<br/>ATMOSPHERE_LIGHT_SYNCHRONIZE"] CN_DLG["国内专属弹窗<br/>EnergyModeForceCharge / EvPriority"] CN_RECV["国内专属 Receiver<br/>AtmosphereLightSyncReceiver"] CN_SVC["国内专属 Service<br/>SettingMisModulePublishService<br/>(驾驶场景接入 MIS)"] end subgraph GLOBAL["🌍 海外版 region/global/AndroidManifest.xml"] GL_LOC["LocationSettingsActivity<br/>(位置设置入口)"] GL_PRI["PrivacySettingsActivity<br/>(隐私设置入口)"] GL_MIC["MicrophoneSettingsActivity"] GL_CAM["CameraSettingsActivity"] GL_PIN["LockPinVerify / LockPinSet<br/>ProvisionLockPinSet<br/>(PIN 码流程)"] GL_INIT["InitialLockSetupService"] GL_THEME["保留状态栏主题<br/>*KeepStatusBar + tools:replace"] end MAIN_BOTH["✅ 共有:SettingsActivity、SubSettingsActivity、DeeplinkActivity<br/>各业务 Fragment 对应的 Activity 等大量公共组件"] MAIN_BOTH --> CN MAIN_BOTH --> GLOBAL style CN fill:#fff3e0 style GLOBAL fill:#e8f5e9 style MAIN_BOTH fill:#e3f2fd
3.3 一个真实差异片段(强制发电弹窗 vs 海外位置设置)
国内 src/main/AndroidManifest.xml:1821(节选):
<!-- 强制发电弹窗(仅国内) -->
<activity
android:name=".common.CarSettingActivities$EnergyModeForceChargeDialogActivity"
android:exported="true"
android:launchMode="singleTask"
android:taskAffinity=":EnergyModeForceCharge"
android:theme="@style/MiCarCarSettingsAlertDialog">
<intent-filter>
<action android:name="micar.android.settings.ENERGY_MODE_FORCE_CHARGE" />
...
</intent-filter>
</activity>海外 src/region/global/AndroidManifest.xml:1811(同一位置换成位置设置入口):
<!-- 海外专属:位置设置入口(响应 android.settings.LOCATION_SOURCE_SETTINGS) -->
<activity
android:name="com.android.car.settings.location.LocationSettingsActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:windowSoftInputMode="adjustResize"
android:exported="true">
<intent-filter>
<action android:name="android.settings.LOCATION_SOURCE_SETTINGS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data android:name="distractionOptimized" android:value="true"/>
</activity>📌 新手记住
- 海外版的 Manifest 不是「增量」,而是整份替换——所以要保证两个 Manifest 里所有「公共组件」一致,否则海外版会缺组件。
- 新增一个只在国内生效的组件:只改
src/main/AndroidManifest.xml,海外版不会自动有,但要注意海外版是独立文件,公共组件需要两边同步。- 新增一个只在海外生效的组件:改
src/region/global/AndroidManifest.xml,国内版不会有。- 如果新增的 Activity 是公共的(两版都要),两个 Manifest 都要加——这容易遗漏,code review 时务必检查。
四、海外专属模块:settingsPage/globalOnly
Manifest 里那些海外新增的 Activity(LocationSettingsActivity、PrivacySettingsActivity、MicrophoneSettingsActivity、CameraSettingsActivity、LockPinVerifyActivity、LockPinSetActivity、ProvisionLockPinSetActivity、InitialLockSetupService)的 Java/Kotlin 实现,几乎都不在 app 模块里,而是集中在独立模块 settingsPage/globalOnly。
4.1 模块自述
settingsPage/globalOnly/readme.md 原文:
本目录包含了海外独立的需求,国内版本不需要这些需求。 该 module 的代码和资源不会编译到国内版本,这样可以减少 APK 大小同时降低内存占用。
需求清单(来自 readme):
- Top-Level Menu 集成 Google 设置入口
- 系统模块增加系统语言设置功能
- Privacy 设置按谷歌原生来(含麦克风/摄像头/位置)
- 锁屏页面(PIN 码设置/验证/开机引导锁屏)
4.2 关键:globalImplementation 让模块只进 global APK
app/build.gradle:168:
dependencies {
...
// 普通依赖,三个 flavor 都会引入
implementation project(":base:settingsBaseLib")
// 👇 只有 global flavor 才引入这个模块!
globalImplementation project(":settingsPage:globalOnly")
...
}globalImplementation 是 Android Gradle 插件为 global flavor 自动生成的依赖配置(同样存在 dcdImplementation、xcdImplementation)。被它依赖的模块不会出现在 dcd / xcd APK 里,从根本上隔离了海外代码。
4.3 globalOnly 模块自己也按 flavor 配置 sourceSet
settingsPage/globalOnly/build.gradle:
productFlavors {
dcd { dimension "miPlatform" }
xcd { dimension "miPlatform" }
global { dimension "miPlatform" }
}
sourceSets {
dcd {} // 空:dcd 下这个 library 不输出任何源
xcd {} // 空
global {
manifest.srcFile 'src/main/AndroidManifest.xml'
java.srcDirs = ['src/main/java']
res.srcDirs = ['src/main/res']
aidl.srcDirs = ['src/main/aidl']
}
}双重保险:即便有人误用 implementation project(":globalOnly"),dcd/xcd flavor 下它也没有源集输出,等于一个空模块。
4.4 globalOnly 包含哪些功能(按目录梳理)
settingsPage/globalOnly/src/main/java/
├── com/android/car/settings/
│ ├── setupservice/
│ │ └── InitialLockSetupService.java # 开机引导锁屏服务
│ ├── common/
│ │ ├── ExtraSettingsPreferenceController.java
│ │ ├── ExtraSettingsLoader.java # Google Settings Inject 注入
│ │ ├── ExtraSettingsUtil.java
│ │ └── LogicalPreferenceGroup.java
│ ├── privacy/ # 海外合规隐私设置(共 20+ 个类)
│ │ ├── PrivacySettingsFragment.java # 隐私总入口
│ │ ├── MicrophoneSettingsFragment.java # 麦克风
│ │ ├── CameraSettingsFragment.java # 摄像头
│ │ ├── VehicleDataFragment.java # 车辆数据
│ │ ├── *PreferenceController.java # 各种 Controller
│ │ └── *RecentAccessUtil.java # 最近访问记录工具
│ ├── bluetooth/
│ │ └── BluetoothRequestPermissionActivity.java # GTS 测试需要的蓝牙授权页
│ ├── security/ # 海外锁屏 PIN 码流程
│ │ ├── LockPinVerifyActivity.kt / LockPinVerifyFragment.java
│ │ ├── LockPinSetActivity.kt / LockPinSetFragment.java
│ │ ├── ProvisionLockPinSetActivity.kt # 开机引导 PIN 设置
│ │ ├── CheckLockWorker.java / SaveLockWorker.java
│ │ └── PasswordHelper.java / ConfirmLockoutHelper.java
│ └── location/ # 海外位置设置
│ ├── LocationSettingsFragment.java
│ ├── LocationStateSwitchPreferenceController.java
│ ├── LocationServicesPreferenceController.java
│ └── LocationRecentAccess*Fragment/Controller.java
4.5 依赖关系图
flowchart TB subgraph APP["app 模块 (com.android.car.settings)"] APPDCD["dcd flavor APK<br/>(国内 DCD)"] APPXCD["xcd flavor APK<br/>(国内 XCD)"] APPGLB["global flavor APK<br/>(海外)"] end subgraph GLOBALONLY["settingsPage/globalOnly<br/>(海外专属)"] GO_PRIVACY["privacy/*<br/>麦克风/摄像头/车辆数据"] GO_LOC["location/*<br/>位置设置"] GO_SEC["security/*<br/>PIN 码设置/验证/开机引导"] GO_BT["bluetooth/*<br/>GTS 蓝牙授权"] GO_INIT["setupservice/*<br/>InitialLockSetupService"] GO_RES["res/<br/>layout/drawable/values-* 资源"] end subgraph BASE["base/ 公共库"] BL[settingsBaseLib] BU[settingsBaseUi] BA[settingsLibAndroid] BT[settingsLibTile] end APPDCD -.❌不引入.- GLOBALONLY APPXCD -.❌不引入.- GLOBALONLY APPGLB ==>|globalImplementation| GLOBALONLY GLOBALONLY ==>|api/implementation| BASE APPGLB ==> BASE style APPGLB fill:#e8f5e9 style GLOBALONLY fill:#fff8e1 style APPDCD fill:#ffebee style APPXCD fill:#ffebee
📌 新手记住
- 海外专属功能首选放进
settingsPage/globalOnly模块,而不是塞到 app 的region/global/java里。前者天然不会进入国内 APK,后者只在 sourceSet 层面隔离(容易误引用)。globalImplementation是关键守门员;globalOnly/build.gradle的空 sourceSet 是第二道闸。- globalOnly 自身依赖
base/公共库,所以可以放心使用DeviceUtil、MiCarSettingsExt等工具。
五、四种「条件编译」手段(按推荐顺序)
Android 没有 C 语言的 #ifdef,但本工程用下面四种方式达到同样效果。按「隔离程度从强到弱」排序:
5.1 手段 A:sourceSet 分目录(编译期隔离)
最强隔离。把代码/资源放在 flavor 专属目录里,其他 flavor 根本编译不到。
- 海外专属 Java:放
app/src/region/global/java/或settingsPage/globalOnly/src/main/java/ - 国内专属资源:放
app/src/region/cn/res/ - 同名资源差异化:在
dcddif/与xcddif/各放一份同名文件(如service_center_provider_info.json)
当前 app/src/region/global/java/ 实际包含的类(少量、作为 app 层入口):
app/src/region/global/java/com/android/car/settings/
├── privacy/
│ ├── MicrophoneSettingsActivity.java # 在 Manifest 注册,跳转到 globalOnly 的 Fragment
│ ├── PrivacySettingsActivity.java
│ └── CameraSettingsActivity.java
└── location/
└── LocationSettingsActivity.java
这 4 个 Activity 是薄壳——它们的逻辑(Fragment、Controller)都在 globalOnly 模块。为什么还要在 app 层放一份?因为 app/build.gradle 的 global sourceSet 要把这些 Activity 的类签名编译进 APK,Manifest 里才能引用。
5.2 手段 B:globalImplementation 依赖隔离(编译期)
整模块级别隔离,见上一节。最干净的方式。
5.3 手段 C:BuildConfig.FLAVOR 判断(编译期常量)
Gradle 为每个 flavor 自动生成 BuildConfig 类,里面有 FLAVOR 字段。本工程的典型用法:
base/settingsBaseUi/src/main/java/com/android/car/settings/common/MiCarSettingsExt.kt:82:
import com.android.car.setings.carui.BuildConfig // 注意:是 carui 这个模块的 BuildConfig
/**
* 检查 app 和 Rom 是否匹配
* - DCD 平台的 ROM 只能跑 dcd flavor 的 APK
* - XCD 平台的 ROM 只能跑 xcd/global flavor 的 APK
*/
fun checkAppMatchRom(stage: String? = null): Boolean {
val isDcdApp: Boolean = BuildConfig.FLAVOR.contains("dcd")
val isDcdRom = DeviceUtil.isDCDPlatform()
MLog.d("check rom match at stage: $stage, isDcdApp = $isDcdApp, isDcdRom = $isDcdRom")
return isDcdRom && isDcdApp || !isDcdRom && !isDcdApp
}⚠️ 注意:
BuildConfig.FLAVOR反映的是当前 BuildConfig 所属模块被编译时的 flavor。跨模块时要用对应模块的 BuildConfig 包名(这里用的是com.android.car.setings.carui.BuildConfig,注意包名拼写)。
5.4 手段 D:DeviceUtil.isXxxRegion() 运行时判断(推荐用于地区差异)
这是本工程最常用的条件分支手段。地区信息来自系统属性 ro.micar.build.region。
base/settingsBaseLib/src/main/java/com/android/car/settings/miauto/common/DeviceUtil.java:39:
public class DeviceUtil {
// 读取系统属性 ro.micar.build.region
// 取值:"cn" 国内 | "global" 海外其他 | "eu" 欧洲 | "ece" 欧洲认证
private static final String BUILD_REGION = SystemProperties.get("ro.micar.build.region");
private static final String BUILD_REGION_CN = "cn";
private static final String BUILD_REGION_GLOBAL = "global";
private static final String BUILD_REGION_EU = "eu";
/** 是否国内(region 为空或 "cn" 都算国内) */
public static boolean isCnRegion() {
return TextUtils.isEmpty(BUILD_REGION) || BUILD_REGION_CN.equals(BUILD_REGION);
}
/** 是否海外(非国内即海外) */
public static boolean isOverseasRegion() {
return !isCnRegion();
}
/** 是否欧洲 */
public static boolean isEURegion() { return BUILD_REGION_EU.equals(BUILD_REGION); }
/** 是否「海外其他地区」(不含欧洲) */
public static boolean isGlobalRegion() {
return BUILD_REGION_GLOBAL.equals(BUILD_REGION);
}
}实际用法(grep 结果,真实存在):
| 调用点 | 文件 | 行为 |
|---|---|---|
DeeplinkActivity.kt:101 | app | 海外走不同的 deep link 路由 |
MenuAccountController.java:63 | app | 海外 + 访客用户时隐藏「个人中心」 |
TopLevelMenuFragment.java:112 | app | 国内才执行某段首页逻辑 |
MenuGoogleController.kt:99 | app | 国内不显示 Google 设置入口 |
LicenseMaster.kt:250 | base | 国内才加载特定 license id 列表 |
💡 注意:
isOverseasRegion()判的是 ROM 地区,不是 APK flavor。理论上 global APK 跑在国行 ROM 上也会返回 false。但实际部署时 flavor 与 ROM 是绑定的(见checkAppMatchRom),所以你可以粗略地认为「global APK = 海外」。
5.5 手段 E:XML 标记 settings:hiddenFeatures(资源层动态隐藏)
这是工程里特有的、用 XML 控制功能在某地区隐藏的机制。常用于 ECE(欧洲型式认证)场景——某些功能在欧洲法规下不允许显示。
声明自定义属性 base/settingsBaseLib/src/main/res/values/attrs.xml:19:
<attr name="hiddenFeatures" format="string"/>在 Preference XML 上打标(region/cn 与 region/global 都这么用):
<!-- IoT/连接/语音等入口在 ECE 地区隐藏 -->
<com.android.car.settings.miauto.preferences.VoiceAssistMenuPreference
android:fragment="..."
android:key="@string/psk_settings_connection_entry"
settings:hiddenFeatures="@string/micar_common_settings_lemans_ece" <!-- 👈 标记 -->
settings:controller="..." />运行时解析 base/settingsBaseUi/src/main/java/com/android/car/settings/common/HiddenFeaturesKeyListHelper.java:60:
String lemansEce = context.getResources()
.getString(R.string.micar_common_settings_lemans_ece);
for (Bundle metadata : preferenceMetadata) {
String hiddenFeatures = metadata.getString(
PreferenceXmlParser.METADATA_HIDDEN_FEATURES);
// 只有当 hiddenFeatures == "lemans_ece" 且当前是 ECE 地区,才放入待隐藏列表
boolean isHidden = lemansEce.equals(hiddenFeatures) && MiCarSettings.isEce();
if (TextUtils.isEmpty(hiddenFeatures) || !isHidden) continue;
list.add(metadata.getString(PreferenceXmlParser.METADATA_KEY));
}MiCarSettings.isEce() 同样基于 ro.micar.build.region:
public static boolean isEce() {
String region = SystemProperties.get(ROM_BUILD_REGION, "");
return TextUtils.equals(region, ECE_REGION); // ECE_REGION = "ece"
}5.6 五种手段对比
flowchart LR A["手段 A<br/>sourceSet 分目录"] B["手段 B<br/>globalImplementation"] C["手段 C<br/>BuildConfig.FLAVOR"] D["手段 D<br/>DeviceUtil.isOverseasRegion()"] E["手段 E<br/>hiddenFeatures XML 标记"] A -->|"隔离:编译期<br/>粒度:文件级<br/>场景:整块海外/国内专属功能"| OUT B -->|"隔离:编译期<br/>粒度:模块级<br/>场景:体量大的海外功能集"| OUT C -->|"隔离:编译期常量<br/>粒度:if 分支<br/>场景:平台判断(dcd/xcd)"| OUT D -->|"隔离:运行时<br/>粒度:if 分支<br/>场景:地区判断(cn/global/eu)"| OUT E -->|"隔离:运行时<br/>粒度:单个 Preference<br/>场景:ECE 合规隐藏个别项"| OUT OUT{"决策:功能差异化"} style A fill:#c8e6c9 style B fill:#c8e6c9 style C fill:#fff9c4 style D fill:#fff9c4 style E fill:#ffe0b2
📌 新手记住
- 能用 sourceSet/globalImplementation 解决的,就别用 if。编译期隔离最安全、APK 最小。
- 平台判断(dcd vs xcd)用
BuildConfig.FLAVOR;地区判断(国内 vs 海外 vs ECE)用DeviceUtil.isXxxRegion()。- ECE 合规要隐藏个别菜单项时,用
settings:hiddenFeatures="@string/micar_common_settings_lemans_ece",不要硬编码 if。
六、资源体系与多语言适配
6.1 三层资源叠加(以 global 为例)
flowchart LR M["app/src/main/res<br/>通用资源 + 默认 values/<br/>values-zh-rCN / values-de / values-en-rGB"] X["app/src/xcddif/res<br/>XCD 平台专属资源<br/>(目前只有 raw/json)"] G["app/src/region/global/res<br/>海外专属资源<br/>(覆盖 top_level_menu 等)"] M --> MERGE["资源合并器<br/>(同 key 后者覆盖前者)"] X --> MERGE G --> MERGE MERGE --> APK["global APK res"] style G fill:#e8f5e9 style M fill:#e3f2fd
region/cn/res 和 region/global/res 都覆盖了 xml/miauto_top_level_menu_fragment.xml(首页菜单),让国内/海外首页展示不同菜单项:
| 菜单项 | 国内(region/cn) | 海外(region/global) |
|---|---|---|
| 辅助驾驶 Fragment | AutopilotSettingsFragment | GlobalAutopilotSettingsFragment(海外版) |
| Location 位置设置 | ❌ 无 | ✅ 有 |
| Privacy 隐私设置 | ❌ 无 | ✅ 有 |
| Google 设置 | ✅ 有(MenuGoogleController) | ✅ 有 |
| 个人中心 | ✅ 有 | ✅ 有 |
6.2 多语言覆盖(8 种语言)
跨模块统计,工程目前包含的语言变体:
| values 目录 | 语言 | 用途 |
|---|---|---|
values/ | 默认(英文兜底) | 所有 string 的 fallback |
values-zh-rCN/ | 简体中文 | 国内主语言 |
values-en-rGB/ | 英文(英国) | 海外主语言 |
values-de/ | 德文 | 欧洲 |
values-fr/ | 法文 | 欧洲 |
values-it/ | 意大利文 | 欧洲 |
values-es/ | 西班牙文 | 欧洲 |
values-nl/ | 荷兰文 | 欧洲 |
values-nb/ | 挪威文(Bokmål) | 北欧 |
values-sv/ | 瑞典文 | 北欧 |
语言资源集中在
base/settingsBaseLib/src/main/res/(9 种语言齐全),app 模块只有values/、values-zh-rCN/、values-en-rGB/、values-de/。翻译时大部分 string 放在 settingsBaseLib。
6.3 翻译回填流程(atlas 系统)
最近一年有一系列 commit:
c33ebf0d5 [Feature][atlas-213] 回填翻译: 设置/dev [all_submitted]
c3d25bf0f [Feature][atlas-449] 回填翻译: 设置/dev [all_submitted]
fb850f3c5 [Feature][atlas-79] 回填翻译: 设置/dev [en_submitted]
...
典型 commit 内容(git show c33ebf0d5 --stat):
.../src/main/res/values-de/strings.xml | 20 +++++++++----------
.../src/main/res/values-en-rGB/strings.xml | 20 +++++++++----------
.../src/main/res/values-es/strings.xml | 10 ++++++++++
.../src/main/res/values-fr/strings.xml | 10 ++++++++++
.../src/main/res/values-it/strings.xml | 10 ++++++++++
.../src/main/res/values-nb/strings.xml | 10 ++++++++++
.../src/main/res/values-nl/strings.xml | 10 ++++++++++
.../src/main/res/values-sv/strings.xml | 10 ++++++++++
8 files changed, 80 insertions(+), 20 deletions(-)
6.4 翻译回填流程图
flowchart TB DEV1["1️⃣ 开发在 dev 分支<br/>修改 values/strings.xml<br/>(默认英文) 或 values-zh-rCN"] PUSH["2️⃣ push 到 Gerrit<br/>dev 分支"] EXPORT["3️⃣ CI 触发<br/>导出待翻译 string<br/>到 atlas 翻译平台"] TRANS["4️⃣ 翻译团队/外包<br/>在 atlas 平台翻译<br/>8 种目标语言"] SUBMIT["5️⃣ 翻译完成<br/>状态: en_submitted → all_submitted"] BOT["6️⃣ Atlas Bot<br/>自动生成回填 commit<br/>[Feature][atlas-XXX] 回填翻译"] REVIEW["7️⃣ Gerrit Code Review<br/>Reviewer: hanguoliang1 等"] MERGE["8️⃣ 合入 dev<br/>values-{de,en-rGB,es,fr,it,<br/>nb,nl,sv}/strings.xml 更新"] DEV1 --> PUSH --> EXPORT --> TRANS --> SUBMIT --> BOT --> REVIEW --> MERGE style BOT fill:#fff8e1 style MERGE fill:#e8f5e9
Commit message 解读:
[Feature][atlas-213] 回填翻译: 设置/dev [all_submitted]
Collie-Order: 9367 # Collie 是小米的本地化订单系统
Collie-Status: all_submitted # all_submitted 表示所有语种都提交完成
Atlas-Order: 213 # Atlas 平台订单号
Jira: MICOCKPIT-142093
Signed-off-by: micar-dev <micar-dev@xiaomi.com>
Change-Id: Ib4b71ff584a9d4c56ea89a7b5dea4f0a26c4a0b7
[en_submitted]:只有英文翻译完成(部分回填)[all_submitted]:全部 8 种语言翻译完成(完整回填)
📌 新手记住
- 新增/修改 string 时,只改
values/(默认)或values-zh-rCN/,不要手动去改values-de/等其他语言——那由 atlas 回填。- 提 PR 后,等 atlas 自动回填翻译;不要自己手动翻译其他语言,避免和平台冲突。
- string 命名规范、不要随意改 key——key 一改,atlas 上的翻译历史就断了。
- 海外资源优先放
region/global/res;通用多语言资源放base/settingsBaseLib/src/main/res/。
七、实战指南:我该把代码/资源放哪里?
7.1 场景速查表
| 你的需求 | 代码放哪 | 资源放哪 | Manifest 改哪 |
|---|---|---|---|
| 三平台都需要的公共功能 | app/src/main/java 或 base/ 各 lib | app/src/main/res 或 base/.../res | 两边都改:main/ + region/global/ |
| 只在 DCD 平台生效 | app/src/dcddif/java | app/src/dcddif/res | 仅 main/AndroidManifest.xml |
| 只在 XCD 平台生效 | app/src/xcddif/java | app/src/xcddif/res | 仅 main/AndroidManifest.xml |
| 只在国内(dcd+xcd)生效 | 主代码放 main,运行时用 DeviceUtil.isCnRegion() 守护 | app/src/region/cn/res | 仅 main/AndroidManifest.xml |
| 只在海外生效(轻量,一个 Activity) | app/src/region/global/java | app/src/region/global/res | 仅 region/global/AndroidManifest.xml |
| 只在海外生效(成体系的功能) | settingsPage/globalOnly/src/main/java ⭐ | settingsPage/globalOnly/src/main/res | settingsPage/globalOnly/src/main/AndroidManifest.xml |
| 欧洲 ECE 法规要求隐藏某菜单项 | 不用动代码 | 在 Preference XML 加 settings:hiddenFeatures="@string/micar_common_settings_lemans_ece" | 不用改 |
7.2 决策流程图
flowchart TD START["新增功能/资源"] Q1{"三平台都要?"} Q2{"海外专属?"} Q3{"功能成体系<br/>(多文件/独立业务)?"} Q4{"仅某一平台<br/>dcd 或 xcd?"} Q5{"ECE 合规隐藏?"} A1["✅ main 或 base 公共目录<br/>Manifest 两边都改"] A2A["✅ settingsPage/globalOnly<br/>(模块级隔离,推荐)"] A2B["✅ app/src/region/global/<br/>(轻量薄壳)"] A3["✅ app/src/dcdif 或 xcddif"] A4["✅ hiddenFeatures XML 标记"] A5["✅ DeviceUtil.isCnRegion()<br/>运行时守护"] START --> Q1 Q1 -- 是 --> A1 Q1 -- 否 --> Q2 Q2 -- 是 --> Q3 Q2 -- 否 --> Q4 Q3 -- 是 --> A2A Q3 -- 否(只一个 Activity) --> A2B Q4 -- 是 --> A3 Q4 -- 否 --> Q5 Q5 -- 是 --> A4 Q5 -- 否 --> A5 style A2A fill:#c8e6c9 style A1 fill:#e3f2fd
7.3 三个高频踩坑点
-
改了
main/AndroidManifest.xml,忘了同步region/global/AndroidManifest.xml→ 海外版 APK 缺组件、Activity not found。改公共组件一定要两边都改。 -
在国内代码里硬编码引用了 globalOnly 的类 → 国内 APK 编译不过(globalOnly 没被
implementation引入)。 → 解决:通过路由 / 反射 / 接口注入,或用Class.forName守护。 -
手动改
values-de/strings.xml后被 atlas 回填覆盖 → 翻译丢失。 → 解决:只改默认values/或values-zh-rCN/,让 atlas 推其他语言。
📌 新手记住
- 首选 sourceSet/globalImplementation,次选 XML 标记,最后才用 if 分支——这是本工程的差异化代码规范。
- 新增海外功能时,默认往
settingsPage/globalOnly放;只有当它是 app 层薄壳 Activity(被 Manifest 注册)时,才放app/src/region/global/java。- 改 Manifest 时,永远问自己一句:这个组件是公共的还是专属的?公共的就改两份。
八、命令速查
# 只编 dcd(国内 DCD)
./gradlew :app:assembleDcdDebug
# 只编 xcd(国内 XCD)
./gradlew :app:assembleXcdDebug
# 只编 global(海外)
./gradlew :app:assembleGlobalDebug
# 出 release 包
./gradlew :app:assembleGlobalRelease
# 当前 git 工作区在哪个 flavor 上(看 task 名)
./gradlew tasks | grep assemble发布产物(见 app/build.gradle 的 publishing 块):
MiCarSettings-<version>-dcd.apkMiCarSettings-<version>-xcd.apkMiCarSettings-<version>-global.apk
九、扩展阅读
- 海外隐私合规(GDPR)相关代码入口:
settingsPage/globalOnly/src/main/java/com/android/car/settings/privacy/PrivacySettingsFragment.java - ECE 法规隐藏机制:
base/settingsBaseUi/src/main/java/com/android/car/settings/common/HiddenFeaturesKeyListHelper.java - 地区判断工具:
base/settingsBaseLib/src/main/java/com/android/car/settings/miauto/common/DeviceUtil.java - 平台/ROM 匹配校验:
base/settingsBaseUi/src/main/java/com/android/car/settings/common/MiCarSettingsExt.kt - 海外功能清单:
settingsPage/globalOnly/readme.md - 翻译回填 commit 范例:
git log --grep="atlas"
十、本篇核心要点回顾
- 三 Flavor 源集 =
main+ 平台 dif(dcdif/xcddif)+ 地区 region(cn/global)的叠加;global额外替换 Manifest。 - 海外专属功能集中放
settingsPage/globalOnly模块,通过globalImplementation只链入 global APK,是国内/海外隔离最干净的手段。 - 差异化优先级:sourceSet / globalImplementation(编译期)>
hiddenFeaturesXML 标记(资源层)>DeviceUtil.isOverseasRegion()/BuildConfig.FLAVOR(代码 if 分支)。 - 改 Manifest 一定要两份都看:
main/与region/global/是独立两份,不是增量合并。 - 多语言只改
values/与values-zh-rCN/,其他语言由 atlas 翻译平台自动回填。