Google 致力于为黑人社区推动种族平等。查看具体举措

实现 eSIM

利用嵌入式 SIM(又称 eSIM 或 eUICC)技术,移动用户可以在没有实体 SIM 卡的情况下,下载运营商配置文件并激活运营商服务。该技术是由 GSMA 推动的全球规范,支持在任何移动设备上进行远程 SIM 配置 (RSP)。从 Android 9 开始,Android 框架为访问 eSIM 和管理 eSIM 上的订阅配置文件提供了标准 API。借助这些 eUICC API,第三方可以在支持 eSIM 的 Android 设备上开发自己的运营商应用和 Local Profile Assistant (LPA)。

LPA 是一款独立的系统应用,应包含在 Android 构建映像中。eSIM 上的配置文件通常由 LPA 管理,因为 LPA 充当着 SM-DP+(准备、存储配置文件包并将其提供给设备的远程服务)和 eUICC 芯片之间的桥梁。LPA APK 可以视需要包含一个界面组件(又称 LPA 界面,简称 LUI),以便为最终用户提供一个中心位置来管理所有嵌入式订阅配置文件。Android 框架可自动发现可用的最佳 LPA 并与之连接,然后通过 LPA 实例路由所有 eUICC 操作。

简化的远程 SIM 配置 (RSP) 架构

图 1. 经过简化的 RSP 架构

有兴趣开发运营商应用的移动网络运营商可以参阅 EuiccManager 中的 API,其中提供了纲要式配置文件管理操作(例如 downloadSubscription()switchToSubscription()deleteSubscription())。

如果您是有兴趣自行开发 LPA 系统应用的设备 OEM,那么您必须扩展 EuiccService,以便 Android 框架能够连接到您的 LPA 服务。此外,您还应使用 EuiccCardManager 中的 API,这些 API 提供了基于 GSMA RSP v2.0 的 ES10x 函数。此类函数用于向 eUICC 芯片发出命令(例如 prepareDownload()loadBoundProfilePackage()retrieveNotificationList()resetMemory())。

EuiccManager 中的 API 需要一个正确实现的 LPA 应用才能正常运行,且 EuiccCardManager API 的调用方必须是 LPA。这是 Android 框架的强制要求。

搭载 Android 10 或更高版本的设备可以支持具有多个 eSIM 卡的设备。如需了解详情,请参阅支持多个 eSIM 卡

开发运营商应用

借助 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 回调,因为这些 API 操作可能需要数秒甚至几分钟的时间才能完成。系统会发送 PendingIntent,并在 EuiccManager#EMBEDDED_SUBSCRIPTION_RESULT_ 空间中提供结果代码,表示由框架指定的错误代码以及传播自 LPA 的任意详细结果代码 EXTRA_EMBEDDED_SUBSCRIPTION_DETAILED_CODE,使运营商应用可以跟踪结果代码来完成日志记录/调试。PendingIntent 回调必须是 BroadcastReceiver

如需下载指定的可下载订阅内容(通过激活码或二维码创建),请执行以下代码:

// 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 resolve all the resolvable errors. */
public static final String ACTION_RESOLVE_RESOLVABLE_ERRORS =
        "android.service.euicc.action.RESOLVE_RESOLVABLE_ERRORS";

运营商权限

如果您打算自行开发运营商应用,该应用会调用 EuiccManager 以将配置文件下载到设备上,那么您的配置文件应在元数据中包含与您的运营商应用对应的运营商权限规则。这是因为属于不同供应商的订阅配置文件可在设备的 eUICC 中共存,但每个运营商应用只能访问它自己拥有的配置文件。例如,运营商 A 应该无法下载、启用或停用运营商 B 拥有的配置文件。

