多卡(DSDS)管理

2 阅读10分钟

一、多卡架构概览

┌──────────────────────────────────────────────────────┐
│           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首选数据卡实时数据连接的卡
DDSDefault 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;
}

核心职责

  1. 监听变化 - 订阅状态、网络请求、语音通话状态变化
  2. 评估 - 根据各种条件计算哪些 Phone 应该激活
  3. 切换 - 向 Modem 发送 setDataAllowed()setPreferredDataModem()
  4. 协调 - 确保多卡场景下各功能正常工作

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_DATAsetDataAllowed(true/false)旧设备,每个 Modem 独立控制
HAL_COMMAND_PREFERRED_DATAsetPreferredDataModem(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 = 1onEvaluate(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 = 2updatePreferredDataPhoneId()
    • 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 的设置协调

有任何具体场景想深入讨论吗?