我来分析一下默认数据卡切换的流程。让我查看相关代码:
现在让我查看onEvaluate的完整实现和数据切换的核心逻辑:
非常好!现在我已经收集了足够的信息。让我整理一下默认数据卡切换的完整流程:
Android 双卡默认数据卡切换流程详解 📱📶
基于 frameworks-opt-telephony 代码分析,默认数据卡(DDS - Default Data Subscription)切换是一个复杂的多模块协同过程。
核心架构概览
用户/系统设置
↓
SubscriptionManagerService
↓
MultiSimSettingController (多卡协调)
↓
PhoneSwitcher (数据卡切换核心)
↓
DataNetworkController (数据网络控制)
↓
RadioConfig/Phone.mCi
↓
RIL → Modem (setPreferredDataModem / setDataAllowed)
一、用户手动切换流程 🔄
触发方式
- 设置界面:设置 → 网络和互联网 → SIM 卡 → 默认数据卡
- 快速设置面板:下拉通知栏切换数据卡
- 程序化调用:
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
协调规则:
- 移动数据开关协调:确保只有默认数据卡的
MOBILE_DATA开启 - 分组订阅处理:同一组的订阅共享数据和漫游设置
- 自动继承:在同组内切换时自动继承设置
步骤 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();
}
评估逻辑:
- 检查默认数据订阅 ID 是否变化
- 检查 phoneId 到 subId 的映射关系
- 更新首选数据 Phone ID(
updatePreferredDataPhoneId()) - 检测变化并决定是否需要发送命令到 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));
}
优先级顺序:
- 紧急呼叫覆盖:紧急呼叫时强制切换到特定卡
- 通话中数据切换:通话时根据配置可能切换到通话卡
- 正常数据卡:用户设置的默认数据卡
步骤 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 命令模式:
-
HAL_COMMAND_ALLOW_DATA(旧模式)
- 多个 Phone 分别调用
setDataAllowed(true/false) - 只允许默认数据卡的数据连接
- 多个 Phone 分别调用
-
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 管理:
- 主卡无信号:自动切换到有信号的副卡
- 主卡网络质量差:切换到信号更好的副卡
- 主卡数据拥塞:切换到网络负载较低的卡
- 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();
}
必须满足:
- 主数据卡已启用数据
- 通话卡已启用数据
- 系统支持通话中数据切换
- 不是 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 结束
处理流程
- 接收紧急覆盖请求
- 立即切换数据到紧急呼叫卡
- 监听呼叫状态变化
- 呼叫结束后启动定时器
- 超时或 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_CHANGED
↓
onEvaluate(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 更新默认网络
↓
完成切换
八、异常处理和重试机制
重试场景
- 通话中失败:等待通话结束后重试
- Radio 不可用:等待 Radio 恢复后重试
- 命令超时: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 双卡设备中默认数据卡切换的完整流程!整个系统设计考虑了用户手动切换、自动智能切换、通话中数据保障、紧急呼叫支持等多种场景,是一个非常复杂但健壮的机制。