为确保配置文件只能供其所有者访问,Android 会根据一种机制向配置文件所有者的应用(即运营商应用)授予特殊权限。Android 平台会加载存储在配置文件的访问规则文件 (ARF) 中的证书,并向由这些证书签名的应用授予权限,以允许其调用 EuiccManager API。总体流程如下所述:

  1. 运营商签署运营商应用 APK;apksigner 工具将公钥证书附加到 APK。
  2. 运营商/SM-DP+ 准备配置文件及其元数据,其中的 ARF 包含以下内容:

    1. 运营商应用的公钥证书的签名(SHA-1 或 SHA-256)(必需)
    2. 运营商应用的软件包名称(可选)
  3. 运营商应用尝试通过 EuiccManager API 执行 eUICC 操作。

  4. Android 平台会验证调用方应用的证书 SHA-1 或 SHA-256 哈希值是否与从目标配置文件的 ARF 中获取的证书签名相匹配。如果运营商应用的软件包名称包含在该 ARF 中,那么它也必须与调用方应用的软件包名称相匹配。

  5. 对签名和软件包名称(如果包含)进行验证后,系统会通过目标配置文件向调用方应用授予运营商权限。

由于配置文件元数据可在配置文件之外使用(以便 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] OCTET 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 功能的终端功能(本地配置文件管理和配置文件下载)。它还需要实现以下方法:

  • IRadio HAL v1.1:setSimPower
  • IRadio HAL v1.2:getIccCardStatus

  • IRadioConfig HAL v1.0:getSimSlotsStatus

调制解调器应将已启用默认启动配置文件的 eSIM 卡识别为有效 SIM 卡,并使该 SIM 卡保持正常使用状态。

对于搭载 Android 10 的设备,必须定义不可移除的 eUICC 插槽 ID 数组。如需查看示例,请参阅 arrays.xml

<resources>
   <!-- Device-specific array of SIM slot indexes which are are embedded eUICCs.
        e.g. If a device has two physical slots with indexes 0, 1, and slot 1 is an
        eUICC, then the value of this array should be:
            <integer-array name="non_removable_euicc_slots">
                <item>1</item>
            </integer-array>
        If a device has three physical slots and slot 1 and 2 are eUICCs, then the value of
        this array should be:
            <integer-array name="non_removable_euicc_slots">
               <item>1</item>
               <item>2</item>
            </integer-array>
        This is used to differentiate between removable eUICCs and built in eUICCs, and should
        be set by OEMs for devices which use eUICCs. -->

   <integer-array name="non_removable_euicc_slots">
       <item>1</item>
   </integer-array>
</resources>

如需查看完整的调制解调器要求列表,请参阅为支持 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。系统会为具有 android.permission.WRITE_EMBEDDED_SUBSCRIPTIONS 权限的所有应用查询 PackageManager,该权限为 android.service.euicc.EuiccService 操作指定了一项服务。系统将选择优先级最高的服务,如果未找到任何服务,则会停用 LPA 支持。

如需实现 LUI,您必须为以下操作提供一个 Activity:

  • android.service.euicc.action.MANAGE_EMBEDDED_SUBSCRIPTIONS
  • android.service.euicc.action.PROVISION_EMBEDDED_SUBSCRIPTION

与服务一样,每个 Activity 都必须获得 android.permission.BIND_EUICC_SERVICE 系统权限,而且都应当包含具有适当操作的 intent 过滤器,并属于 android.service.euicc.category.EUICC_UI 类别以及非零优先级。选择这些 Activity 的实现方案与选择 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 Activity)取决于设计选择。

EuiccCardManager

EuiccCardManager 是用于与 eSIM 芯片进行通信的接口。它可提供 ES10 函数(如 GSMA RSP 规范中所述)并处理低级 APDU 请求/响应命令以及 ASN.1 解析。EuiccCardManager 是一种系统 API,只能由具有系统权限的应用调用。

运营商应用、LPA 和 Euicc 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);

通过运营商应用激活 eSIM 卡配置文件

在搭载 Android 9 或更高版本的设备上,您可以使用运营商应用激活 eSIM 卡并下载配置文件。运营商应用可以通过直接调用 downloadSubscription 或向 LPA 提供激活码的方式下载配置文件。

如果运营商应用通过调用 downloadSubscription 下载配置文件,该调用会强制要求应用可以通过 BF76 元数据标记管理配置文件,该标记编码了配置文件的运营商权限规则。如果配置文件没有 BF76 标记或者其 BF76 标记与执行调用的运营商应用的签名不符,那么下载会遭拒。

