07 - 多 Flavor 与海外适配

本篇是新人培训文档的第 07 篇。前几篇你已经熟悉了工程结构、PreferenceController 范式、车辆接口;本篇专门解决一个问题:这套代码是怎么「一套源码、三套产物」地编译出国内 dcd / 国内 xcd / 海外 global 三个 APK 的?我新增一个功能,到底该把代码/资源放到哪个目录?


一、本篇概览

MiCarSettings 是一个典型的多平台 + 多地区工程:

  • 平台维度(miPlatform)dcd(国内 DCD 平台)、xcd(国内 XCD 平台)、global(海外,复用 XCD 平台代码)。
  • 地区维度cn(国内)、global(海外,含欧洲 ECE 认证区域)。

这两条维度交叉,决定了每一行代码、每一个资源文件最终是否会进入某个 APK。掌握本篇后,你就能精准地控制功能可见性。

读完你能掌握

  1. 看懂 app/build.gradlesourceSets 的叠加规则,能画出三个 Flavor 各自的源集拼图。
  2. 知道海外版 global 与国内版在 AndroidManifest.xml 上有哪些组件差异,以及为什么。
  3. 会用四种条件编译/差异化手段:sourceSet 分目录、globalImplementation 依赖、BuildConfig.FLAVORDeviceUtil.isOverseasRegion()settings:hiddenFeatures XML 标记。
  4. 搞清海外专属模块 settingsPage/globalOnly 的作用、依赖方式与包含的功能。
  5. 看懂 atlas 翻译回填 commit 的格式,知道多语言资源放在哪、怎么回流。
  6. 新增「仅国内」/「仅海外」功能时,代码、资源、Manifest 三件套分别该放哪里。

二、三个 Flavor 的源集(sourceSet)拼图

2.1 sourceSets 全文(带中文注释)

下面是 app/build.gradlesourceSets 块的原文(精简版),位置在 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 源集对照表

FlavorAndroidManifestjava 源集res 源集aidl 源集
dcd(国内 DCD)src/main/AndroidManifest.xmlmain/java + dcddif/javamain/res + dcddif/res + region/cn/resmain/aidl + dcddif/aidl
xcd(国内 XCD)src/main/AndroidManifest.xmlmain/java + xcddif/javamain/res + xcddif/res + region/cn/resmain/aidl + xcddif/aidl
global(海外)src/region/global/AndroidManifest.xml ⚠️main/java + xcddif/java + region/global/javamain/res + xcddif/res + region/global/resmain/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 是国内车控平台,海外不用
海外新增 ActivityLocationSettingsActivity
PrivacySettingsActivity
MicrophoneSettingsActivity
CameraSettingsActivity
海外合规要求:独立的位置/隐私/麦克风/摄像头设置入口
海外新增服务InitialLockSetupService海外开机引导的初始锁屏设置服务
海外新增锁屏LockPinVerifyActivityLockPinSetActivityProvisionLockPinSetActivity海外 PIN 码设置/验证流程(国内走另一套)
Activity 主题DarkFullscreenThemeFullscreenThemeDarkFullscreenThemeKeepStatusBarFullscreenThemeKeepStatusBar(带 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(LocationSettingsActivityPrivacySettingsActivityMicrophoneSettingsActivityCameraSettingsActivityLockPinVerifyActivityLockPinSetActivityProvisionLockPinSetActivityInitialLockSetupService)的 Java/Kotlin 实现,几乎都不在 app 模块里,而是集中在独立模块 settingsPage/globalOnly

4.1 模块自述

settingsPage/globalOnly/readme.md 原文:

本目录包含了海外独立的需求,国内版本不需要这些需求。 该 module 的代码和资源不会编译到国内版本,这样可以减少 APK 大小同时降低内存占用。

需求清单(来自 readme):

  1. Top-Level Menu 集成 Google 设置入口
  2. 系统模块增加系统语言设置功能
  3. Privacy 设置按谷歌原生来(含麦克风/摄像头/位置)
  4. 锁屏页面(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 自动生成的依赖配置(同样存在 dcdImplementationxcdImplementation)。被它依赖的模块不会出现在 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/ 公共库,所以可以放心使用 DeviceUtilMiCarSettingsExt 等工具。

五、四种「条件编译」手段(按推荐顺序)

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.gradleglobal 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:101app海外走不同的 deep link 路由
MenuAccountController.java:63app海外 + 访客用户时隐藏「个人中心」
TopLevelMenuFragment.java:112app国内才执行某段首页逻辑
MenuGoogleController.kt:99app国内不显示 Google 设置入口
LicenseMaster.kt:250base国内才加载特定 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/cnregion/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/resregion/global/res 都覆盖了 xml/miauto_top_level_menu_fragment.xml(首页菜单),让国内/海外首页展示不同菜单项:

菜单项国内(region/cn)海外(region/global)
辅助驾驶 FragmentAutopilotSettingsFragmentGlobalAutopilotSettingsFragment(海外版)
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/javabase/ 各 libapp/src/main/resbase/.../res两边都改main/ + region/global/
只在 DCD 平台生效app/src/dcddif/javaapp/src/dcddif/resmain/AndroidManifest.xml
只在 XCD 平台生效app/src/xcddif/javaapp/src/xcddif/resmain/AndroidManifest.xml
只在国内(dcd+xcd)生效主代码放 main,运行时用 DeviceUtil.isCnRegion() 守护app/src/region/cn/resmain/AndroidManifest.xml
只在海外生效(轻量,一个 Activity)app/src/region/global/javaapp/src/region/global/resregion/global/AndroidManifest.xml
只在海外生效(成体系的功能)settingsPage/globalOnly/src/main/javasettingsPage/globalOnly/src/main/ressettingsPage/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 三个高频踩坑点

  1. 改了 main/AndroidManifest.xml,忘了同步 region/global/AndroidManifest.xml → 海外版 APK 缺组件、Activity not found。改公共组件一定要两边都改

  2. 在国内代码里硬编码引用了 globalOnly 的类 → 国内 APK 编译不过(globalOnly 没被 implementation 引入)。 → 解决:通过路由 / 反射 / 接口注入,或用 Class.forName 守护。

  3. 手动改 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.gradlepublishing 块):

  • MiCarSettings-<version>-dcd.apk
  • MiCarSettings-<version>-xcd.apk
  • MiCarSettings-<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"

十、本篇核心要点回顾

  1. 三 Flavor 源集 = main + 平台 dif(dcdif/xcddif)+ 地区 region(cn/global)的叠加global 额外替换 Manifest。
  2. 海外专属功能集中放 settingsPage/globalOnly 模块,通过 globalImplementation 只链入 global APK,是国内/海外隔离最干净的手段。
  3. 差异化优先级:sourceSet / globalImplementation(编译期)> hiddenFeatures XML 标记(资源层)> DeviceUtil.isOverseasRegion() / BuildConfig.FLAVOR(代码 if 分支)。
  4. 改 Manifest 一定要两份都看main/region/global/ 是独立两份,不是增量合并。
  5. 多语言只改 values/values-zh-rCN/,其他语言由 atlas 翻译平台自动回填。