一、多卡架构概览
┌──────────────────────────────────────────────────────┐
│ Dual SIM Dual Standby (DSDS) │
├──────────────────────────────────────────────────────┤
│ SIM卡槽1(PhoneId=0) SIM卡槽2(PhoneId=1) │
│ ↓ ↓ │
│ Modem0 Modem1 │
│ (独立Modem) (独立Modem) │
│ ↓ ↓ │
│ Phone[0] Phone[1] │
│ (逻辑手机) (逻辑手机) │
│ ↓ ↓ │
│ RIL[0] RIL[1] │
│ ↓ ↓ │
├──────────────────────────────────────────────────────┤
│ PhoneSwitcher (协调器) │
│ - 管理多卡活跃状态 │
│ - 协调数据连接切换 │
│ - 处理语音/数据/短信默认卡设置 │
├──────────────────────────────────────────────────────┤
│ MultiSimSettingController (设置管理) │
│ - 默认语音卡、数据卡、短信卡管理 │
│ - 卡组同步设置 │
├──────────────────────────────────────────────────────┤
│ SubscriptionManagerService (订阅管理) │
│ - SubId ↔ PhoneId 映射 │
│ - 订阅信息存储与同步 │
└──────────────────────────────────────────────────────┘
二、核心概念
2.1 关键术语
| 术语 | 含义 | 例子 |
|---|---|---|
| PhoneId | 物理卡槽编号 | 0 (主卡), 1 (副卡) |
| SubId | 订阅ID,逻辑SIM的唯一标识 | 1, 2, 3... |
| Default Data SubId | 默认数据卡 | 用于网络浏览的卡 |
| Default Voice SubId | 默认语音卡 | 用于拨号的卡 |
| Default SMS SubId | 默认短信卡 | 用于发送短信的卡 |
| Preferred Data SubId | 首选数据卡 | 实时数据连接的卡 |
| DDS | Default Data Subscription | 默认数据卡的简称 |
2.2 PhoneId vs SubId 映射
// PhoneFactory 初始化时
sPhones = new Phone[numPhones]; // 基于物理卡槽
sCommandsInterfaces = new RIL[numPhones];
// PhoneSwitcher 维护映射
int[] mPhoneSubscriptions = new int[mActiveModemCount];
// mPhoneSubscriptions[0] = 1; // PhoneId 0 对应 SubId 1
// mPhoneSubscriptions[1] = 2; // PhoneId 1 对应 SubId 2
// 反向查询
SubscriptionManagerService.getPhoneId(subId) // SubId → PhoneId
SubscriptionManager.getSubscriptionId(phoneId) // PhoneId → SubId
三、PhoneSwitcher 工作原理
3.1 PhoneSwitcher 的责任
/**
* Utility singleton to monitor subscription changes and incoming NetworkRequests
* and determine which phone/phones are active.
*/
public class PhoneSwitcher extends Handler {
// 最多有多少个 Modem 可以同时进行数据连接
protected int mMaxDataAttachModemCount;
// 当前活跃的 Modem 数量 (1=单卡, 2=双卡)
protected int mActiveModemCount;
// PhoneId → SubId 映射
protected int[] mPhoneSubscriptions;
// 每个 Phone 的活跃状态
protected PhoneState[] mPhoneStates;
// 首选数据卡信息
protected int mPreferredDataPhoneId;
protected WatchedInt mPreferredDataSubId;
// 用户设置的主数据卡
protected int mPrimaryDataSubId;
}
核心职责:
- 监听变化 - 订阅状态、网络请求、语音通话状态变化
- 评估 - 根据各种条件计算哪些 Phone 应该激活
- 切换 - 向 Modem 发送
setDataAllowed()或setPreferredDataModem() - 协调 - 确保多卡场景下各功能正常工作
3.2 评估算法(onEvaluate)
protected boolean onEvaluate(boolean requestsChanged, String reason) {
// 1. 检查用户默认数据卡是否改变
int primaryDataSubId = mSubscriptionManagerService.getDefaultDataSubId();
if (primaryDataSubId != mPrimaryDataSubId) {
mPrimaryDataSubId = primaryDataSubId;
diffDetected = true;
}
// 2. 检查 PhoneId → SubId 映射是否改变
for (int i = 0; i < mActiveModemCount; i++) {
int sub = SubscriptionManager.getSubscriptionId(i);
if (sub != mPhoneSubscriptions[i]) {
mPhoneSubscriptions[i] = sub;
diffDetected = true;
}
}
// 3. 如果有变化,更新首选数据 PhoneId
if (diffDetected) {
updatePreferredDataPhoneId();
}
// 4. 根据最大数据连接 Modem 数,决定激活哪些 Phone
if (mMaxDataAttachModemCount == mActiveModemCount) {
// 所有 Modem 都可以数据连接,激活全部
for (int i = 0; i < mMaxDataAttachModemCount; i++) {
newActivePhones.add(i);
}
} else {
// 仅一个 Modem 支持数据,需要优先级选择
// 优先级:通话中的 Phone > 有网络请求的 Phone > 默认数据 Phone
// 首先激活通话中的 Phone
if (mPhoneIdInVoiceCall != INVALID_PHONE_INDEX) {
newActivePhones.add(mPhoneIdInVoiceCall);
}
// 再激活有网络请求的 Phone
for (TelephonyNetworkRequest networkRequest : mNetworkRequestList) {
int phoneIdForRequest = phoneIdForRequest(networkRequest);
if (phoneIdForRequest == INVALID_PHONE_INDEX) continue;
if (newActivePhones.contains(phoneIdForRequest)) continue;
newActivePhones.add(phoneIdForRequest);
if (newActivePhones.size() >= mMaxDataAttachModemCount) break;
}
// 最后添加默认数据 Phone
if (newActivePhones.size() < mMaxDataAttachModemCount
&& !newActivePhones.contains(mPreferredDataPhoneId)) {
newActivePhones.add(mPreferredDataPhoneId);
}
}
// 5. 激活/停用 Phone
for (int phoneId = 0; phoneId < mActiveModemCount; phoneId++) {
if (!newActivePhones.contains(phoneId)) {
deactivate(phoneId);
}
}
for (int phoneId : newActivePhones) {
activate(phoneId);
}
}
流程图:
触发事件(订阅变化、网络请求、通话状态等)
↓
onEvaluate() 评估
├─ 检查默认数据卡是否改变?
├─ 检查 PhoneId/SubId 映射是否改变?
├─ 更新首选数据 PhoneId
└─ 计算应该激活的 Phone 列表
↓
激活需要的 Phone,停用不需要的 Phone
↓
向 Modem 发送命令 (setDataAllowed / setPreferredDataModem)
↓
Modem 更新数据连接
3.3 Phone 激活/停用过程
protected void activate(int phoneId) {
switchPhone(phoneId, true);
}
protected void deactivate(int phoneId) {
switchPhone(phoneId, false);
}
private void switchPhone(int phoneId, boolean active) {
PhoneState state = mPhoneStates[phoneId];
if (state.active == active) return;
state.active = active;
logl((active ? "activate " : "deactivate ") + phoneId);
state.lastRequested = System.currentTimeMillis();
sendRilCommands(phoneId); // 向 Modem 发送命令
}
protected void sendRilCommands(int phoneId) {
Message message = Message.obtain(this, EVENT_MODEM_COMMAND_DONE, phoneId);
if (mHalCommandToUse == HAL_COMMAND_ALLOW_DATA) {
// 多卡设备:调用 setDataAllowed()
if (mActiveModemCount > 1) {
PhoneFactory.getPhone(phoneId).mCi.setDataAllowed(
isPhoneActive(phoneId), message);
}
} else if (mHalCommandToUse == HAL_COMMAND_PREFERRED_DATA) {
// 优先使用 setPreferredDataModem()
if (phoneId == mPreferredDataPhoneId) {
mRadioConfig.setPreferredDataModem(mPreferredDataPhoneId, message);
}
}
}
两种切换模式:
| 模式 | 命令 | 适用场景 |
|---|---|---|
| HAL_COMMAND_ALLOW_DATA | setDataAllowed(true/false) | 旧设备,每个 Modem 独立控制 |
| HAL_COMMAND_PREFERRED_DATA | setPreferredDataModem(phoneId) | 新设备,Modem 自动处理 |
四、MultiSimSettingController 管理默认设置
4.1 默认卡设置的规则
/**
* This class will make sure below setting rules are coordinated:
*
* 1) Grouped subscriptions will have same settings for MOBILE_DATA and DATA_ROAMING.
* 2) Default settings updated automatically. It may be cleared or inherited within group.
* 3) For primary subscriptions, only default data subscription will have MOBILE_DATA on.
*/
public class MultiSimSettingController extends Handler {
关键规则:
规则1:卡组内数据设置同步
┌──────────────┬──────────────┐
│ SubId 1 │ SubId 2 │
│ (同组) │ (同组) │
├──────────────┼──────────────┤
│ MOBILE_DATA: ON │ MOBILE_DATA: ON │
│ DATA_ROAMING: ON │ DATA_ROAMING: ON │
└──────────────┴──────────────┘
→ 两张卡如果在同一卡组,移动数据和漫游设置必须一致
规则2:默认卡自动更新
用户卡配置 PhoneSwitcher 动作
• 只有1张主卡 → 自动设为默认卡
• 2张主卡,移除1张 → 询问用户选择默认卡
• 主卡改为副卡 → 清空该设置
规则3:仅默认数据卡启用移动数据
主卡1 主卡2
SubId: 1 SubId: 2
默认: ✓ 默认: ✗
MOBILE_DATA: ON MOBILE_DATA: OFF
→ 只有被选为默认卡的主卡,用户才能启用移动数据
4.2 用户启用数据时的流程
private void onUserDataEnabled(int subId, boolean enable, boolean setDefaultData) {
// 1. 同步组内所有卡的数据设置
setUserDataEnabledForGroup(subId, enable);
SubscriptionInfo subInfo = mSubscriptionManagerService.getSubscriptionInfo(subId);
int defaultDataSubId = mSubscriptionManagerService.getDefaultDataSubId();
// 2. 如果用户启用了非默认卡的数据,自动将其设为默认卡
if (defaultDataSubId != subId && subInfo != null && !subInfo.isOpportunistic()
&& enable && subInfo.isActive() && setDefaultData) {
mSubscriptionManagerService.setDefaultDataSubId(subId);
}
}
用户流程:
用户在副卡(SubId=2)启用移动数据
↓
MultiSimSettingController.onUserDataEnabled(subId=2, enable=true)
↓
检查:SubId 2 不是默认数据卡
↓
自动设 SubId 2 为默认数据卡
↓
PhoneSwitcher 检测到默认卡变化
↓
触发 onEvaluate(),切换数据连接到副卡
五、多卡场景案例
场景1:副卡发送彩信
初始状态:
- SubId 1 (主卡): 默认数据卡,Internet 连接活跃
- SubId 2 (副卡): 非默认卡,无数据连接
步骤1: App 调用 MmsManager.sendMms(subId=2)
↓
步骤2: MmsService 请求数据连接
→ ConnectivityService.requestNetwork()
→ 指定 TelephonyNetworkSpecifier(subId=2)
↓
步骤3: TelephonyNetworkFactory[subId=2] 处理请求
↓
步骤4: PhoneSwitcher.onRequestNetwork() 被触发
↓
步骤5: onEvaluate() 评估
• 检查 SubId 2 的网络请求
• MMS 是受限制请求(没有 NET_CAPABILITY_NOT_RESTRICTED)
• 即使 SubId 2 不是默认卡,也可以激活
↓
步骤6: activate(phoneId=1) 为副卡激活数据
↓
步骤7: sendRilCommands(phoneId=1)
→ setDataAllowed(phoneId=1, true) 或
→ setPreferredDataModem(phoneId=1)
↓
步骤8: Modem 为副卡建立 MMS 数据连接
↓
步骤9: MMS 操作完成,释放 MMS 网络请求
↓
步骤10: PhoneSwitcher 检测请求释放
↓
步骤11: 如果没有其他请求在副卡,停用副卡
→ deactivate(phoneId=1)
→ setDataAllowed(phoneId=1, false)
↓
步骤12: 数据连接切回主卡
多卡指标记录:
private void collectRequestNetworkMetrics(NetworkRequest networkRequest) {
// 在多卡设备上,MMS 请求会临时禁用默认数据SIM卡
if (mActiveModemCount > 1 && networkRequest.hasCapability(
NetworkCapabilities.NET_CAPABILITY_MMS)) {
OnDemandDataSwitch onDemandDataSwitch = new OnDemandDataSwitch();
onDemandDataSwitch.apn = TelephonyEvent.ApnType.APN_TYPE_MMS;
onDemandDataSwitch.state = TelephonyEvent.EventState.EVENT_STATE_START;
TelephonyMetrics.getInstance().writeOnDemandDataSwitch(onDemandDataSwitch);
}
}
场景2:副卡通话时的数据连接
初始状态:
- SubId 1 (主卡): 默认数据卡,网络浏览活跃
- SubId 2 (副卡): 无数据连接
用户在副卡接听来电(SubId 2)
↓
RadioIndication.callStateChanged() 更新通话状态
↓
PhoneSwitcher.updatesIfPhoneInVoiceCallChanged()
→ 更新 mPhoneIdInVoiceCall = 1
↓
onEvaluate(REQUESTS_UNCHANGED, "call state changed")
↓
updatePreferredDataPhoneId() 评估
┌─ 检查:副卡在通话中?Yes
├─ 检查:IMS 注册在非 IWLAN?Yes
├─ 检查:副卡用户启用了通话期间数据?Yes
└─ 决定:切换数据到副卡(PhoneId=1)
↓
激活副卡数据连接
↓
用户结束通话
↓
mPhoneIdInVoiceCall = INVALID_PHONE_INDEX
↓
onEvaluate() 重新评估
→ 数据连接切回主卡
关键代码:
protected void updatePreferredDataPhoneId() {
int imsRegTech = mImsRegTechProvider.get(mContext, mPhoneIdInVoiceCall);
if (isAnyVoiceCallActiveOnDevice() && imsRegTech != REGISTRATION_TECH_IWLAN) {
// 有语音通话且非 IWLAN
mPreferredDataPhoneId = shouldSwitchDataDueToInCall()
? mPhoneIdInVoiceCall : getFallbackDataPhoneIdForInternetRequests();
} else {
mPreferredDataPhoneId = getFallbackDataPhoneIdForInternetRequests();
}
}
private boolean shouldSwitchDataDueToInCall() {
Phone voicePhone = findPhoneById(mPhoneIdInVoiceCall);
Phone defaultDataPhone = getPhoneBySubId(mPrimaryDataSubId);
return defaultDataPhone != null && defaultDataPhone.isUserDataEnabled()
&& voicePhone != null && voicePhone.getDataSettingsManager().isDataEnabled();
}
场景3:用户手动切换默认数据卡
设置应用:用户选择副卡(SubId=2)为默认数据卡
↓
SubscriptionManagerService.setDefaultDataSubId(2)
↓
PhoneSwitcher 监听到 ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED
↓
mDefaultDataChangedReceiver.onReceive()
↓
evaluateIfImmediateDataSwitchIsNeeded()
→ onEvaluate(REQUESTS_UNCHANGED, "default data changed")
↓
onEvaluate() 执行
• mPrimaryDataSubId = 2
• updatePreferredDataPhoneId()
• mPreferredDataPhoneId = 1 (对应 SubId 2 的 PhoneId)
↓
检查当前活跃的 Phone
• 主卡(PhoneId=0)目前活跃 → deactivate(0)
• 副卡(PhoneId=1)应该活跃 → activate(1)
↓
sendRilCommands(phoneId=1)
→ setPreferredDataModem(1) 或 setDataAllowed(1, true)
↓
Modem 将数据连接从主卡切换到副卡
↓
ConnectivityService 更新路由
↓
应用的网络连接现在使用副卡
六、多卡复杂场景处理
6.1 优先级系统
PhoneSwitcher 使用 优先级队列 来决定哪些 Phone 激活:
优先级(高→低):
1. 通话中的 Phone(mPhoneIdInVoiceCall)
2. 有网络请求的 Phone
- 受限请求优先级高(如 MMS)
- 不受限请求(如 Internet)需要是默认卡
3. 默认数据 Phone(mPreferredDataPhoneId)
List<Integer> newActivePhones = new ArrayList<>();
// 优先级1:通话中的 Phone
if (mPhoneIdInVoiceCall != INVALID_PHONE_INDEX) {
newActivePhones.add(mPhoneIdInVoiceCall);
}
// 优先级2:有网络请求的 Phone
if (newActivePhones.size() < mMaxDataAttachModemCount) {
for (TelephonyNetworkRequest networkRequest : mNetworkRequestList) {
int phoneIdForRequest = phoneIdForRequest(networkRequest);
if (phoneIdForRequest == INVALID_PHONE_INDEX) continue;
if (newActivePhones.contains(phoneIdForRequest)) continue;
newActivePhones.add(phoneIdForRequest);
if (newActivePhones.size() >= mMaxDataAttachModemCount) break;
}
}
// 优先级3:默认数据 Phone
if (newActivePhones.size() < mMaxDataAttachModemCount
&& !newActivePhones.contains(mPreferredDataPhoneId)) {
newActivePhones.add(mPreferredDataPhoneId);
}
6.2 受限 vs 不受限请求
private int phoneIdForRequest(TelephonyNetworkRequest networkRequest) {
NetworkRequest netRequest = networkRequest.getNativeNetworkRequest();
int subId = getSubIdFromNetworkSpecifier(netRequest.getNetworkSpecifier());
// 情况1:未指定卡,使用默认卡
if (subId == DEFAULT_SUBSCRIPTION_ID) return mPreferredDataPhoneId;
if (subId == INVALID_SUBSCRIPTION_ID) return INVALID_PHONE_INDEX;
int preferredDataSubId = /* 获取默认数据卡的 SubId */;
// 情况2:Internet 请求且有 NOT_RESTRICTED 能力
// → 必须是默认卡或在验证中的卡
if (netRequest.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
&& netRequest.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
&& subId != preferredDataSubId
&& subId != mValidator.getSubIdInValidation()) {
return INVALID_PHONE_INDEX; // 拒绝请求
}
// 情况3:其他请求(MMS、SUPL 等)
// → 总是允许,找到对应的 PhoneId
int phoneId = INVALID_PHONE_INDEX;
for (int i = 0; i < mActiveModemCount; i++) {
if (mPhoneSubscriptions[i] == subId) {
phoneId = i;
break;
}
}
return phoneId;
}
网络请求分类:
受限请求(NOT_RESTRICTED):
├─ NET_CAPABILITY_INTERNET
│ → 必须是默认卡或在验证中
│ → 不能同时有多个 Internet 连接
└─ 用于生产环境流量
不受限请求:
├─ NET_CAPABILITY_MMS
│ → 可以在任何卡上建立
│ → 即使不是默认卡
├─ NET_CAPABILITY_SUPL
│ → 定位服务,可在任何卡上
├─ NET_CAPABILITY_IMS
│ → VoIP 服务,可在任何卡上
└─ 用于系统或特定服务
七、单卡切换到多卡
7.1 多卡配置变化
private synchronized void onMultiSimConfigChanged(int activeModemCount) {
// 从双卡切回单卡或反之
if (mActiveModemCount == activeModemCount) return;
int oldActiveModemCount = mActiveModemCount;
mActiveModemCount = activeModemCount;
// 调整数组大小
mPhoneSubscriptions = copyOf(mPhoneSubscriptions, mActiveModemCount);
mPhoneStates = copyOf(mPhoneStates, mActiveModemCount);
// 从双卡 → 单卡:清理多余 Phone 的状态
for (int phoneId = oldActiveModemCount - 1; phoneId >= mActiveModemCount; phoneId--) {
mCurrentDdsSwitchFailure.remove(phoneId);
}
// 从单卡 → 双卡:初始化新 Phone 的状态
for (int phoneId = oldActiveModemCount; phoneId < mActiveModemCount; phoneId++) {
mPhoneStates[phoneId] = new PhoneState();
Phone phone = PhoneFactory.getPhone(phoneId);
if (phone == null) continue;
// 注册监听新 Phone 的事件
phone.registerForEmergencyCallToggle(this, EVENT_EMERGENCY_TOGGLE, null);
phone.registerForPreciseCallStateChanged(this, EVENT_PRECISE_CALL_STATE_CHANGED, null);
// ... 更多监听
}
}
八、紧急情况处理
8.1 紧急通话时的数据切换
public void overrideDefaultDataForEmergency(int phoneId, int overrideTimeSec,
CompletableFuture<Boolean> dataSwitchResult) {
// 在紧急通话期间,临时切换默认数据卡
// 原因:某些运营商/Modem 不支持在非默认卡上的 GNSS SUPL 请求
Message msg = obtainMessage(EVENT_OVERRIDE_DDS_FOR_EMERGENCY);
EmergencyOverrideRequest request = new EmergencyOverrideRequest();
request.mPhoneId = phoneId;
request.mGnssOverrideTimeMs = overrideTimeSec * 1000;
request.mOverrideCompleteFuture = dataSwitchResult;
msg.obj = request;
msg.sendToTarget();
}
紧急流程:
用户在副卡拨打紧急电话 (911/120)
↓
Telecom 调用 overrideDefaultDataForEmergency(phoneId=1, 30秒)
↓
PhoneSwitcher 临时设置 mEmergencyOverride
↓
updatePreferredDataPhoneId() 优先使用紧急 Phone 的数据
→ mPreferredDataPhoneId = 1 (副卡)
↓
数据连接切到副卡,确保 GNSS SUPL 可用
↓
30秒后(或用户挂断后更早)
↓
恢复原默认数据卡设置
九、常见问题与解决
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 副卡无法使用数据 | 副卡不是默认数据卡且请求被拒 | 用户手动启用副卡移动数据,或请求使用 TelephonyNetworkSpecifier 指定卡 |
| 切换卡时网络断连 | PhoneSwitcher 切换延迟过大 | 检查 Modem 响应时间,调整超时时间 |
| 彩信发送到错卡 | SubId 追踪不正确 | 确保应用通过 MmsManager.sendMms(subId, ...) 明确指定卡 |
| 紧急通话失败 | Modem 不支持数据切换 | 设备制造商需要在 HAL 中实现支持 |
这就是 Android 多卡管理的完整机制!核心是 PhoneSwitcher 的优先级评估和激活机制,以及 MultiSimSettingController 的设置协调。
有任何具体场景想深入讨论吗?