以下部分介绍了如何使用激活码通过运营商应用激活 eSIM 卡。

使用激活码激活 eSIM 卡

如果使用激活码激活 eSIM 卡配置文件,LPA 会从运营商应用那获取激活码并下载该配置文件。此流程可由 LPA 发起,并且 LPA 可控制整个界面流程,也就是说不会显示运营商应用界面。这种方法绕过了 BF76 标记检查,网络运营商无需实现整个 eSIM 卡激活界面流程,包括下载 eSIM 卡配置文件和处理错误。

定义运营商 eUICC 配置服务

LPA 和运营商应用通过 ICarrierEuiccProvisioningServiceIGetActivationCodeCallback 这两个 AIDL 接口通信。运营商应用必须实现 ICarrierEuiccProvisioningService 接口,并在其清单声明中公开该接口。LPA 必须绑定到 ICarrierEuiccProvisioningService 并实现 IGetActivationCodeCallback。如需详细了解如何实现和公开 AIDL 接口,请参阅定义 AIDL 接口

如需定义 AIDL 接口,请同时为 LPA 和运营商应用创建以下 AIDL 文件:

  • ICarrierEuiccProvisioningService.aidl

    package android.service.euicc;
    
    import android.service.euicc.IGetActivationCodeCallback;
    
    oneway interface ICarrierEuiccProvisioningService {
        // The method to get the activation code from the carrier app. The caller needs to pass in
        // the implementation of IGetActivationCodeCallback as the parameter.
        void getActivationCode(in IGetActivationCodeCallback callback);
    
        // The method to get the activation code from the carrier app. The caller needs to pass in
        // the activation code string as the first parameter and the implementation of
        // IGetActivationCodeCallback as the second parameter. This method provides the carrier
        // app the device EID which allows a carrier to pre-bind a profile to the device's EID before
        // the download process begins.
        void getActivationCodeForEid(in String eid, in IGetActivationCodeCallback callback);
    }
    
    
  • IGetActivationCodeCallback.aidl

    package android.service.euicc;
    
    oneway interface IGetActivationCodeCallback {
        // The call back method needs to be called when the carrier app gets the activation
        // code successfully. The caller needs to pass in the activation code string as the
        // parameter.
        void onSuccess(String activationCode);
    
        // The call back method needs to be called when the carrier app failed to get the
        // activation code.
        void onFailure();
    }
    

LPA 实现示例

如需绑定到运营商应用的 ICarrierEuiccProvisioningService 实现,LPA 必须将 ICarrierEuiccProvisioningService.aidlIGetActivationCodeCallback.aidl 都复制到您的项目中并实现 ServiceConnection

@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
    mCarrierProvisioningService = ICarrierEuiccProvisioningService.Stub.asInterface(iBinder);
}

在绑定到运营商应用的 ICarrierEuiccProvisioningService 实现后,LPA 会调用 getActivationCodegetActivationCodeForEid,通过传递 IGetActivationCodeCallback Stub 类的实现从运营商应用获取激活码。

getActivationCodegetActivationCodeForEid 之间的区别在于,getActivationCodeForEid 允许运营商在下载流程开始之前,将配置文件预先绑定到设备的 EID。

void getActivationCodeFromCarrierApp() {
    IGetActivationCodeCallback.Stub callback =
            new IGetActivationCodeCallback.Stub() {
                @Override
                public void onSuccess(String activationCode) throws RemoteException {
                    // Handle the case LPA success to get activation code from a carrier app.
                }

                @Override
                public void onFailure() throws RemoteException {
                    // Handle the case LPA failed to get activation code from a carrier app.
                }
            };

    try {
        mCarrierProvisioningService.getActivationCode(callback);
    } catch (RemoteException e) {
        // Handle Remote Exception
    }
}

运营商应用的实现示例

