Я люблю свою работу, потому что на удивление для меня с годами она становится все более нетривиальной и увлекательной. Моя история внедрения автоматической установки eSim была интересна тем, что разрабатывалась вслепую без возможности протестировать функционал на момент разработки. О eSIM и о своей истории и пойдет речь в данной статье.

Основные понятия

Начиная с Android 9 (API level 28) появилась возможность работать с eSIM. Данная функциональность может быть внедрена в приложения Мобильного оператора, либо как отдельная функциональность существующего приложения, которое может быть не связано напрямую с Мобильным оператором. Для внедрения не потребуются дополнительные разрешения в манифесте приложения, поэтому можно не так сильно беспокоится за процесс ревью на стороне магазина приложения.

Давайте рассмотрим архитектуру RSP (remote SIM provisioning).

image.png

LPA (Local Profile Assistant) - это сущность, которая служит универсальным механизмом по работе с профилями eSIM на устройстве. LPA состоит из двух компонентов:

  • Backend. Работает с API eUICC для управления eSIM на устройстве

  • Frontend. LPA UI или LUI. Визуальная часть системы для взаимодействия с пользователем, например, когда необходимо дополнительное разрешение или действие от пользователя.

LPA взаимодействует с eUICC (embedded Universal Integrated Circuit Card). eUICC позволяет загружать, хранить и переключать профили eSIM без наличия физической SIM-карты. AOSP не предоставляет дефолтной реализации LPA, именно поэтому не все телефоны поддерживают работу с eSIM. SM-DP+ (Subscription Manager – Data Preparation) это платформа, которая предоставляет доступ к eSIM профилям. Operator на схеме это компания мобильного оператора, которая формируем eSIM профиля и поставляет в SM-DP+ для безопасного распространения. Operator так же взаимодействует с End User с целью продажи своих продуктов.

Хочу отметить, что существует возможность создать свою реализацию LPA на устройстве через создания своей backend (расширив возможности android.service.euicc.EuiccService ), а так же реализовать свой LUI (через создания своей Activity с определенным набором параметров в intent-filter ). В данной статье я не буду говорить про этот способ, а речь пойдет про использование LPA, которая присутствует на устройстве. Начнем со скачивания eSIM профиля на устройство.

Скачиваем eSIM профиль

Если устройство поддерживает eSIM, что можно проверить через обращения к EuiccManager и его свойству isEnabled , то вам необходимо получить activationCode для скачивания профиля eSIM. Данный код вы можете получить, например, сделав сетевой запрос внутри функциональности вашего приложения. ActivationCode состоит из двух частей: smdp address и activation token.

val smdpAddress = "SMDP.GSMA.COM"
val activationToken = "04386-AGYFT-A74Y8-3F815"
val activationCode = "1\\$$smdpAddress\\$$activationToken"

Далее подготавливаем подписку для скачивания

val subscription = DownloadableSubscription.forActivationCode(activationCode)
val intent = EsimInstallerBroadcastReceiver.createStartIntent(context)
val pendingIntent = EsimInstallerBroadcastReceiver.createCallbackIntent(
		context, 
		intent,
)

В коде выше я формирую pendingIntent для того, чтобы получить callback по результату скачивания профиля eSIM. Внутри EsimInstallerBroadcastReceiver находятся методы:

const val DOWNLOAD_ACTION = "download_subscription"

fun createStartIntent(context: Context): Intent {
		return Intent(DOWNLOAD_ACTION).setPackage(context.packageName)
}

fun createCallbackIntent(context: Context, startIntent: Intent): PendingIntent {
		return PendingIntent.getBroadcast(
		/* context = */ context,
        /* requestCode = */ 0,
        /* intent = */ startIntent,
        /* flags = */ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
     )
}

После этого я могу инициировать скачивание профиля через

val euiccManager = context.getSystemService(EUICC_SERVICE) as EuiccManager
euiccManager.downloadSubscription(
    /* subscription = */ subscription,
    /* switchAfterDownload = */ true,
    /* callbackIntent = */ callbackIntent,
)

Для удобства можно установить флаг switchAfterDownload, позволяющий автоматически переключиться на установленный профиль. Однако при необходимости, можно реализовать включение загруженного профиля отдельным методом. Подробнее об этом и о других возможностях eUICC можно почитать в документации.

Метод downloadSubscription требует наличия android.Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS , либо ваше приложение должны обладать правами по управлению и текущим активным профилем, и профилем, который будет скачиваться. Если условия не выполняются, то в EsimInstallerBroadcastReceiver в метод onRecieve придет resultCode равный EuiccManager.EMBEDDED_SUBSCRIPTION_RESULT_RESOLVABLE_ERROR . Давайте теперь рассмотрим, как мы можем решить данную “Resolvable error”.

Обработка ошибок скачивания профиля

После запуска скачивания профиля в onReceive нашего EsimInstallerBroadcastReceiver придут результаты, которые нам необходимо будет обработать.

override fun onReceive(context: Context, intent: Intent) {
    when {
        DOWNLOAD_ACTION != intent.action -> return
        resultCode == EuiccManager.EMBEDDED_SUBSCRIPTION_RESULT_OK -> {
            onEmbeddedSubscriptionSucceed()
        }
        resultCode == EuiccManager.EMBEDDED_SUBSCRIPTION_RESULT_RESOLVABLE_ERROR -> {
            onStartResolutionActivity(intent)
        }
        else -> onEmbeddedSubscriptionFailed()
    }
}

