Subscription 管理

0 阅读7分钟

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 表

所有订阅信息存储在 TelephonyProviderSimInfo 表中,内容 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 什么是活跃订阅?

当且仅当以下条件都满足时,订阅才是活跃的:

  1. SimSlotIndex >= 0(已分配到 SIM 卡槽)
  2. 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多卡设置协调,确保同组订阅设置同步
ISubAIDL 接口,定义 SubscriptionManagerService 的 IPC 方法
PhoneSwitcher多卡数据连接管理(详见多卡管理章节)

这就是 Android 电话框架中 Subscription 管理的完整体系。它是 DSDS(双卡双待)管理的基础,为系统如何跟踪、访问和协调多个 SIM 卡提供了数据模型和操作框架。

有任何关于 Subscription 管理的具体问题吗?我可以深入解释任何特定的部分。