若要让 LPA 绑定到运营商应用,运营商应用必须将 ICarrierEuiccProvisioningService.aidlIGetActivationCodeCallback.aidl 都复制到您的项目中,并在 AndroidManifest.xml 文件中声明 ICarrierEuiccProvisioningService 服务。此服务必须请求 android.permission.WRITE_EMBEDDED_SUBSCRIPTIONS 系统权限,以确保只有 LPA 这款系统特权应用才能与其绑定。此服务还必须包含具有 android.service.euicc.action.BIND_CARRIER_PROVISIONING_SERVICE 操作的 intent 过滤器。

AndroidManifest.xml

<application>
  ...
  <service
      android:name=".CarrierEuiccProvisioningService"
      android:exported="true"
      android:permission="android.permission.WRITE_EMBEDDED_SUBSCRIPTIONS">
    <intent-filter>
      <action android:name="android.service.euicc.action.BIND_CARRIER_PROVISIONING_SERVICE"/>
    </intent-filter>
  </service>
  ...
</application>

若要实现 AIDL 运营商应用服务,请创建一个服务,展开 Stub 类,并实现 getActivationCodegetActivationCodeForEid 方法。然后,LPA 可以调用任一方法来获取配置文件激活码。如果从运营商的服务器成功获取激活码,运营商应用应调用 IGetActivationCodeCallback#onSuccess 并传入激活码以做出响应。如果获取失败,运营商应用应做出 IGetActivationCodeCallback#onFailure 做出响应。

CarrierEuiccProvisioningService.java

import android.service.euicc.ICarrierEuiccProvisioningService;
import android.service.euicc.ICarrierEuiccProvisioningService.Stub;
import android.service.euicc.IGetActivationCodeCallback;

public class CarrierEuiccProvisioningService extends Service {
    private final ICarrierEuiccProvisioningService.Stub binder =
        new Stub() {
            @Override
            public void getActivationCode(IGetActivationCodeCallback callback) throws RemoteException {
                String activationCode = // do whatever work necessary to get an activation code (HTTP requests to carrier server, fetch from storage, etc.)
                callback.onSuccess(activationCode);
            }

            @Override
            public void getActivationCodeForEid(String eid, IGetActivationCodeCallback callback) throws RemoteException {
                String activationCode = // do whatever work necessary (HTTP requests, fetch from storage, etc.)
                callback.onSuccess(activationCode);
            }
      }
}

在 LPA 激活流程中启动运营商应用界面

在搭载 Android 11 及更高版本的设备上,LPA 可以启动运营商应用的界面。这个功能非常实用,因为运营商应用可能需要用户提供其他信息才能向 LPA 提供激活码。例如,运营商可能会要求用户通过登录来激活其电话号码或执行其他移植服务。

以下是在 LPA 中启动运营商应用界面的流程:

  1. LPA 向包含相应操作的运营商应用软件包发送 android.service.euicc.action.START_CARRIER_ACTIVATION intent,以启动运营商应用的激活流程。(必须在清单声明中使用 android:permission="android.permission.WRITE_EMBEDDED_SUBSCRIPTIONS" 保护运营商应用接收器,避免其收到来自非 LPA 应用的 intent。)

    String packageName = // The carrier app's package name
    
    Intent carrierAppIntent =
        new Intent(“android.service.euicc.action.START_CARRIER_ACTIVATION”)
            .setPackage(packageName);
    
    ResolveInfo activity =
        context.getPackageManager().resolveActivity(carrierAppIntent, 0);
    
    carrierAppIntent
        .setClassName(activity.activityInfo.packageName, activity.activityInfo.name);
    
    startActivityForResult(carrierAppIntent, requestCode);
    
  2. 运营商应用使用自己的界面来完成工作。例如,使用户登录或将 HTTP 请求发送到运营商的后端。

  3. 运营商应用通过调用 setResult(int, Intent)finish() 对 LAP 做出响应。

    1. 如果运营商应用做出 RESULT_OK 响应,那么 LPA 会继续激活流程。如果运营商应用确定用户应扫描二维码,而不是让 LPA 绑定运营商应用的服务,那么运营商应用会用 setResult(int, Intent)(返回 RESULT_OK)以及包含布尔型 extra android.telephony.euicc.extra.USE_QR_SCANNER(设为 true)的 Intent 实例对 LAP 做出响应。然后,LPA 查看该 extra,并启动二维码扫描程序,而不是绑定运营商应用的 ICarrierEuiccProvisioningService 实现。
    2. 如果运营商应用崩溃或做出 RESULT_CANCELED 响应(默认响应代码),那么 LPA 会取消 eSIM 卡激活流程。
    3. 如果运营商应用做出除 RESULT_OKRESULT_CANCELED 以外的响应,那么 LPA 会将其视为错误。

    出于安全考虑,LPA 不应直接接受结果 intent 中提供的激活码,以确保非 LPA 调用者无法从运营商应用获取激活码。

