Android 双卡默认数据卡切换流程详解

88 阅读8分钟

我来分析一下默认数据卡切换的流程。让我查看相关代码:

现在让我查看onEvaluate的完整实现和数据切换的核心逻辑:

非常好!现在我已经收集了足够的信息。让我整理一下默认数据卡切换的完整流程:


Android 双卡默认数据卡切换流程详解 📱📶

基于 frameworks-opt-telephony 代码分析,默认数据卡(DDS - Default Data Subscription)切换是一个复杂的多模块协同过程。


核心架构概览

用户/系统设置
    ↓
SubscriptionManagerService
    ↓
MultiSimSettingController (多卡协调)
    ↓
PhoneSwitcher (数据卡切换核心)
    ↓
DataNetworkController (数据网络控制)
    ↓
RadioConfig/Phone.mCi
    ↓
RIL → Modem (setPreferredDataModem / setDataAllowed)

一、用户手动切换流程 🔄

触发方式

  1. 设置界面:设置 → 网络和互联网 → SIM 卡 → 默认数据卡
  2. 快速设置面板:下拉通知栏切换数据卡
  3. 程序化调用SubscriptionManager.setDefaultDataSubscriptionId()

详细步骤

步骤 1: SubscriptionManagerService 接收请求

/**
 * Set the default data subscription id.
 *
 * @param subId The default data subscription id.
 *
 * @throws SecurityException if callers do not hold the required permission.
 */
