嵌入式 SIM(又称 eSIM 或 eUICC)是一种最新技术,可让移动用户在没有实体 SIM 卡的情况下,下载运营商配置文件并激活运营商服务。该技术是由 GSMA 推动的全球规范,支持在任何移动设备上进行远程 SIM 配置。从 Android 9 开始,Android 框架提供了用于访问 eSIM 和管理 eSIM 上的订阅配置文件的标准 API。借助这些 eUICC API,第三方可以在支持 eSIM 的 Android 设备上开发自己的运营商应用和 Local Profile Assistant (LPA)。
LPA 是一款独立的系统应用,应包含在 Android 编译映像中。对 eSIM 上配置文件的管理通常由 LPA 完成,因为它充当着 SM-DP+(用来准备、存储配置文件包并将其交付给设备的远程服务)和 eUICC 芯片之间的桥梁。LPA APK 可以选择性地包含一个界面组件(又称 LPA 界面或 LUI),以便为最终用户提供一个中心位置来管理所有嵌入式订阅配置文件。Android 框架可自动发现可用性最高的 LPA 并与之连接,然后通过 LPA 实例路由所有 eUICC 操作。
图 1. 简化的远程 SIM 配置 (RSP) 架构
有兴趣开发运营商应用的移动网络运营商可以参阅 EuiccManager 中的 API,其中介绍了高级配置文件管理操作(例如 downloadSubscription()
、switchToSubscription()
和 deleteSubscription()
)。
如果您是有兴趣自行开发 LPA 系统应用的原始设备制造商 (OEM),那么您必须为 Android 框架扩展 EuiccService 以连接到您的 LPA 服务。此外,您还应使用 EuiccCardManager 中的 API,这些 API 提供了基于 GSMA 远程 SIM 配置 (RSP) v2.0 的 ES10x 函数。此类函数用于向 eUICC 芯片发出命令(例如 prepareDownload()
、loadBoundProfilePackage()
、retrieveNotificationList()
和 resetMemory()
)。
EuiccManager 中的 API 需要一个正确实现的 LPA 应用才能正常运行,且 EuiccCardManager API 的调用程序必须是 LPA。这是 Android 框架的强制要求。
开发运营商应用
借助 Android 9 中的 eUICC API,移动网络运营商可以开发运营商品牌的应用,以便直接管理其配置文件。这包括下载和删除运营商所拥有的订阅配置文件,以及切换到运营商所拥有的配置文件。
EuiccManager
EuiccManager
是应用与 LPA 交互的主入口点。这包括可下载、删除及切换到运营商所拥有的订阅的运营商应用。此外,这还包括 LUI 系统应用,该应用可提供用于管理所有嵌入式订阅的中心位置/界面,而且可以不同于提供 EuiccService
的应用。
要使用公共 API,运营商应用必须先通过 Context#getSystemService
获取 EuiccManager
的实例:
EuiccManager mgr = (EuiccManager) context.getSystemService(Context.EUICC_SERVICE);
在执行任何 eSIM 操作之前,您应检查设备是否支持 eSIM。如果系统已指定 android.hardware.telephony.euicc 功能且存在 LPA 软件包,EuiccManager#isEnabled()
通常会返回 true。
boolean isEnabled = mgr.isEnabled();
if (!isEnabled) {
return;
}
要获取有关 eUICC 硬件和 eSIM 操作系统版本的信息,请执行以下命令:
EuiccInfo info = mgr.getEuiccInfo();
String osVer = info.getOsVersion();
很多 API(例如 downloadSubscription()
和 switchToSubscription()
)都会使用 PendingIntent
回调,因为回调操作可能需要数秒甚至几分钟的时间才能完成。系统会使用 EuiccManager#EMBEDDED_SUBSCRIPTION_RESULT_
空间中的结果代码发送 PendingIntent
,该代码提供了由框架指定的错误代码,以及传播自 LPA 的任意详细结果代码 EXTRA_EMBEDDED_SUBSCRIPTION_DETAILED_CODE
,这使得运营商应用可以通过跟踪来完成日志记录/调试操作。PendingIntent
必须是 BroadcastReceiver
。
要下载指定的 DownloadableSubscription
(通过激活码或二维码创建),请执行以下命令:
// Register receiver.
static final String ACTION_DOWNLOAD_SUBSCRIPTION = "download_subscription";
static final String LPA_DECLARED_PERMISSION
= "com.your.company.lpa.permission.BROADCAST";
BroadcastReceiver receiver =
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (!action.equals(intent.getAction())) {
return;
}
resultCode = getResultCode();
detailedCode = intent.getIntExtra(
EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_DETAILED_CODE,
0 /* defaultValue*/);
resultIntent = intent;
}
};
context.registerReceiver(receiver,
new IntentFilter(ACTION_DOWNLOAD_SUBSCRIPTION),
LPA_DECLARED_PERMISSION /* broadcastPermission*/,
null /* handler */);
// Download subscription asynchronously.
DownloadableSubscription sub = DownloadableSubscription
.forActivationCode(code /* encodedActivationCode*/);
Intent intent = new Intent(action);
PendingIntent callbackIntent = PendingIntent.getBroadcast(
getContext(), 0 /* requestCode */, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
mgr.downloadSubscription(sub, true /* switchAfterDownload */,
callbackIntent);
要切换到某条订阅内容(已指定订阅 ID),请执行以下命令:
// Register receiver.
static final String ACTION_SWITCH_TO_SUBSCRIPTION = "switch_to_subscription";
static final String LPA_DECLARED_PERMISSION
= "com.your.company.lpa.permission.BROADCAST";
BroadcastReceiver receiver =
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (!action.equals(intent.getAction())) {
return;
}
resultCode = getResultCode();
detailedCode = intent.getIntExtra(
EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_DETAILED_CODE,
0 /* defaultValue*/);
resultIntent = intent;
}
};
context.registerReceiver(receiver,
new IntentFilter(ACTION_SWITCH_TO_SUBSCRIPTION),
LPA_DECLARED_PERMISSION /* broadcastPermission*/,
null /* handler */);
// Switch to a subscription asynchronously.
Intent intent = new Intent(action);
PendingIntent callbackIntent = PendingIntent.getBroadcast(
getContext(), 0 /* requestCode */, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
mgr.switchToSubscription(1 /* subscriptionId */, callbackIntent);
有关 EuiccManager
API 和代码示例的完整列表,请参阅 eUICC API。
可解决的错误
有时,系统无法完成 eSIM 操作,但用户可以直接解决由此导致的错误。例如,如果配置文件元数据提示需要运营商确认码,则 downloadSubscription
可能会失败。或者,如果运营商应用具备目标配置文件的运营商权限(即运营商拥有该配置文件),但它不具备当前所启用配置文件的运营商权限(因此需要征得用户的同意),switchToSubscription
可能也会失败。
在这些情况下,调用程序使用 EuiccManager#EMBEDDED_SUBSCRIPTION_RESULT_RESOLVABLE_ERROR
进行回调。回调 Intent
将包含内部 extra,以便在调用程序将其传递给 EuiccManager#startResolutionActivity
时,可以通过 LUI 请求解决问题。再次以确认码为例,EuiccManager#startResolutionActivity
会触发 LUI 屏幕来让用户输入确认码,输入完成后,下载操作即会恢复。这种方法使得运营商应用能够完全控制在何时显示该界面,同时也为 LPA/LUI 提供了一种扩展方法,用于在将来添加处理“用户可恢复问题”的新方法(无需客户端应用做出更改)。
Android 9 在 EuiccService
中定义了 LUI 应处理的上述可修正的错误:
/**
* Alert the user that this action will result in an active SIM being
* deactivated. To implement the LUI triggered by the system, you need to define
* this in AndroidManifest.xml.
*/
public static final String ACTION_RESOLVE_DEACTIVATE_SIM =
"android.service.euicc.action.RESOLVE_DEACTIVATE_SIM";
/**
* Alert the user about a download/switch being done for an app that doesn't
* currently have carrier privileges.
*/
public static final String ACTION_RESOLVE_NO_PRIVILEGES =
"android.service.euicc.action.RESOLVE_NO_PRIVILEGES";
/** Ask the user to input carrier confirmation code. */
public static final String ACTION_RESOLVE_CONFIRMATION_CODE =
"android.service.euicc.action.RESOLVE_CONFIRMATION_CODE";
运营商权限
如果您是打算自行开发运营商应用(调用 EuiccManager
以将配置文件下载到设备上)的运营商,那么您的配置文件应在元数据中包含与您的运营商应用相对应的运营商权限规则。这是因为属于不同供应商的订阅配置文件可在同一设备的 eUICC 中共存,但每个运营商应用只能访问相应运营商拥有的配置文件。例如,运营商 A 应该无法下载、启用或停用运营商 B 拥有的配置文件。
为确保某个配置文件仅供其所有者访问,Android 采用了一种机制向配置文件所有者的应用(即运营商应用)授予特殊权限。Android 平台会加载存储在配置文件的访问规则文件 (ARF) 中的证书,并向由这些证书签名的应用授予权限,以允许其调用 EuiccManager
API。详细流程如下所述:
- 运营商签署运营商应用 APK;apksigner 工具将公钥证书附加到 APK。
运营商/SM-DP+ 准备配置文件及其元数据,其中的 ARF 包含以下内容:
- 运营商应用的公钥证书的签名(SHA-1 或 SHA-256)(必需)
- 运营商应用的软件包名称(可选)
运营商应用尝试通过
EuiccManager
API 执行 eUICC 操作。Android 平台会验证调用程序应用证书的 SHA-1 或 SHA-256 哈希是否与从目标配置文件的 ARF 中获取的证书签名相匹配。如果运营商应用的软件包名称包含在该 ARF 中,那么它也必须与调用程序应用的软件包名称相匹配。
对签名和软件包名称(如果包含)进行验证后,系统便会通过目标配置文件向调用程序应用授予运营商权限。
由于配置文件元数据可在配置文件之外使用(以便 LPA 在下载配置文件之前通过 SM-DP+ 检索配置文件元数据,或在配置文件停用后通过 ISD-R 进行检索),因此,它应当包含与该配置文件相同的运营商权限规则。
eUICC 操作系统和 SM-DP+ 必须支持配置文件元数据中的专有标记 BF76。标记内容应与 UICC 运营商权限中指定的 ARA(访问规则小程序)所返回的运营商权限规则相同:
RefArDo ::= [PRIVATE 2] SEQUENCE { -- Tag E2
refDo [PRIVATE 1] SEQUENCE { -- Tag E1
deviceAppIdRefDo [PRIVATE 1] OCTET STRING (SIZE(20|32)), -- Tag C1
pkgRefDo [PRIVATE 10] OCTET STRING (SIZE(0..127)) OPTIONAL -- Tag CA
},
arDo [PRIVATE 3] SEQUENCE { -- Tag E3
permArDo [PRIVATE 27] BIT STRING (SIZE(8)) -- Tag DB
}
}
要详细了解应用签名,请参阅为您的应用签名。要详细了解运营商权限,请参阅 UICC 运营商权限。
开发 LPA 应用
您可以实现自己的 LPA,但必须将其与 Android Euicc API 连接起来。以下部分简要概述了如何开发 LPA 应用,以及如何将其与 Android 系统集成。
硬件/调制解调器要求
eUICC 芯片上的 LPA 和 eSIM 操作系统必须支持最低 2.0 或 2.2 版的 GSMA RSP(远程 SIM 配置)。您还应当安排使用具有与之匹配的 RSP 版本的 SM-DP+ 和 SM-DS 服务器。要详细了解 RSP 架构,请参阅 GSMA SGP.21 RSP 架构规范。
此外,要与 Android 9 中的 eUICC API 集成,设备调制解调器应发送支持已编码的 eUICC 功能的终端功能(本地配置文件管理和配置文件下载)。它还需要实现以下 API:
- IRadio HAL v1.1:setSimPower
- IRadio HAL v1.2:getIccCardStatus
- IRadioConfig HAL v1.0:getSimSlotsStatus
调制解调器应将已启用默认启动配置文件的 eSIM 卡识别为有效 SIM 卡,并使该 SIM 卡保持正常使用状态。
有关调制解调器要求的完整列表,请参阅 eSIM 支持的调制解调器要求。
EuiccService
LPA 由两个独立的组件组成(可在同一 APK 中实现):LPA 后端和 LPA 界面或 LUI。
要实现 LPA 后端,您必须扩展 EuiccService
并在清单文件中声明此服务。该服务必须请求获得 android.permission.BIND_EUICC_SERVICE
系统权限,以确保只有该系统才能与之绑定。该服务还必须包含具有 android.service.euicc.EuiccService
操作的 intent 过滤器。如果设备上存在多个实现,则应将 intent 过滤器的优先级设置为非零值。例如:
<service
android:name=".EuiccServiceImpl"
android:permission="android.permission.BIND_EUICC_SERVICE">
<intent-filter android:priority="100">
<action android:name="android.service.euicc.EuiccService" />
</intent-filter>
</service>
Android 框架从内部确定处于活跃状态的 LPA,并视需要与其交互以支持 Android eUICC API。系统会查询 PackageManager
(针对具有 android.permission.WRITE_EMBEDDED_SUBSCRIPTIONS
权限的所有应用),而该权限为 android.service.euicc.EuiccService
操作指定了一项服务。系统将选择优先级最高的服务,如果未找到任何服务,则会停用 LPA 支持。
要实现 LUI,您必须为以下操作提供操作组件:
android.service.euicc.action.MANAGE_EMBEDDED_SUBSCRIPTIONS
android.service.euicc.action.PROVISION_EMBEDDED_SUBSCRIPTION
与服务一样,每个操作组件都必须获得 android.permission.BIND_EUICC_SERVICE
系统权限,而且都应当包含具有适当操作的 intent 过滤器、android.service.euicc.category.EUICC_UI
类别以及非零优先级。选择这些操作组件的实现与选择 EuiccService
实现所使用的逻辑是类似的。例如:
<activity android:name=".MyLuiActivity"
android:exported="true"
android:permission="android.permission.BIND_EUICC_SERVICE">
<intent-filter android:priority="100">
<action android:name="android.service.euicc.action.MANAGE_EMBEDDED_SUBSCRIPTIONS" />
<action android:name="android.service.euicc.action.PROVISION_EMBEDDED_SUBSCRIPTION" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.service.euicc.category.EUICC_UI" />
</intent-filter>
</activity>
这意味着,实现这些屏幕的界面的 APK 可以与实现 EuiccService
的 APK 不同。使用单个 APK 还是多个 APK(例如一个用于实现 EuiccService
,另一个用于提供 LUI 操作组件)取决于设计时的选择。
EuiccCardManager
EuiccCardManager
是用于与 eSIM 芯片进行通信的接口。它可提供 ES10 函数(如 GSMA RSP 规范中所述)并处理低级 APDU 请求/响应命令以及 ASN.1 解析。
EuiccCardManager
是一种系统 API,只能由具有系统权限的应用进行调用。
图 2. 运营商应用和 LPA 同时使用 Euicc API
通过 EuiccCardManager
实现的配置文件操作 API 要求调用程序是 LPA。这是 Android 框架的强制要求。这意味着调用程序必须扩展 EuiccService
并在清单文件中得以声明(如前几部分所述)。
与 EuiccManager
类似,要使用 EuiccCardManager
API,您的 LPA 必须先通过 Context#getSystemService
获取 EuiccCardManager
的实例:
EuiccCardManager cardMgr = (EuiccCardManager) context.getSystemService(Context.EUICC_CARD_SERVICE);
接下来,要获取 eUICC 上的所有配置文件,请执行以下命令:
ResultCallback<EuiccProfileInfo[]> callback =
new ResultCallback<EuiccProfileInfo[]>() {
@Override
public void onComplete(int resultCode,
EuiccProfileInfo[] result) {
if (resultCode == EuiccCardManagerReflector.RESULT_OK) {
// handle result
} else {
// handle error
}
}
};
cardMgr.requestAllProfiles(eid, AsyncTask.THREAD_POOL_EXECUTOR, callback);
在内部,EuiccCardManager
会通过 AIDL 接口绑定到 EuiccCardController
(在电话进程中运行),而且每个 EuiccCardManager
方法均可以通过不同的专用 AIDL 接口从电话进程接收回调。使用 EuiccCardManager
API 时,调用程序 (LPA) 必须通过所调用的回调提供 Executor
。该 Executor
可以在单个线程上运行,也可以在线程池上运行,具体取决于您的选择。
大多数 EuiccCardManager
API 都具有相同的使用模式。例如,要将绑定的配置文件包加载到 eUICC,请执行以下命令:
...
cardMgr.loadBoundProfilePackage(eid, boundProfilePackage,
AsyncTask.THREAD_POOL_EXECUTOR, callback);
要切换为具有指定 ICCID 的其他配置文件,请执行以下命令:
...
cardMgr.switchToProfile(eid, iccid, true /* refresh */,
AsyncTask.THREAD_POOL_EXECUTOR, callback);
要通过 eUICC 芯片获取默认的 SM-DP+ 地址,请执行以下命令:
...
cardMgr.requestDefaultSmdpAddress(eid, AsyncTask.THREAD_POOL_EXECUTOR,
callback);
要检索指定通知事件的通知列表,请执行以下命令:
...
cardMgr.listNotifications(eid,
EuiccNotification.Event.INSTALL
| EuiccNotification.Event.DELETE /* events */,
AsyncTask.THREAD_POOL_EXECUTOR, callback);
验证
AOSP 未附带 LPA 实现,因此,您不应在所有 Android 版本中都提供 LPA 支持(并非每款手机都支持 eSIM)。因此,我们没有提供端到端的 CTS 测试用例。不过,AOSP 中提供了基本的测试用例,以确保公开的 eUICC API 在 Android 版本中有效。
您应该确保版本通过以下 CTS 测试用例(针对公共 API):
https://android.googlesource.com/platform/cts/+/master/tests/tests/telephony/src/android/telephony/
实现运营商应用的运营商应该执行其常规的内部质量保证周期测试,以确保所有实现的功能都能正常运行。运营商应用至少应能够列出同一运营商拥有的所有订阅配置文件,下载并安装配置文件,激活配置文件上的服务,在配置文件之间切换以及删除配置文件。
如果您目前正在开发自己的 LPA,则应进行更严格的测试。您应与调制解调器供应商、eUICC 芯片或 eSIM 操作系统供应商、SM-DP+ 供应商以及运营商进行合作,以解决相关问题并确保 LPA 在 RSP 架构中的互操作性。进行大量的手动测试是必不可少的。为获得最佳测试覆盖率,您应遵循 GSMA SGP.23 RSP 测试计划。