1. 核心概念
什么是 Subscription(订阅)?
Subscription 是对设备上单个 SIM 卡或 eSIM 的逻辑表示。每个 Subscription 都有一个唯一的 SubId(Subscription ID),用来标识该订阅在系统中的身份。
关键概念:
- SubId:订阅的逻辑标识符(整数),用于应用层和框架层通信
- PhoneId:物理调制解调器的索引,也称为逻辑 SIM 卡槽索引
- IccId:SIM 卡的集成电路卡标识符(集成电路卡号)
- Active Subscription:当前在 SIM 卡槽中加载并可用的订阅
Subscription 的类型
// 本地 SIM:物理 SIM 或 eSIM
public static final int SUBSCRIPTION_TYPE_LOCAL_SIM = SimInfo.SUBSCRIPTION_TYPE_LOCAL_SIM;
// 远程 SIM:通过蓝牙等连接的外部设备的 SIM
public static final int SUBSCRIPTION_TYPE_REMOTE_SIM = SimInfo.SUBSCRIPTION_TYPE_REMOTE_SIM;
2. 核心数据模型
SubscriptionInfo(公开 API 类)
38:65:frameworks_base/telephony/java/android/telephony/SubscriptionInfo.java
private final int mId; // SubId
private final String mIccId; // SIM 卡号
private final int mSimSlotIndex; // SIM 槽位索引
private final CharSequence mDisplayName; // 用户显示名称
private final CharSequence mCarrierName; // 运营商名称
private final int mDisplayNameSource; // 显示名称来源
private final int mIconTint; // 图标颜色
private final String mNumber; // 电话号码
private final int mDataRoaming; // 数据漫游设置
private final String mMcc; // 移动国家代码
private final String mMnc; // 移动网络代码
private final boolean mIsEmbedded; // 是否为 eSIM
private final ParcelUuid mGroupUuid; // 订阅组 UUID
private final int mCarrierId; // 运营商 ID
private final boolean mIsOpportunistic; // 是否为机会性订阅
private final boolean mAreUiccApplicationsEnabled; // UICC 应用是否启用
SubscriptionInfoInternal(内部使用)
这是框架内部使用的模型,完全对应 SimInfo 数据库表的每一列。
318:410:frameworks_opt_telephony/src/java/com/android/internal/telephony/subscription/SubscriptionInfoInternal.java
// 包含所有数据库字段,如 IMSI、RCS 配置、网络类型权限等
private final String mImsi;
private final String mEnabledMobileDataPolicies;
private final int mIsRcsUceEnabled;
private final int mIsCrossSimCallingEnabled;
private final byte[] mRcsConfig;
private final String mAllowedNetworkTypesForReasons;
// ... 更多字段
3. Subscription 数据库
SimInfo 表
所有订阅信息存储在 TelephonyProvider 的 SimInfo 表中,内容 URI 为 content://telephony/siminfo。
4286:4300:frameworks_base/core/java/android/provider/Telephony.java
public static final class SimInfo {
public static final Uri CONTENT_URI = Uri.parse("content://telephony/siminfo");
// 包含 50+ 列,存储所有订阅相关数据
}
SubscriptionDatabaseManager
负责 SimInfo 表的读写缓存,所有数据库访问都应通过此类进行。
84:678:frameworks_opt_telephony/src/java/com/android/internal/telephony/subscription/SubscriptionDatabaseManager.java
/**
* The subscription database manager is the wrapper of {@link SimInfo} table.
* It's a full memory cache of the entire subscription database.
*/
public class SubscriptionDatabaseManager extends Handler {
// 内存缓存,使用读写锁保护
private Map<Integer, SubscriptionInfoInternal> mAllSubscriptionInfoInternalCache;
}
4. Subscription 生命周期
4.1 Subscription 发现与创建流程
┌─────────────────────────────────┐
│ UiccController │
│ (UICC 卡管理) │
└──────────────┬──────────────────┘
│ updateSimState()
▼
┌──────────────────────────────────────────┐
│ SubscriptionManagerService │
│ updateSimState() │
└──────────────┬───────────────────────────┘
│
updateSubscription()
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
检查 IccId 查找现有订阅 创建新订阅
是否存在 (数据库) (若无则插入)
关键代码:
1499:1522:frameworks_opt_telephony/src/java/com/android/internal/telephony/subscription/SubscriptionManagerService.java
if (!TextUtils.isEmpty(iccId)) {
// 检查订阅是否已存在
subInfo = mSubscriptionDatabaseManager.getSubscriptionInfoInternalByIccId(iccId);
int subId;
if (subInfo == null) {
// 这是新 SIM 卡。插入新记录。
subId = insertSubscriptionInfo(iccId, phoneId, null,
SubscriptionManager.SUBSCRIPTION_TYPE_LOCAL_SIM);
// 设置显示名称
mSubscriptionDatabaseManager.setDisplayName(subId,
mContext.getResources().getString(R.string.default_card_name, getCardNumber(subId)));
} else {
subId = subInfo.getSubscriptionId();
log("Found existing subscription. subId= " + subId + ", phoneId=" + phoneId);
}
// 更新 SIM 槽位索引,使订阅变为活跃状态
if (subInfo != null && subInfo.areUiccApplicationsEnabled()) {
mSlotIndexToSubId.put(phoneId, subId);
mSubscriptionDatabaseManager.setSimSlotIndex(subId, phoneId);
}
}
4.2 插入新订阅
1092:1114:frameworks_opt_telephony/src/java/com/android/internal/telephony/subscription/SubscriptionManagerService.java
private int insertSubscriptionInfo(@NonNull String iccId, int slotIndex,
@Nullable String displayName, @SubscriptionType int subscriptionType) {
String defaultAllowNetworkTypes = Phone.convertAllowedNetworkTypeMapIndexToDbName(...);
SubscriptionInfoInternal.Builder builder = new SubscriptionInfoInternal.Builder()
.setIccId(iccId)
.setCardString(iccId)
.setSimSlotIndex(slotIndex)
.setType(subscriptionType)
.setIconTint(getColor())
.setAllowedNetworkTypesForReasons(defaultAllowNetworkTypes);
int subId = mSubscriptionDatabaseManager.insertSubscriptionInfo(builder.build());
return subId;
}
5. Subscription 查询与访问
5.1 公开 API(SubscriptionManager)
应用通过 SubscriptionManager 类查询订阅信息:
// 获取所有订阅信息
List<SubscriptionInfo> allSubs = subscriptionManager.getAllSubscriptionInfoList();
// 获取活跃订阅
List<SubscriptionInfo> activeSubs = subscriptionManager.getActiveSubscriptionInfoList();
// 通过 SubId 获取
SubscriptionInfo info = subscriptionManager.getActiveSubscriptionInfo(subId);
// 获取默认订阅
int defaultDataSubId = SubscriptionManager.getDefaultDataSubscriptionId();
int defaultVoiceSubId = SubscriptionManager.getDefaultVoiceSubscriptionId();
5.2 内部访问(SubscriptionDatabaseManager)
框架内部通过 SubscriptionDatabaseManager 直接访问数据库:
2442:2463:frameworks_opt_telephony/src/java/com/android/internal/telephony/subscription/SubscriptionDatabaseManager.java
@Nullable
public SubscriptionInfoInternal getSubscriptionInfoInternal(int subId) {
mReadWriteLock.readLock().lock();
try {
return mAllSubscriptionInfoInternalCache.get(subId);
} finally {
mReadWriteLock.readLock().unlock();
}
}
@NonNull
public List<SubscriptionInfoInternal> getAllSubscriptions() {
mReadWriteLock.readLock().lock();
try {
return new ArrayList<>(mAllSubscriptionInfoInternalCache.values());
} finally {
mReadWriteLock.readLock().unlock();
}
}
6. 默认订阅(Default Subscriptions)
6.1 三种默认订阅
- Default Voice SubId:用于语音通话的默认订阅
- Default Data SubId:用于移动数据的默认订阅
- Default SMS SubId:用于 SMS 的默认订阅
6.2 默认值存储
默认值存储在系统设置(Settings.Global)中:
489:500:frameworks_opt_telephony/src/java/com/android/internal/telephony/subscription/SubscriptionManagerService.java
mDefaultVoiceSubId = new WatchedInt(Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.MULTI_SIM_VOICE_CALL_SUBSCRIPTION,
SubscriptionManager.INVALID_SUBSCRIPTION_ID)) {
@Override
public boolean set(int newValue) {
int oldValue = mValue;
if (super.set(newValue)) {
logl("Default voice subId changed from " + oldValue + " to " + newValue);
Settings.Global.putInt(mContext.getContentResolver(),
Settings.Global.MULTI_SIM_VOICE_CALL_SUBSCRIPTION, newValue);
// ... 广播变化
return true;
}
return false;
}
};
6.3 自动更新默认值
MultiSimSettingController 负责在多卡场景下自动更新默认值:
619:650:frameworks_opt_telephony/src/java/com/android/internal/telephony/MultiSimSettingController.java
/**
* Automatically update default settings (data / voice / sms).
*
* 1) If the default subscription is still active, keep it unchanged.
* 2) Or if there's another active primary subscription that's in the same group,
* make it the new default value.
* 3) Or if there's only one active primary subscription, automatically set default
* data subscription on it.
* 4) If none above is met, clear the default value to INVALID.
*/
protected void updateDefaults() {
if (!isReadyToReevaluate()) return;
List<SubscriptionInfo> activeSubInfos = mSubscriptionManagerService
.getActiveSubscriptionInfoList(...);
if (ArrayUtils.isEmpty(activeSubInfos)) {
// 无活跃订阅,清除默认值
mSubscriptionManagerService.setDefaultDataSubId(INVALID_SUBSCRIPTION_ID);
mSubscriptionManagerService.setDefaultVoiceSubId(INVALID_SUBSCRIPTION_ID);
mSubscriptionManagerService.setDefaultSmsSubId(INVALID_SUBSCRIPTION_ID);
return;
}
// 自动更新...
}
7. 订阅组(Subscription Groups)
7.1 订阅组概念
订阅组用于关联多个订阅(通常是主订阅和机会性订阅),它们共享某些设置。
200:240:frameworks_base/telephony/java/com/android/internal/telephony/ISub.aidl
/**
* Inform SubscriptionManager that subscriptions in the list are bundled as a group.
* Typically it's a primary subscription and an opportunistic subscription.
* Being in the same group means they might be activated or deactivated together.
*/
ParcelUuid createSubscriptionGroup(in int[] subIdList, String callingPackage);
void addSubscriptionsIntoGroup(in int[] subIdList, in ParcelUuid groupUuid,
String callingPackage);
void removeSubscriptionsFromGroup(in int[] subIdList, in ParcelUuid groupUuid,
String callingPackage);
List<SubscriptionInfo> getSubscriptionsInGroup(in ParcelUuid groupUuid,
String callingPackage, String callingFeatureId);
7.2 组设置同步
同组内的订阅必须共享相同的数据设置:
372:394:frameworks_opt_telephony/src/java/com/android/internal/telephony/MultiSimSettingController.java
/**
* Make sure MOBILE_DATA of subscriptions in same group are synced.
*/
private void onUserDataEnabled(int subId, boolean enable, boolean setDefaultData) {
// 确保同组订阅的 MOBILE_DATA 设置同步
setUserDataEnabledForGroup(subId, enable);
SubscriptionInfo subInfo = mSubscriptionManagerService.getSubscriptionInfo(subId);
int defaultDataSubId = mSubscriptionManagerService.getDefaultDataSubId();
// 如果用户启用了非默认的非机会性订阅,将其设为默认
if (defaultDataSubId != subId && subInfo != null && !subInfo.isOpportunistic()
&& enable && subInfo.isActive() && setDefaultData) {
mSubscriptionManagerService.setDefaultDataSubId(subId);
}
}
8. 活跃与非活跃订阅
8.1 什么是活跃订阅?
当且仅当以下条件都满足时,订阅才是活跃的:
SimSlotIndex >= 0(已分配到 SIM 卡槽)- UICC 应用已启用(
areUiccApplicationsEnabled() == true)
1517:1522:frameworks_opt_telephony/src/java/com/android/internal/telephony/subscription/SubscriptionManagerService.java
if (subInfo != null && subInfo.areUiccApplicationsEnabled()) {
mSlotIndexToSubId.put(phoneId, subId);
// 更新 SIM 槽位索引。这将使订阅变为活跃状态。
mSubscriptionDatabaseManager.setSimSlotIndex(subId, phoneId);
}
8.2 非活跃订阅的 SimSlotIndex
非活跃订阅的 SimSlotIndex 被设置为 SubscriptionManager.INVALID_SIM_SLOT_INDEX (-1)。
9. 机会性订阅(Opportunistic Subscriptions)
机会性订阅是自动选择的订阅,用于特定场景(如某些地区有更便宜的数据计划)。
boolean isOpportunistic = subscriptionInfo.isOpportunistic();
// 机会性订阅不能是默认订阅
// 它们通常与主订阅组合使用
10. 关键流程:SIM 插入/移除
10.1 SIM 插入流程
SIM Card Inserted
│
▼
UiccController detects state
│
▼
UiccController.updateSimState()
│
▼
SubscriptionManagerService.updateSimState()
│
┌────────────┼────────────┐
▼ ▼ ▼
检查 IccId 数据库查询 创建/更新
是否新 SIM 现有订阅 订阅记录
│ │ │
└────────────┼────────────┘
▼
updateSubscription() completes
│
┌──────────┼──────────┐
▼ ▼ ▼
更新显示名 更新运营商 广播变化
更新位置代码 更新电话号码 通知应用
关键代码在 updateSubscription() 方法中:
1536:1558:frameworks_opt_telephony/src/java/com/android/internal/telephony/subscription/SubscriptionManagerService.java
if (simState == TelephonyManager.SIM_STATE_LOADED) {
// 获取 MCC/MNC 信息
String mccMnc = mTelephonyManager.getSimOperatorNumeric(subId);
if (!TextUtils.isEmpty(mccMnc)) {
if (subId == getDefaultSubId()) {
MccTable.updateMccMncConfiguration(mContext, mccMnc);
}
setMccMnc(subId, mccMnc);
}
// 获取国家代码
String iso = TelephonyManager.getSimCountryIsoForPhone(phoneId);
if (!TextUtils.isEmpty(iso)) {
setCountryIso(subId, iso);
}
// 获取电话号码
String msisdn = PhoneFactory.getPhone(phoneId).getLine1Number();
if (!TextUtils.isEmpty(msisdn)) {
setDisplayNumber(msisdn, subId);
}
}
10.2 SIM 移除流程
SIM Card Removed
│
▼
UiccController detects ABSENT
│
▼
SubscriptionManagerService.updateSimState()
│
▼
markSubscriptionsInactive(phoneId)
│
┌──────────────┼──────────────┐
▼ ▼ ▼
设置 SimSlotIndex 清除显示名 清除运营商
为 INVALID(-1) 清除位置信息 通知更新
11. 权限和访问控制
11.1 权限要求
// 读取订阅信息
@RequiresPermission(anyOf = {
Manifest.permission.READ_PHONE_STATE,
"carrier privileges"
})
// 修改订阅信息
@RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE)
// 读取设备标识符(获取 IccId、GroupUuid)
@RequiresPermission(Manifest.permission.READ_PHONE_NUMBERS)
11.2 运营商特权访问
具有运营商特权的应用可以访问相关订阅的更多信息。
12. SubscriptionManager 缓存机制
为了提高性能,SubscriptionManager 使用了 PropertyInvalidatedCache:
private static final PropertyInvalidatedCache<Integer, Integer> sGetSubIdCache =
new PropertyInvalidatedCache<Integer, Integer>(16, CACHE_PROPERTY_PREFIX + "subscription") {
@Override
protected Integer recompute(Integer slotIndex) {
// 查询数据库或从 SubscriptionManagerService 获取
}
};
当订阅信息变化时,缓存会被自动清除。
13. 实践:查询和使用订阅
// 获取 SubscriptionManager
SubscriptionManager subscriptionManager =
context.getSystemService(SubscriptionManager.class);
// 获取所有活跃订阅
List<SubscriptionInfo> activeSubscriptions =
subscriptionManager.getActiveSubscriptionInfoList();
for (SubscriptionInfo subInfo : activeSubscriptions) {
int subId = subInfo.getSubscriptionId();
String displayName = subInfo.getDisplayName().toString();
int simSlotIndex = subInfo.getSimSlotIndex();
String iccId = subInfo.getIccId();
String carrierName = subInfo.getCarrierName().toString();
Log.d(TAG, "SubId: " + subId + ", Name: " + displayName
+ ", Slot: " + simSlotIndex + ", Carrier: " + carrierName);
}
// 为特定订阅创建 TelephonyManager
TelephonyManager telManager =
context.getSystemService(TelephonyManager.class)
.createForSubscriptionId(subId);
// 获取该订阅的网络运营商
String operatorName = telManager.getNetworkOperatorName();
14. 关键类总结
| 类 | 作用 |
|---|---|
SubscriptionManager | 公开 API,应用端查询订阅信息 |
SubscriptionManagerService | 系统服务,管理所有订阅的后端 |
SubscriptionDatabaseManager | 订阅数据库管理,缓存和访问 SimInfo 表 |
SubscriptionInfo | 公开订阅信息模型 |
SubscriptionInfoInternal | 内部订阅信息模型,完整对应数据库 |
MultiSimSettingController | 多卡设置协调,确保同组订阅设置同步 |
ISub | AIDL 接口,定义 SubscriptionManagerService 的 IPC 方法 |
PhoneSwitcher | 多卡数据连接管理(详见多卡管理章节) |
这就是 Android 电话框架中 Subscription 管理的完整体系。它是 DSDS(双卡双待)管理的基础,为系统如何跟踪、访问和协调多个 SIM 卡提供了数据模型和操作框架。
有任何关于 Subscription 管理的具体问题吗?我可以深入解释任何特定的部分。