第一阶段:双卡模式架构
1.1 双卡模式分类(MultiSimVariants)
public enum MultiSimVariants {
DSDS, // Dual SIM Dual Standby(双卡双待)
DSDA, // Dual SIM Dual Active(双卡双活)
TSTS, // Triple SIM Triple Standby(三卡三待)
UNKNOWN
}
三种模式对比:
| 模式 | 特性 | 语音能力 | 数据能力 | 应用场景 |
|---|---|---|---|---|
| DSDS | 一个卡主动,另一个卡待机 | 1 个卡只能接听 | 1 个卡数据 | 入门 / 中端机型 |
| DSDA | 两个卡都主动 | 2 个卡同时语音 | 2 个卡同时数据 | 高端 / 特定运营商 |
| TSTS | 三个卡,一个待机 | 1-2 个卡语音 | 1-2 个卡数据 | 少见 |
DSDS 工作模式:
┌──────────────────┐ ┌──────────────────┐
│ SIM Slot 1 │ │ SIM Slot 2 │
│ (主动) │ │ (待机) │
│ │ │ │
│ 语音: 可接听 │ <→ │ 语音: 无法接听 │
│ 数据: 可使用 │ │ 数据: 无 │
│ 信号显示: 强 │ │ 信号显示: 弱 │
└──────────────────┘ └──────────────────┘
DSDA 工作模式:
┌──────────────────┐ ┌──────────────────┐
│ SIM Slot 1 │ │ SIM Slot 2 │
│ (主动) │ │ (主动) │
│ │ │ │
│ 语音: 可通话 │ │ 语音: 可通话 │
│ 数据: 可使用 │ ←→ │ 数据: 可使用 │
│ 信号显示: 强 │ │ 信号显示: 强 │
└──────────────────┘ └──────────────────┘
1.2 在 Android 中查询双卡模式
// 获取当前设备的双卡模式
TelephonyManager tm = context.getSystemService(TelephonyManager.class);
// 方法 1:获取多卡配置
MultiSimVariants config = tm.getMultiSimConfiguration();
// 返回:DSDS / DSDA / TSTS / UNKNOWN
// 方法 2:获取活跃 Modem 数量
int activeModemCount = tm.getActiveModemCount();
// 返回:1(单卡)、2(双卡)、3(三卡)
// 方法 3:获取硬件支持的最大 Modem 数
int maxModemCount = tm.getSupportedModemCount();
// 返回:2(支持双卡但可能运行在单卡模式)
// 方法 4:获取最大同时活跃卡数
int maxActiveVoiceSubId = tm.getMaxNumberOfSimultaneouslyActiveSims();
// 返回:1(DSDS/TSTS)或 2(DSDA)
// 方法 5:查询是否支持多卡
int multiSimSupported = tm.isMultiSimSupported();
// 返回:MULTISIM_ALLOWED 或 MULTISIM_NOT_SUPPORTED_BY_HARDWARE
1.3 设备属性配置
# ro.telephony.sim_config 属性决定双卡模式
# 在 device.mk 中设置
ro.telephony.sim_config=dsds # 双卡双待
ro.telephony.sim_config=dsda # 双卡双活
ro.telephony.sim_config=tsts # 三卡三待
# ro.telephony.max_active_modems 属性
# 指定最多可同时激活多少个 Modem
ro.telephony.max_active_modems=2
# 运行时模式切换
ro.telephony.dsds_mode=2 # 2=always on DSDS(不能切换回单卡)
第二阶段:多Phone实例与多Modem管理
2.1 Phone 实例创建(PhoneFactory)
public class PhoneFactory {
// 全局 Phone 实例数组
private static Phone[] sPhones; // Phone 对象数组
private static RIL[] sCommandsInterfaces; // RIL 接口数组
private static UiccController sUiccController; // UICC 控制器
// 初始化双卡
public static void makeDefaultPhones(Context context) {
synchronized (sLockProxyPhones) {
// 获取活跃 Modem 数量
int numPhones = TelephonyManager.getDefault().getActiveModemCount();
// 创建对应数量的 Phone 实例
sPhones = new Phone[numPhones]; // 每个卡一个 Phone
sCommandsInterfaces = new RIL[numPhones]; // 每个卡一个 RIL
sTelephonyNetworkFactories = new TelephonyNetworkFactory[numPhones];
for (int i = 0; i < numPhones; i++) {
// 为每个 Modem 创建 RIL 接口
sCommandsInterfaces[i] = new RIL(context,
RadioAccessFamily.getRafFromNetworkType(networkModes[i]),
cdmaSubscription,
i, // Modem index
featureFlags);
// 为每个 Modem 创建 Phone 对象
sPhones[i] = createPhone(context, i);
// 创建 ImsPhone(支持 VoLTE)
if (hasImsSupport) {
sPhones[i].createImsPhone();
}
}
// 初始化 UICC 控制器(管理 SIM 卡)
sUiccController = UiccController.make(context);
// 初始化 PhoneSwitcher(数据卡切换)
PhoneSwitcher.make(maxDataAttachModemCount, context, ...);
}
}
}
关键概念:
- PhoneId:物理卡槽索引(0 = 卡槽 1,1 = 卡槽 2)
- SubId:订阅 ID(逻辑概念,可能映射到任何卡槽)
- Slot Index:物理卡槽位置
// 获取 Phone 实例
Phone phone0 = PhoneFactory.getPhone(0); // 卡槽 0 对应的 Phone
Phone phone1 = PhoneFactory.getPhone(1); // 卡槽 1 对应的 Phone
// SubId 与 PhoneId 映射
int phoneId = SubscriptionManager.getPhoneId(subId);
int subId = SubscriptionManager.getSubscriptionId(phoneId);
2.2 动态切换单卡/双卡模式
// PhoneFactory 动态调整
public static void onMultiSimConfigChanged(Context context, int activeModemCount) {
synchronized (sLockProxyPhones) {
int prevActiveModemCount = sPhones.length;
// 如果数量相同,无需操作
if (prevActiveModemCount == activeModemCount) return;
// 仅支持增加,不支持减少(减少需要重启)
if (prevActiveModemCount > activeModemCount) return;
// 扩展数组
sPhones = Arrays.copyOf(sPhones, activeModemCount);
sCommandsInterfaces = Arrays.copyOf(sCommandsInterfaces, activeModemCount);
// 为新增的 Modem 创建对象
for (int i = prevActiveModemCount; i < activeModemCount; i++) {
// 创建新的 RIL 接口
sCommandsInterfaces[i] = new RIL(context, ...);
// 创建新的 Phone 对象
sPhones[i] = createPhone(context, i);
// 如果支持 IMS,创建 ImsPhone
if (hasImsSupport) {
sPhones[i].createImsPhone();
}
}
}
}
2.3 多 Phone 实例的业务逻辑
// 例:获取两个卡的服务状态
Phone phone0 = PhoneFactory.getPhone(0);
Phone phone1 = PhoneFactory.getPhone(1);
ServiceState ss0 = phone0.getServiceState();
ServiceState ss1 = phone1.getServiceState();
// 例:监听两个卡的呼叫状态
phone0.registerForPreciseCallStateChanged(handler, EVENT_CALL_STATE_CHANGED_0, null);
phone1.registerForPreciseCallStateChanged(handler, EVENT_CALL_STATE_CHANGED_1, null);
第三阶段:订阅管理与默认卡选择
3.1 订阅概念(SubscriptionInfo)
public class SubscriptionInfo {
private int mId; // 订阅 ID(SubId)
private int mIccId; // ICC ID(UICC 唯一标识)
private int mSimSlotIndex; // 卡槽索引
private String mDisplayName; // 显示名称
private String mCarrierName; // 运营商名称
private int mColor; // 颜色标记(UI)
private boolean mIsEmbedded; // 是否为 eSIM
private int mIconTint; // 图标色调
private String mNumber; // 电话号码
private int mDataRoaming; // 漫游状态
private int mMnc; // MNC(移动网络代码)
private int mMcc; // MCC(移动国家代码)
}
3.2 默认卡设置(三层默认)
在双卡场景下,需要分别设置:
// 1. 默认数据卡(用于互联网)
SubscriptionManager.setDefaultDataSubId(subId);
int defaultDataSubId = SubscriptionManager.getDefaultDataSubscriptionId();
// 2. 默认语音卡(用于拨号)
SubscriptionManager.setDefaultVoiceSubId(subId);
int defaultVoiceSubId = SubscriptionManager.getDefaultVoiceSubscriptionId();
// 3. 默认短信卡(用于发短信)
SubscriptionManager.setDefaultSmsSubId(subId);
int defaultSmsSubId = SubscriptionManager.getDefaultSmsSubscriptionId();
设置时机:
// SubscriptionManagerService 维护这些设置
public class SubscriptionManagerService extends Handler {
private WatchedInt mDefaultVoiceSubId; // 默认语音卡
private WatchedInt mDefaultDataSubId; // 默认数据卡
private WatchedInt mDefaultSmsSubId; // 默认短信卡
}
// 从系统设置中读取
Settings.Global.MULTI_SIM_VOICE_CALL_SUBSCRIPTION // 语音设置
Settings.Global.MULTI_SIM_DATA_CALL_SUBSCRIPTION // 数据设置
Settings.Global.MULTI_SIM_SMS_SUBSCRIPTION // 短信设置
3.3 订阅组(SubscriptionGroup)
// 某些运营商允许将多个订阅分组
public class SubscriptionGroup {
private String mGroupUuid;
private List<Integer> mSubscriptionIds; // 组内的订阅 ID 列表
}
// 获取订阅所在的组
SubscriptionInfo[] groupedSubs =
SubscriptionManager.getSubscriptionsInGroup(groupUuid);
// 组内订阅共享设置
// - MOBILE_DATA:整组共用一个开关
// - DATA_ROAMING:整组共用一个开关
// - 默认数据卡:只有一个
第四阶段:PhoneSwitcher 数据卡切换逻辑
4.1 PhoneSwitcher 职责
public class PhoneSwitcher extends Handler {
/**
* PhoneSwitcher 是双卡模式下最核心的组件,负责:
* 1. 监听订阅变化和网络请求
* 2. 决定哪个卡作为数据卡
* 3. 控制 Modem 的 PS 附着状态
* 4. 处理语音通话时的自动数据切换
* 5. 处理紧急呼叫时的数据卡切换
*/
}
4.2 数据卡选择优先级
// PhoneSwitcher 中的关键变量
protected int mPrimaryDataSubId; // 用户设置的主数据卡
protected int mPreferredDataPhoneId; // 当前活跃的数据 Phone ID
protected int mAutoSelectedDataSubId; // 自动选择的数据卡(用于 CBRS)
protected int mOpptDataSubId; // 机会性数据卡(CBRS)
// 数据卡选择顺序:
// 1. 紧急呼叫进行中 → 切换到有信号的卡(EmergencyStateTracker)
// 2. 语音通话进行中 → 自动切换到通话卡(shouldSwitchDataDueToInCall)
// 3. 机会性数据卡可用 → 使用机会性卡(CBRS/优先网络)
// 4. 自动数据切换 → 根据信号强度自动切换
// 5. 用户设置 → 使用用户选择的主卡
4.3 onEvaluate 数据卡选择逻辑
public class PhoneSwitcher {
protected boolean onEvaluate(boolean requestsChanged, String reason) {
StringBuilder sb = new StringBuilder(reason);
// 1. 检查用户设置的主数据卡是否改变
int primaryDataSubId = mSubscriptionManagerService.getDefaultDataSubId();
if (primaryDataSubId != mPrimaryDataSubId) {
mPrimaryDataSubId = primaryDataSubId;
mLastSwitchPreferredDataReason = DATA_SWITCH_REASON_MANUAL;
}
// 2. 检查是否有活跃的语音呼叫(DSDS 下自动切换)
if (updatesIfPhoneInVoiceCallChanged()) {
// 如果有通话,检查是否需要切换数据卡到通话卡
if (shouldSwitchDataDueToInCall()) {
// 切换到通话 Phone
switchDataToPhoneInCall();
}
}
// 3. 检查订阅是否变化
for (int i = 0; i < mActiveModemCount; i++) {
int sub = SubscriptionManager.getSubscriptionId(i);
if (sub != mPhoneSubscriptions[i]) {
// 订阅已改变,需要重新评估
mPhoneSubscriptions[i] = sub;
notifySubscriptionsMappingChanged();
}
}
// 4. 评估是否需要立即切换(无需等待验证)
evaluateIfImmediateDataSwitchIsNeeded();
// 5. 发送 ALLOW_DATA 命令到 Modem
updateAllowedModems();
return true; // 数据卡已改变
}
}
4.4 ALLOW_DATA 命令
在 DSDS 模式下,Modem 只允许一个卡进行 PS(数据)附着:
┌─────────────────────────────────────────┐
│ ALLOW_DATA 命令(RIL 命令) │
├─────────────────────────────────────────┤
│ 发送:Phone 0 - ALLOW_DATA (1) │ 允许卡 0 数据
│ Phone 1 - ALLOW_DATA (0) │ 禁止卡 1 数据
│ │
│ 结果:Phase switch 发生(约 1 秒) │
│ 旧数据卡断开连接 │
│ 新数据卡建立连接 │
└─────────────────────────────────────────┘
HAL 命令(新方式):
setPreferredDataModem(phoneId) // 直接设置首选 Modem
第五阶段:紧急呼叫的双卡处理
5.1 紧急呼叫时的卡选择
public class EmergencyStateTracker {
// 紧急呼叫优先级选择:
// 1. 有有效订阅的卡 → 优先选择该卡
// 2. 无任何订阅 → 选择 Phone 0(默认)
// 3. 如果选中的卡无信号 → 切换到有信号的卡
public boolean needToSwitchPhone(Phone phone) {
int subId = phone.getSubId();
int phoneId = phone.getPhoneId();
// 检查当前卡是否有效
if (!isSimReady(phoneId, subId)) {
// SIM 不可用
if (!SubscriptionManager.isValidSubscriptionId(subId)) {
// 无订阅 → 尝试切换到有订阅的其他卡
if (phoneId != 0 || isThereOtherPhone(phoneId, true)) {
return true; // 需要切换
}
} else {
// 有订阅但 SIM 未就绪 → 尝试切换到其他卡
if (isThereOtherPhone(phoneId, false)) {
return true; // 需要切换
}
}
}
return false; // 不需要切换
}
}
5.2 紧急呼叫时的数据卡切换
public class EmergencyStateTracker {
// 紧急呼叫时需要:
// 1. 确保数据卡能进行 PS 附着(可能需要切换)
// 2. 通知系统进入紧急模式
// 3. 在紧急呼叫结束后恢复原来的设置
private CompletableFuture<Boolean>
possiblyOverrideDefaultDataForEmergencyCall(Phone phone) {
// 获取当前数据卡
int oldDds = PhoneSwitcher.getInstance().getPreferredDataPhoneId();
int emergencyPhoneId = phone.getPhoneId();
// 如果紧急呼叫卡不是当前数据卡,切换
if (emergencyPhoneId != oldDds) {
// 1. 即时切换数据卡到紧急呼叫卡
PhoneSwitcher.getInstance().overrideDefaultDataPhoneId(
emergencyPhoneId);
// 2. 等待数据连接建立
// 3. 紧急呼叫结束后恢复原来的数据卡
}
return completableFuture; // 返回切换完成信号
}
}
第六阶段:多卡场景下的业务流程
6.1 来电处理
┌─────────────────────────────────────────────────────────────┐
│ 双卡设备接收来电(DSDS 模式) │
└─────────────────────────────────────────────────────────────┘
网络发来呼叫信息
↓
卡 0 RRC 接收(数据卡)→ 处理下行数据
卡 1 RRC 接收(待机卡)→ 处理下行语音信号
↓
来电事件通知(在卡 1 上)
↓
系统查询默认语音卡设置
├→ 如果来电卡 = 默认语音卡
│ → 直接呼叫响起
├→ 如果来电卡 ≠ 默认语音卡
│ → 根据配置播放所有来电或仅默认卡来电
└→ 用户可选择接听或拒绝
↓
接听来电
├→ 切换到语音卡的优先级提升
├→ 数据可能中断或降级(DSDS 只有一个 Modem 工作)
└→ 数据卡自动切换到语音卡(可选功能)
6.2 拨号处理
┌─────────────────────────────────────────────────────────────┐
│ 双卡设备拨出呼叫(DSDS 模式) │
└─────────────────────────────────────────────────────────────┘
用户拨号或选择卡
↓
应用查询默认语音卡或用户选择
├→ 无特殊选择 → 使用默认语音卡
├→ 用户指定 → 使用指定卡
└→ 多个卡都可用 → 询问用户选择
↓
在选定卡上建立语音连接
├→ 切换待机卡到该卡
├→ 如果该卡不是数据卡 → 自动切换数据到该卡
│ (可能导致原数据卡连接断开)
└→ 通话接通
↓
通话进行中
├→ 数据功能受限(只有通话卡可用)
├→ 如果需要数据 → 使用通话卡的数据
└→ 其他卡无法使用
↓
通话结束
├→ 数据卡恢复到用户设置
├→ 通话卡回到待机状态
└→ 系统恢复正常
6.3 发短信处理
┌─────────────────────────────────────────────────────────────┐
│ 双卡设备发送短信(DSDS 模式) │
└─────────────────────────────────────────────────────────────┘
应用调用 sendTextMessage()
↓
系统查询默认短信卡
├→ 已设置 → 使用该卡
├→ 未设置 → 使用默认语音卡
└→ 如果启用"每次询问" → 弹窗让用户选择
↓
在选定卡上发送短信
├→ 如果该卡不是当前活跃卡 → 可能需要切换
└→ CS 域短信发送
↓
短信发送完成
├→ 如果数据卡没改变 → 继续使用原数据卡
├→ 如果因短信改变了卡 → 恢复数据卡设置
└→ 系统恢复正常
第七阶段:关键数据结构
7.1 PhoneState(PhoneSwitcher 内部状态)
private class PhoneState {
public int mPhoneId;
public int mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
// 该 Phone 的呼叫状态
public Call.State mForegroundCall = Call.State.IDLE;
public Call.State mBackgroundCall = Call.State.IDLE;
public Call.State mRingingCall = Call.State.IDLE;
// IMS 相关
public boolean mImsConnected = false;
}
7.2 MultiSimSettingController(多卡设置协调)
public class MultiSimSettingController extends Handler {
/**
* 协调多卡场景下的设置规则:
* 1. 订阅分组:组内共享 MOBILE_DATA 和 DATA_ROAMING 设置
* 2. 默认设置:自动继承或清除
* 3. 主卡数据:只有主卡允许开启数据
*/
}
第八阶段:双卡模式切换(DSDS 动态开关)
8.1 单卡 ↔ 双卡切换
// 用户在设置中切换单卡/双卡模式
public void switchMultiSimConfig(int numOfSims) {
// numOfSims: 1(单卡)或 2(双卡)
mPhoneConfigurationManager.switchMultiSimConfig(numOfSims);
// 流程:
// 1. 设置系统属性:ro.telephony.sim_config
// 2. 触发 Modem 重配置
// 3. 通知所有组件:onMultiSimConfigChanged()
// ├→ PhoneFactory: 调整 Phone 实例
// ├→ PhoneSwitcher: 调整数据卡管理
// ├→ UiccController: 重新扫描 SIM 卡
// └→ SubscriptionManagerService: 调整订阅
// 4. 通常需要重启
}
8.2 模式切换时的业务处理
单卡 → 双卡切换:
┌───────────────────────────────────────┐
│ 1. 创建第二个 Phone 实例 │
│ 2. 创建第二个 RIL 通道 │
│ 3. 扫描第二个卡槽的 SIM 卡 │
│ 4. 创建新的订阅信息 │
│ 5. 设置默认卡(语音/数据/短信) │
│ 6. 启动 PhoneSwitcher 数据卡切换 │
│ 7. 通知系统 UI 更新(显示两个信号) │
└───────────────────────────────────────┘
双卡 → 单卡切换:
┌───────────────────────────────────────┐
│ 1. 选择主卡(通常是卡 0) │
│ 2. 禁用第二个 Modem │
│ 3. 删除第二个订阅 │
│ 4. 销毁 PhoneSwitcher 数据卡管理 │
│ 5. 清理第二个 Phone 实例 │
│ 6. 通知系统 UI 更新(仅显示一个信号)│
└───────────────────────────────────────┘
第九阶段:双卡中的特殊场景
9.1 DSDS 下的语音 + 数据限制
DSDS 设备只有一个 RF 模块和一个 Modem,因此:
┌────────────────────────────────────────────┐
│ 场景 1:在卡 0 通话中,用户打开数据 │
├────────────────────────────────────────────┤
│ 结果:数据自动切换到卡 0(通话卡) │
│ 卡 1 无法使用数据 │
│ 通话优先级更高 │
└────────────────────────────────────────────┘
┌────────────────────────────────────────────┐
│ 场景 2:在卡 1 通话中,卡 0 有来电 │
├────────────────────────────────────────────┤
│ 结果:当前通话继续在卡 1 上 │
│ 卡 0 来电进入等待或被拒绝 │
│ 等待 SRVCC(语音转换)机制 │
└────────────────────────────────────────────┘
9.2 DSDA 下的双语音 + 双数据
DSDA 设备有两个独立的 RF 模块和 Modem,因此:
┌────────────────────────────────────────────┐
│ 卡 0 通话(语音)+ 卡 1 数据 │
├────────────────────────────────────────────┤
│ ✓ 支持:同时进行(独立的无线通道) │
│ - 卡 0 可以通话 │
│ - 卡 1 可以同时使用数据 │
│ - 用户体验最佳 │
└────────────────────────────────────────────┘
┌────────────────────────────────────────────┐
│ 卡 0 通话 + 卡 1 通话(并发呼叫) │
├────────────────────────────────────────────┤
│ ✓ 支持:两个通话可同时进行 │
│ - 卡 0 通话进行中 │
│ - 卡 1 来电铃声响起 │
│ - 用户可选择接听卡 1 的来电 │
│ - 形成会议通话或三方通话 │
└────────────────────────────────────────────┘
现在让我更新 TODO 进度:
总结:Android 双卡功能核心知识点
我已经为你详细讲解了 Android 双卡功能的完整体系。让我总结关键要点:
✅ 1. 双卡模式架构(DSDS/DSDA)
- DSDS:一个卡主动,一个卡待机(入门/中端)
- DSDA:两个卡都主动(高端/特定运营商)
- 通过
TelephonyManager.getMultiSimConfiguration()查询
✅ 2. 多 Phone 实例管理
- PhoneFactory 为每个卡槽创建独立的
Phone实例 - 每个 Phone 对应一个
RIL通道和一个Modem - 支持动态切换单卡/双卡模式(通常需要重启)
✅ 3. 订阅与默认卡
- 三层默认设置:语音卡、数据卡、短信卡
- SubscriptionId (SubId):逻辑概念,映射到物理卡槽
- 订阅分组:运营商可将多个 SubId 分组,组内共享设置
✅ 4. PhoneSwitcher 数据卡切换
- 监听所有订阅和网络请求的变化
- 决定哪个卡作为数据卡(数据优先级排序)
- 向 Modem 发送
ALLOW_DATA命令控制 PS 附着 - 语音通话时自动切换数据卡到通话卡
✅ 5. 紧急呼叫处理
- EmergencyStateTracker 管理紧急呼叫
- 紧急呼叫时自动切换到有信号的卡
- 紧急呼叫进行中,可能需要切换数据卡以确保连接
- 紧急呼叫结束后恢复原设置
✅ 6. DSDS 下的限制
- 只有一个 Modem:不能同时语音 + 数据
- 解决方案:利用 VoLTE(IMS)在数据卡上进行 VoIP 通话
- 通话时数据自动切换到通话卡
✅ 7. DSDA 的优势
- 两个独立 Modem:可同时语音 + 数据
- 支持双通话(并发通话)
- 最佳用户体验,但成本更高
这是面试中关于 Android 多卡/双卡功能 的完整知识体系。涵盖了从硬件架构、系统设计、订阅管理、卡切换逻辑,到特殊场景处理的所有方面!