在运营商应用中启动 LPA 激活流程

从 Android 11 开始,运营商应用可以使用 eUICC API 启动 LUI 以激活 eSIM 卡。此方法可以显示 LPA 的 eSIM 卡激活流程界面来激活 eSIM 卡配置文件。然后,LPA 会在 eSIM 卡配置文件激活完成后发送广播。

  1. LPA 必须声明一个 Activity,其中包含一个具有 android.service.euicc.action.START_EUICC_ACTIVATION 操作的 intent 过滤器。应将该 intent 过滤器的优先级设置为非零值,以防设备上存在多个实现。例如:

    <application>
      ...
    <activity
        android:name=".CarrierAppInitActivity"
        android:exported="true">
    
        <intent-filter android:priority="100">
            <action android:name="android.service.euicc.action.START_EUICC_ACTIVATION" />
        </intent-filter>
    </activity>
      ...
    </application>
    
  2. 运营商应用使用自己的界面来完成工作。例如,使用户登录或将 HTTP 请求发送到运营商的后端。

  3. 此时,运营商应用必须准备好通过其 ICarrierEuiccProvisioningService 实现提供激活码。运营商应用通过调用具有 android.telephony.euicc.action.START_EUICC_ACTIVATION 操作的 startActivityForResult(Intent, int) 启动 LPA。LPA 还会检查布尔型 extra android.telephony.euicc.extra.USE_QR_SCANNER。如果值为 true,那么 LPA 会启动二维码扫描程序,让用户扫描配置文件的二维码。

  4. 在 LPA 侧,LPA 绑定到运营商应用的 ICarrierEuiccProvisioningService 实现,以获取激活码并下载相应的配置文件。LPA 在下载期间会显示所有必需的界面元素,例如加载屏幕。

  5. 当 LPA 激活流程完成后,LPA 会向运营商应用发送一个结果代码,而运营商应用会在 onActivityResult(int, int, Intent) 中处理该结果代码。

    1. 如果 LPA 成功下载了新的 eSIM 卡配置文件,那么会做出 RESULT_OK 响应。
    2. 如果用户在 LPA 中取消了 eSIM 卡配置文件激活流程,那么 LPA 会做出 RESULT_CANCELED 响应。
    3. 如果 LPA 做出除 RESULT_OKRESULT_CANCELED 以外的响应,那么运营商应用会将该响应视为错误。

    出于安全考虑,LPA 不会直接接受所提供的 intent 中的激活码,以确保非 LPA 调用者无法从运营商应用获取激活码。

支持多个 eSIM 卡

对于搭载 Android 10 或更高版本的设备,EuiccManager 类支持具有多个 eSIM 卡的设备。具有单个 eSIM 卡且升级到 Android 10 的设备不需要对 LPA 实现进行任何修改,因为平台会自动将 EuiccManager 实例与默认 eUICC 关联。对于采用 Radio HAL 版本 1.2 或更高版本的设备,默认 eUICC 由平台决定;对于采用 Radio HAL 版本低于 1.2 的设备,默认 eUICC 由 LPA 决定。

要求

如需支持多个 eSIM 卡,设备必须具有多个 eUICC,可以是内置 eUICC,也可以是能够插入可移除 eUICC 的物理 SIM 卡插槽。

要支持多个 eSIM 卡,必须使用 Radio HAL 版本 1.2 或更高版本。建议使用 Radio HAL 版本 1.4 和 RadioConfig HAL 版本 1.2。

实现