@Override
@RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE)
public void setDefaultDataSubId(int subId) {
    enforcePermissions("setDefaultDataSubId", Manifest.permission.MODIFY_PHONE_STATE);

    if (subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) {
        throw new RuntimeException("setDefaultDataSubId called with DEFAULT_SUBSCRIPTION_ID");
    }

    enforceTelephonyFeatureWithException(getCurrentPackageName(), "setDefaultDataSubId");

职责:

  • 权限检查(MODIFY_PHONE_STATE
  • 更新数据库设置(Settings.Global.MULTI_SIM_DATA_CALL_SUBSCRIPTION
  • 通知订阅变化

步骤 2: MultiSimSettingController 协调多卡设置

/**
 * This class will make sure below setting rules are coordinated across different subscriptions
 * and phones in multi-SIM case:
 *
 * 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.
 *    If default subscription A switches to profile B which is in the same group, B will
 *    become the new default.
 * 3) For primary subscriptions, only default data subscription will have MOBILE_DATA on.
 */
public class MultiSimSettingController extends Handler {

关键事件:

EVENT_DEFAULT_DATA_SUBSCRIPTION_CHANGED = 6

协调规则:

  1. 移动数据开关协调:确保只有默认数据卡的 MOBILE_DATA 开启
  2. 分组订阅处理:同一组的订阅共享数据和漫游设置
  3. 自动继承:在同组内切换时自动继承设置

步骤 3: PhoneSwitcher 评估和执行切换

/**
 * Utility singleton to monitor subscription changes and incoming NetworkRequests
 * and determine which phone/phones are active.
 * <p>
 * Manages the ALLOW_DATA calls to modems and notifies phones about changes to
 * the active phones.  Note we don't wait for data attach (which may not happen anyway).
 */
public class PhoneSwitcher extends Handler {

事件触发:

EVENT_PRIMARY_DATA_SUB_CHANGED

核心方法:onEvaluate()

protected boolean onEvaluate(boolean requestsChanged, String reason) {
    StringBuilder sb = new StringBuilder(reason);

    // If we use HAL_COMMAND_PREFERRED_DATA,
    boolean diffDetected = mHalCommandToUse != HAL_COMMAND_PREFERRED_DATA && requestsChanged;

    // Check if user setting of default non-opportunistic data sub is changed.
    int primaryDataSubId = mSubscriptionManagerService.getDefaultDataSubId();
    if (primaryDataSubId != mPrimaryDataSubId) {
        sb.append(" mPrimaryDataSubId ").append(mPrimaryDataSubId).append("->")
            .append(primaryDataSubId);
        mPrimaryDataSubId = primaryDataSubId;
        mLastSwitchPreferredDataReason = DataSwitch.Reason.DATA_SWITCH_REASON_MANUAL;
    }

    // Check to see if there is any active subscription on any phone
    boolean hasAnyActiveSubscription = false;

    // Check if phoneId to subId mapping is changed.
    for (int i = 0; i < mActiveModemCount; i++) {
        int sub = SubscriptionManager.getSubscriptionId(i);

        if (SubscriptionManager.isValidSubscriptionId(sub)) hasAnyActiveSubscription = true;

        if (sub != mPhoneSubscriptions[i]) {
            sb.append(" phone[").append(i).append("] ").append(mPhoneSubscriptions[i]);
            sb.append("->").append(sub);
            if (mAutoSelectedDataSubId == mPhoneSubscriptions[i]) {
                mAutoSelectedDataSubId = DEFAULT_SUBSCRIPTION_ID;
            }
            mPhoneSubscriptions[i] = sub;

            if (!mFlags.changeMethodOfObtainingImsRegistrationRadioTech()) {
                // Listen to IMS radio tech change for new sub
                if (SubscriptionManager.isValidSubscriptionId(sub)) {
                    registerForImsRadioTechChange(mContext, i);
                }
            }

            diffDetected = true;
            mAutoDataSwitchController.notifySubscriptionsMappingChanged();
        }
    }

    if (!hasAnyActiveSubscription) {
        transitionToEmergencyPhone();
    } else {
        if (VDBG) log("Found an active subscription");
    }

    // Check if phoneId for preferred data is changed.
    int oldPreferredDataPhoneId = mPreferredDataPhoneId;

    // Check if subId for preferred data is changed.
    int oldPreferredDataSubId = mPreferredDataSubId.get();

    // When there are no subscriptions, the preferred data phone ID is invalid, but we want
    // to keep a valid phoneId for Emergency, so skip logic that updates for preferred data
    // phone ID. Ideally there should be a single set of checks that evaluate the correct
    // phoneId on a service-by-service basis (EIMS being one), but for now... just bypass
    // this logic in the no-SIM case.
    if (hasAnyActiveSubscription) updatePreferredDataPhoneId();

    if (oldPreferredDataPhoneId != mPreferredDataPhoneId) {
        sb.append(" preferred data phoneId ").append(oldPreferredDataPhoneId)
                .append("->").append(mPreferredDataPhoneId);
        diffDetected = true;
    } else if (oldPreferredDataSubId != mPreferredDataSubId.get()) {
        logl("SIM refresh, notify dds change");
        // Inform connectivity about the active data phone
        notifyPreferredDataSubIdChanged();
    }

评估逻辑:

  1. 检查默认数据订阅 ID 是否变化
  2. 检查 phoneId 到 subId 的映射关系
  3. 更新首选数据 Phone ID(updatePreferredDataPhoneId()
  4. 检测变化并决定是否需要发送命令到 Modem

步骤 4: 更新首选数据 Phone ID

protected void updatePreferredDataPhoneId() {
    if (mEmergencyOverride != null && findPhoneById(mEmergencyOverride.mPhoneId) != null) {
        // Override DDS for emergency even if user data is not enabled, since it is an
        // emergency.
        // TODO: Provide a notification to the user that metered data is currently being
        // used during this period.
        logl("updatePreferredDataPhoneId: preferred data overridden for emergency."
                + " phoneId = " + mEmergencyOverride.mPhoneId);
        mPreferredDataPhoneId = mEmergencyOverride.mPhoneId;
        mLastSwitchPreferredDataReason = DataSwitch.Reason.DATA_SWITCH_REASON_UNKNOWN;
    } else {
        int imsRegTech = mImsRegTechProvider.get(mContext, mPhoneIdInVoiceCall);
        if (isAnyVoiceCallActiveOnDevice() && imsRegTech != REGISTRATION_TECH_IWLAN) {
            if (imsRegTech != REGISTRATION_TECH_CROSS_SIM) {
                mPreferredDataPhoneId = shouldSwitchDataDueToInCall()
                        ? mPhoneIdInVoiceCall : getFallbackDataPhoneIdForInternetRequests();
            } else {
                logl("IMS call on cross-SIM, skip switching data to phone "
                        + mPhoneIdInVoiceCall);
            }
        } else {
            mPreferredDataPhoneId = getFallbackDataPhoneIdForInternetRequests();
        }
    }

    mPreferredDataSubId.set(SubscriptionManager.getSubscriptionId(mPreferredDataPhoneId));
}

优先级顺序:

  1. 紧急呼叫覆盖:紧急呼叫时强制切换到特定卡
  2. 通话中数据切换:通话时根据配置可能切换到通话卡
  3. 正常数据卡:用户设置的默认数据卡

步骤 5: 发送命令到 Modem

protected void sendRilCommands(int phoneId) {
    if (!SubscriptionManager.isValidPhoneId(phoneId)) {
        logl("sendRilCommands: skip dds switch due to invalid phoneId=" + phoneId);
        return;
    }

    Message message = Message.obtain(this, EVENT_MODEM_COMMAND_DONE, phoneId);
    if (mHalCommandToUse == HAL_COMMAND_ALLOW_DATA || mHalCommandToUse == HAL_COMMAND_UNKNOWN) {
        // Skip ALLOW_DATA for single SIM device
        if (mActiveModemCount > 1) {
            PhoneFactory.getPhone(phoneId).mCi.setDataAllowed(isPhoneActive(phoneId), message);
        }
    } else if (phoneId == mPreferredDataPhoneId) {
        // Only setPreferredDataModem if the phoneId equals to current mPreferredDataPhoneId
        logl("sendRilCommands: setPreferredDataModem - phoneId: " + phoneId);
        mRadioConfig.setPreferredDataModem(mPreferredDataPhoneId, message);
    }
}

两种 HAL 命令模式:

  1. HAL_COMMAND_ALLOW_DATA(旧模式)

    • 多个 Phone 分别调用 setDataAllowed(true/false)
    • 只允许默认数据卡的数据连接
  2. HAL_COMMAND_PREFERRED_DATA(新模式,推荐)

    • 通过 RadioConfig.setPreferredDataModem(phoneId) 一次性设置
    • Modem 自动管理各卡的数据连接状态

步骤 6: RIL 层处理

通过 HIDL/AIDL 接口发送到 HAL:

RIL.setPreferredDataModem() 
    → IRadioConfig.setPreferredDataModem()
    → Vendor HAL Implementation
    → Modem Firmware

步骤 7: Modem 执行切换

  • 原数据卡:断开 PDN 连接(deactivate)
  • 新数据卡:建立 PDN 连接(activate)
  • PS Attach 到新卡的网络
  • 更新路由表和网络接口

步骤 8: 响应处理和状态同步

成功响应:

EVENT_MODEM_COMMAND_DONE → onDdsSwitchResponse()
  • 更新内部状态
  • 通知 DataNetworkController 数据卡已切换
  • 广播 ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED

失败响应:

  • 记录失败原因
  • 根据情况重试(EVENT_MODEM_COMMAND_RETRY
  • 常见失败原因:
    • OP_NOT_ALLOWED_DURING_VOICE_CALL: 通话中不允许切换
    • RADIO_NOT_AVAILABLE: 无线电不可用
    • INTERNAL_ERR: 内部错误

二、自动数据卡切换(Auto Data Switch) 🔀

触发场景

AutoDataSwitchController 管理:

  1. 主卡无信号:自动切换到有信号的副卡
  2. 主卡网络质量差:切换到信号更好的副卡
  3. 主卡数据拥塞:切换到网络负载较低的卡
  4. 5G/4G 网络优选:优先选择更快的网络

验证流程

切换前需要验证新卡的网络连接:

CellularNetworkValidator.validate()
    ↓
建立测试数据连接
    ↓
执行网络验证(ping/HTTP)
    ↓
EVENT_NETWORK_VALIDATION_DONE

验证成功:执行切换 验证失败:取消切换,保持原数据卡


三、通话中数据切换(Data During Call) 📞

场景说明

某些双卡设备支持通话时自动切换数据到通话卡,保证:

  • VoLTE 通话质量
  • 通话时的数据业务(如视频通话)
  • SUPL 定位服务

切换条件

private boolean shouldSwitchDataDueToInCall() {
    Phone voicePhone = findPhoneById(mPhoneIdInVoiceCall);
    Phone defaultDataPhone = getPhoneBySubId(mPrimaryDataSubId);
    return defaultDataPhone != null // check user enabled data
            && defaultDataPhone.isUserDataEnabled()
            && voicePhone != null // check user enabled voice during call feature
            && voicePhone.getDataSettingsManager().isDataEnabled();
}

必须满足:

  1. 主数据卡已启用数据
  2. 通话卡已启用数据
  3. 系统支持通话中数据切换
  4. 不是 IWLAN 或 Cross-SIM 通话

切换时机

  • 呼叫开始EVENT_PRECISE_CALL_STATE_CHANGED
  • 呼叫结束:恢复到默认数据卡

四、紧急呼叫数据切换(Emergency DDS Override) 🚨

场景说明

紧急呼叫(如 911/112)时,可能需要临时切换数据卡以支持:

  • E-GNSS 辅助定位
  • 紧急回呼(ECBM - Emergency Callback Mode)
  • IMS 紧急呼叫

覆盖请求

EmergencyOverrideRequest:
- mPhoneId: 紧急呼叫使用的 Phone ID
- mGnssOverrideTimeMs: 呼叫结束后保持时长
- mRequiresEcmFinish: 是否等待 ECBM 结束

处理流程

  1. 接收紧急覆盖请求
  2. 立即切换数据到紧急呼叫卡
  3. 监听呼叫状态变化
  4. 呼叫结束后启动定时器
  5. 超时或 ECBM 结束后恢复原数据卡

五、状态机和网络请求处理 🔄

PhoneState 状态

protected static class PhoneState {
    public volatile boolean active = false;
    public long lastRequested = 0;
}
  • active = true: Phone 允许 PS Attach(数据连接)
  • active = false: Phone 不允许数据连接

网络请求监听

PhoneSwitcherNetworkRequestListener extends NetworkFactory

处理网络请求:

  • needNetworkFor(): 新的网络请求到达
  • releaseNetworkFor(): 网络请求释放
  • 触发 onEvaluate() 重新评估数据卡分配

六、关键数据结构

变量含义
mPrimaryDataSubId用户设置的默认数据订阅 ID
mAutoSelectedDataSubId自动选择的数据订阅 ID(自动切换场景)
mPreferredDataPhoneId当前首选数据 Phone ID
mPreferredDataSubId当前首选数据订阅 ID
mPhoneIdInVoiceCall当前通话的 Phone ID
mPhoneSubscriptions[]每个 Phone 对应的订阅 ID
mPhoneStates[]每个 Phone 的激活状态
mEmergencyOverride紧急呼叫覆盖请求

七、完整流程时序图

用户切换数据卡
    ↓
Settings.Global 更新
    ↓
SubscriptionManagerService.setDefaultDataSubId()
    ↓
广播 ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED
    ↓
MultiSimSettingController.EVENT_DEFAULT_DATA_SUBSCRIPTION_CHANGED
    ↓
协调多卡数据开关设置
    ↓
PhoneSwitcher.EVENT_PRIMARY_DATA_SUB_CHANGEDonEvaluate(REQUESTS_CHANGED, "primary data sub changed")
    ↓
updatePreferredDataPhoneId()
    ↓
检测到 mPreferredDataPhoneId 变化
    ↓
sendRilCommands(mPreferredDataPhoneId)
    ↓
RadioConfig.setPreferredDataModem() 或 Phone.setDataAllowed()
    ↓
RIL → HAL → Modem
    ↓
Modem 执行数据卡切换
    ↓
EVENT_MODEM_COMMAND_DONE
    ↓
onDdsSwitchResponse()
    ↓
notifyPreferredDataSubIdChanged()
    ↓
DataNetworkController 更新数据网络
    ↓
重建 PDN 连接在新数据卡上
    ↓
ConnectivityService 更新默认网络
    ↓
完成切换

八、异常处理和重试机制

重试场景

  1. 通话中失败:等待通话结束后重试
  2. Radio 不可用:等待 Radio 恢复后重试
  3. 命令超时:5 秒后重试

重试间隔

MODEM_COMMAND_RETRY_PERIOD_MS = 5000  // 5秒

失败记录

mCurrentDdsSwitchFailure.get(phoneId)

记录每个 Phone 的失败原因,用于智能重试策略。


九、日志和调试

关键日志标签

  • PhoneSwitcher: 数据卡切换核心
  • MultiSimSettingController: 多卡设置协调
  • DataNetworkController: 数据网络控制

本地日志

mLocalLog.log("evaluating due to " + reason)

使用 LocalLog 记录关键事件,可通过 dumpsys 查看:

adb shell dumpsys phone

这就是 Android 双卡设备中默认数据卡切换的完整流程!整个系统设计考虑了用户手动切换、自动智能切换、通话中数据保障、紧急呼叫支持等多种场景,是一个非常复杂但健壮的机制。