В коде выше я вызываю соответствующие callback, которые паредаю в момент создания EsimInstallerBroadcastReceiver в Acitvity своей функциональности. При успехе или ошибке вы можете показать экран успеха пользователю и закончить флоу функциональности. Данное событие можно легко встроить в любую архитектуру: если у вас MVP, то вызовите View, которая в свою очередь передаст событие в Presentor; если у вас MVVP, то вызовите view, которая передаст событие во ViewModel; если у вас MVI, то вызовите сущность, отвечающую в вашей архитектуре за представление и передайте соответствующий Wish.

Для того, чтобы запустить процесс решения ��шибок, которые возможно решить с помощью пользователя (то есть пользователю будет показан один или несколько диалогов с запросом разрешения на те или иные действия), нам нужно сново сформировать pendingIntent для работы с результатом решения ошибок:

val startIntent = EsimErrorResolverBroadcastReceiver.createStartIntent(activity)
val callbackIntent = EsimErrorResolverBroadcastReceiver.createCallbackIntent(
		activity,
		startIntent,
)

Где внутри EsimErrorResolverBroadcastReceiver находится

const val START_RESOLUTION_ACTION = "start_resolution_action"

fun createStartIntent(context: Context): Intent {
    return Intent(START_RESOLUTION_ACTION).setPackage(context.packageName)
}

fun createCallbackIntent(context: Context, startIntent: Intent): PendingIntent {
    return PendingIntent.getBroadcast(
        /* context = */ context,
        /* requestCode = */ 0,
        /* intent = */ startIntent,
        /* flags = */ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
    )
}

После чего мы может запустить процесс решения разрешимых ошибок

euiccManager.startResolutionActivity(
    /* activity = */ activity,
    /* requestCode = */ 0,
    /* resultIntent = */ resultIntent, 
    /* callbackIntent = */ callbackIntent,
)

Важно отметить, что resultIntent - это Intent , который пришел в EsimInstallerBroadcastReceiver вместе с ошибкой EMBEDDED_SUBSCRIPTION_RESULT_RESOLVABLE_ERROR .

После того, как пользователь закончит работу с LUI, результат разрешения ошибок придет в метод onRecieve нашего EsimErrorResolverBroadcastReceiver . Внутри которого можно разместить вызов callback успеха или ошибки

override fun onReceive(context: Context, intent: Intent) {
    when {
        START_RESOLUTION_ACTION != intent.action -> return
        resultCode == EuiccManager.EMBEDDED_SUBSCRIPTION_RESULT_OK -> onEmbeddedSubscriptionSucceed()
        else -> onEmbeddedSubscriptionFailed()
    }
}

Полученный intent можно разобрать на параметры и отправить в систему логирования при необходимости. В��с могут заинтересовать такие параметры как

Мы прошли путь от скачивания профиля до его установки через разрешения ошибок пользователем. Но почему профиль все равно не будет установлен?

Выдача оператором прав на работу с профилями

Поскольку оператор имеет права только на свои профиля, в рамках системы есть ограничения на доступ к разным профилям. Для того, чтобы доступы на установку профиля появились у нашего приложения, мы должны передать оператору:

  1. Обязательно. Подпись (SHA-1 или SHA-256) сертификата публичного ключа нашего приложения.

  2. Необязательно, но строго рекомендуется. Пакет приложения.

Таким образом при наличии данных значений у SM-DP+ и внутри профиля система выдаст нашему приложения доступы на работу с соответствующим профилем eSIM.

Поддержки нескольких активных профилей

Начиная с Android 13 (API level 33) появилась поддержка MEPs (multiple enabled profiles). Это позволяет пользователю иметь несколько активных профилей eSIM одновременно. Это может быть важно, если вы более тонко работаете с eUICC и управляете портами, куда будут установлены ваши профили. В моем же примере дополнительная адаптация множественных профилей не нужна из-за включенного параметра switchAfterDownload при запуске скачивания профиля. Благодаря этому параметру я могу не волноваться, в какой порт поставится мой профиль, потому что система решит это за меня по данной логике:

  1. В режиме SS (Single SIM) профиль будет загружен и установлен в существующий один слот (default port 0)

  2. В режиме DSDS будет найден первый доступный порт, в котором будет активирован профиль

  3. Если не будет активных портов, то будет отправлена ошибка EMBEDDED_SUBSCRIPTION_RESULT_RESOLVABLE_ERROR , при разрешении которой пользователю будет предложено деактивировать существующий профиль.

Моя история внедрения работы с eUICC

Мое выражение лица наверно совпадала с моментом, когда я в первый раз в жизни открыл документацию по работе Dagger2, но в этот раз у меня было куда больше уверенности, что это решаемая задача, пусть и совершенно отличная об обычных задач по изменению типографики заголовка или сбора Json для нового Server-Driven экрана. Неопределенность лишь оставалась в том, что на момент готового функционала Оператор все еще не успевал добавить необходимую подпись в свою инфраструктуру, из-за чего написанный функционал был залит в git без какой-либо возможности протестировать happy path. Эта история закончилась позитивна и к моему удивлению (я предчувствовал баги) после доработок на стороне Оператора eSIM успешно установилась у специалиста QA.

Литература