如需支持多个 eSIM 卡(包括可移除的 eUICC 或可编程的 SIM 卡),LPA 必须实现 EuiccService,以接收与调用方提供的卡 ID 对应的插槽 ID。

arrays.xml 中指定的 non_removable_euicc_slots 资源是表示设备内置 eUICC 的插槽 ID 的一组整数。您必须指定此资源,以便平台能够确定所插入的 eUICC 是否可移除。

适用于具有多个 eSIM 卡的设备的运营商应用

在针对具有多个 eSIM 卡的设备开发运营商应用时,可以使用 EuiccManager 中的 createForCardId 方法创建固定到给定卡 ID 的 EuiccManager 对象。卡 ID 是一个整数值,用于唯一标识设备上的 UICC 或 eUICC。

如需获取设备默认 eUICC 的卡 ID,请使用 TelephonyManager 中的 getCardIdForDefaultEuicc 方法。如果 Radio HAL 版本低于 1.2,此方法会返回 UNSUPPORTED_CARD_ID;如果设备尚未读取 eUICC,会返回 UNINITIALIZED_CARD_ID

您也可从 TelephonyManager 中的 getUiccCardsInfogetUiccSlotsInfo(系统 API)以及 SubscriptionInfo 中的 getCardId 获取卡 ID。

如果某个 EuiccManager 对象已使用特定卡 ID 实例化,所有操作均会定向到具有该卡 ID 的 eUICC。当相应 eUICC 变得无法访问时(例如,当它被关闭或移除时),EuiccManager 将不再工作。

您可以使用以下代码示例创建运营商应用。

示例 1:获取有效订阅,并实例化 EuiccManager

// Get the active subscription and instantiate an EuiccManager for the eUICC which holds
// that subscription
SubscriptionManager subMan = (SubscriptionManager)
        mContext.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE);
int cardId = subMan.getActiveSubscriptionInfo().getCardId();
EuiccManager euiccMan = (EuiccManager) mContext.getSystemService(Context.EUICC_SERVICE)
            .createForCardId(cardId);

示例 2:遍历 UICC 并针对可移除的 eUICC 实例化 EuiccManager

// On a device with a built-in eUICC and a removable eUICC, iterate through the UICC cards
// to instantiate an EuiccManager associated with a removable eUICC
TelephonyManager telMan = (TelephonyManager)
        mContext.getSystemService(Context.TELEPHONY_SERVICE);
List<UiccCardInfo> infos = telMan.getUiccCardsInfo();
int removableCardId = -1; // valid cardIds are 0 or greater
for (UiccCardInfo info : infos) {
    if (info.isRemovable()) {
        removableCardId = info.getCardId();
        break;
    }
}
if (removableCardId != -1) {
    EuiccManager euiccMan = (EuiccManager) mContext.getSystemService(Context.EUICC_SERVICE)
            .createForCardId(removableCardId);
}

验证

AOSP 未附带 LPA 实现,因此,您不需要在所有 Android build 中都提供 LPA 支持(并非每款手机都支持 eSIM 卡)。所以,我们没有提供端到端的 CTS 测试用例。不过,AOSP 中提供了基本的测试用例,以确保公开的 eUICC API 在 Android build 中有效。

您应该确保 build 能够顺利通过以下 CTS 测试用例(针对公共 API):/platform/cts/tests/tests/telephony/current/src/android/telephony/euicc/cts

实现运营商应用的运营商应该执行其常规的内部质保周期测试,以确保所有实现的功能都能正常运行。运营商应用至少应能够列出同一运营商拥有的所有订阅配置文件、下载并安装配置文件、激活配置文件上的服务、在配置文件之间切换以及删除配置文件。

如果您开发自己的 LPA,则应进行更严格的测试。您应与调制解调器供应商、eUICC 芯片或 eSIM 操作系统供应商、SM-DP+ 供应商以及运营商密切合作,以解决相关问题并确保 LPA 在 RSP 架构中的互操作性。进行大量的手动测试是必不可少的。为获得最佳测试覆盖率,您应遵循 GSMA SGP.23 RSP 测试计划