Implementing eSIM Profile Switching in Mobile App
Switching between installed eSIM profiles is simpler than activation — profile already exists on eUICC, just need to make it active. But even here platforms add complexities: iOS requires user confirmation via system dialog, Android on some devices reboots radio module, taking 10–30 seconds with connection loss.
Android: EuiccManager.switchToSubscription
fun switchToProfile(subscriptionId: Int) {
// Get subscriptionId from SubscriptionManager
val activeSubInfo = subscriptionManager.activeSubscriptionInfoList
val currentEsimId = activeSubInfo?.firstOrNull { it.isEmbedded }?.subscriptionId
if (currentEsimId == subscriptionId) {
showMessage("This profile is already active")
return
}
euiccManager.switchToSubscription(
subscriptionId,
PendingIntent.getBroadcast(
context,
REQUEST_CODE_SWITCH,
Intent(ACTION_ESIM_SWITCH_COMPLETE),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
)
}
// BroadcastReceiver for result handling
private val switchReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val resultCode = intent.getIntExtra(EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_DETAILED_CODE, 0)
if (resultCode == EuiccManager.EMBEDDED_SUBSCRIPTION_RESULT_OK) {
onSwitchSuccess()
} else {
onSwitchError(resultCode)
}
}
}
switchToSubscription doesn't take callback directly — only PendingIntent. Not a bug: operation is asynchronous and may require user interaction. Result comes to BroadcastReceiver.
Switching Without System UI (Privileged)
On Samsung and some others available via android.telephony.euicc.EuiccManager.switchToSubscription(int, boolean) with forceDeactivateSim = true flag. Works only for apps with WRITE_EMBEDDED_SUBSCRIPTIONS.
// Samsung-specific flag for switching without dialog
if (Build.MANUFACTURER.equals("samsung", ignoreCase = true)) {
euiccManager.switchToSubscription(
subscriptionId,
forceDeactivateSim = true, // Don't ask user
callbackIntent = pendingIntent
)
}
Important: on some Qualcomm devices, radio reboots after switchToSubscription without warning. Warn user beforehand: "After switching, connection will drop for 15–30 seconds."
iOS: Only Through Settings or Carrier Entitlement
On iOS, programmatic switching between eSIM profiles is unavailable for regular apps. Two options:
Link to Settings: UIApplication.shared.open(URL(string: "App-Prefs:root=Cellular")!) — opens "Cellular" section in settings, user switches manually. Not best UX, but only way without entitlement.
Carrier entitlement + CTSubscriptionManager: available only for operator apps with com.apple.developer.CoreTelephony.Subscriber entitlement. Apple provides only to licensed operators:
import CoreTelephony
let subscriptionManager = CTSubscriptionManager()
// Get list of profiles
let subscriptions = subscriptionManager.subscriptions
// subscriptions: [String: CTSubscriber] — key is subscriber ID
// For switching — only via system UI or operator
// No direct API for switch without entitlement
Getting List of Installed Profiles
Android:
fun getInstalledEsimProfiles(): List<SubscriptionInfo> {
val allSubs = subscriptionManager.availableSubscriptionInfoList ?: emptyList()
return allSubs.filter { it.isEmbedded }
.map { sub ->
// sub.displayName — operator name
// sub.simSlotIndex: -1 if inactive, >= 0 if active
// sub.iccId — unique profile identifier
sub
}
}
simSlotIndex == -1 for inactive profiles — important for status display in list.
iOS (without entitlement):
import CoreTelephony
func getActiveCarriers() -> [CTCarrier] {
let networkInfo = CTTelephonyNetworkInfo()
return networkInfo.serviceSubscriberCellularProviders?.values
.compactMap { $0 }
.filter { $0.carrierName != nil }
?? []
}
Only active profiles. List of all installed (including inactive) — unavailable without carrier entitlement.
Switching UX
Switch time: Android — 10–30 seconds (radio reboot on Qualcomm, faster on MTK). iOS via Settings — uncontrollable. Show progress indicator with warning about temporary connection loss.
After successful switch, verify new active profile via SubscriptionManager.getActiveSubscriptionInfo(newSubscriptionId) — ensure completion before UI update.
Timeline
Android: switching between profiles with BroadcastReceiver and error handling — 1–2 weeks. iOS: deeplink to Settings + show current profiles — 3–5 days. Carrier-grade solution for both platforms with full control via entitlement: 1–2 months.







