package im.angry.openeuicc.service import android.content.Context import android.content.Intent import android.os.Build import android.service.euicc.* import android.telephony.UiccSlotMapping import android.telephony.euicc.DownloadableSubscription import android.telephony.euicc.EuiccInfo import android.util.Log import net.typeblog.lpac_jni.LocalProfileInfo import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone import im.angry.openeuicc.util.* import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import kotlin.IllegalStateException class OpenEuiccService : EuiccService(), OpenEuiccContextMarker { companion object { const val TAG = "OpenEuiccService" } private val hasInternalEuicc by lazy { telephonyManager.uiccCardsInfoCompat.any { it.isEuicc && !it.isRemovable } } // TODO: Should this be configurable? private fun shouldIgnoreSlot(physicalSlotId: Int) = if (hasInternalEuicc) { // For devices with an internal eUICC slot, ignore any removable UICC telephonyManager.uiccCardsInfoCompat.find { it.physicalSlotIndex == physicalSlotId }!!.isRemovable } else { // Otherwise, we can report at least one removable eUICC to the system without confusing // it too much. telephonyManager.uiccCardsInfoCompat.firstOrNull { it.isEuicc }?.physicalSlotIndex == physicalSlotId } private data class EuiccChannelManagerContext( val euiccChannelManagerService: EuiccChannelManagerService ) { val euiccChannelManager get() = euiccChannelManagerService.euiccChannelManager } /** * Bind to EuiccChannelManagerService, run the callback with a EuiccChannelManager instance, * and then unbind after the callback is finished. All methods in this class that require access * to a EuiccChannelManager should be wrapped inside this call. * * This ensures that we only spawn and connect to APDU channels when we absolutely need to, * instead of keeping them open unnecessarily in the background at all times. * * This function cannot be inline because non-local returns may bypass the unbind */ private fun withEuiccChannelManager(fn: suspend EuiccChannelManagerContext.() -> T): T { val (binder, unbind) = runBlocking { bindServiceSuspended( Intent( this@OpenEuiccService, EuiccChannelManagerService::class.java ), Context.BIND_AUTO_CREATE ) } if (binder == null) { throw RuntimeException("Unable to bind to EuiccChannelManagerService; aborting") } val localBinder = binder as EuiccChannelManagerService.LocalBinder val ret = runBlocking { EuiccChannelManagerContext(localBinder.service).fn() } unbind() return ret } override fun onGetEid(slotId: Int): String? = withEuiccChannelManager { val portId = euiccChannelManager.findFirstAvailablePort(slotId) if (portId < 0) return@withEuiccChannelManager null euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> channel.lpa.eID } } private fun ensurePortIsMapped(slotId: Int, portId: Int) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { return } val mappings = telephonyManager.simSlotMapping.toMutableList() mappings.firstOrNull { it.physicalSlotIndex == slotId && it.portIndex == portId }?.let { throw IllegalStateException("Slot $slotId port $portId has already been mapped") } val idx = mappings.indexOfFirst { it.physicalSlotIndex != slotId || it.portIndex != portId } if (idx >= 0) { mappings[idx] = UiccSlotMapping(portId, slotId, mappings[idx].logicalSlotIndex) } mappings.firstOrNull { it.physicalSlotIndex == slotId && it.portIndex == portId } ?: run { throw IllegalStateException("Cannot map slot $slotId port $portId") } try { telephonyManager.simSlotMapping = mappings return } catch (_: Exception) { // ignore } // Sometimes hardware supports one ordering but not the reverse telephonyManager.simSlotMapping = mappings.reversed() } private suspend fun retryWithTimeout( timeoutMillis: Int, backoff: Int = 1000, f: suspend () -> T? ): T? { val startTimeMillis = System.currentTimeMillis() do { try { f()?.let { return@retryWithTimeout it } } catch (_: Exception) { // Ignore } finally { delay(backoff.toLong()) } } while (System.currentTimeMillis() - startTimeMillis < timeoutMillis) return null } override fun onGetOtaStatus(slotId: Int): Int { // Not implemented return 5 // EUICC_OTA_STATUS_UNAVAILABLE } override fun onStartOtaIfNecessary( slotId: Int, statusChangedCallback: OtaStatusChangedCallback? ) { // Not implemented } override fun onGetDownloadableSubscriptionMetadata( slotId: Int, subscription: DownloadableSubscription?, forceDeactivateSim: Boolean ): GetDownloadableSubscriptionMetadataResult { // Stub: return as-is and do not fetch anything // This is incompatible with carrier eSIM apps; should we make it compatible? return GetDownloadableSubscriptionMetadataResult(RESULT_OK, subscription) } override fun onGetDefaultDownloadableSubscriptionList( slotId: Int, forceDeactivateSim: Boolean ): GetDefaultDownloadableSubscriptionListResult { // Stub: we do not implement this (as this would require phoning in a central GSMA server) return GetDefaultDownloadableSubscriptionListResult(RESULT_OK, arrayOf()) } override fun onGetEuiccProfileInfoList(slotId: Int): GetEuiccProfileInfoListResult = withEuiccChannelManager { Log.i(TAG, "onGetEuiccProfileInfoList slotId=$slotId") if (slotId == -1 || shouldIgnoreSlot(slotId)) { Log.i(TAG, "ignoring slot $slotId") return@withEuiccChannelManager GetEuiccProfileInfoListResult( RESULT_FIRST_USER, arrayOf(), true ) } // TODO: Temporarily enable the slot to access its profiles if it is currently unmapped val port = euiccChannelManager.findFirstAvailablePort(slotId) if (port == -1) { return@withEuiccChannelManager GetEuiccProfileInfoListResult( RESULT_FIRST_USER, arrayOf(), true ) } return@withEuiccChannelManager try { euiccChannelManager.withEuiccChannel(slotId, port) { channel -> val filteredProfiles = if (preferenceRepository.unfilteredProfileListFlow.first()) channel.lpa.profiles else channel.lpa.profiles.operational val profiles = filteredProfiles.map { EuiccProfileInfo.Builder(it.iccid).apply { setProfileName(it.name) setNickname(it.displayName) setServiceProviderName(it.providerName) setState( when (it.state) { LocalProfileInfo.State.Enabled -> EuiccProfileInfo.PROFILE_STATE_ENABLED LocalProfileInfo.State.Disabled -> EuiccProfileInfo.PROFILE_STATE_DISABLED } ) setProfileClass( when (it.profileClass) { LocalProfileInfo.Clazz.Testing -> EuiccProfileInfo.PROFILE_CLASS_TESTING LocalProfileInfo.Clazz.Provisioning -> EuiccProfileInfo.PROFILE_CLASS_PROVISIONING LocalProfileInfo.Clazz.Operational -> EuiccProfileInfo.PROFILE_CLASS_OPERATIONAL } ) }.build() } GetEuiccProfileInfoListResult( RESULT_OK, profiles.toTypedArray(), channel.port.card.isRemovable ) } } catch (e: EuiccChannelManager.EuiccChannelNotFoundException) { GetEuiccProfileInfoListResult( RESULT_FIRST_USER, arrayOf(), true ) } } override fun onGetEuiccInfo(slotId: Int): EuiccInfo { return EuiccInfo("Unknown") // TODO: Can we actually implement this? } override fun onDeleteSubscription(slotId: Int, iccid: String): Int = withEuiccChannelManager { Log.i(TAG, "onDeleteSubscription slotId=$slotId iccid=$iccid") if (shouldIgnoreSlot(slotId)) return@withEuiccChannelManager RESULT_FIRST_USER val ports = euiccChannelManager.findAvailablePorts(slotId) if (ports.isEmpty()) return@withEuiccChannelManager RESULT_FIRST_USER // Check that the profile has been disabled on all slots val enabledAnywhere = ports.any { port -> euiccChannelManager.withEuiccChannel(slotId, port) { channel -> channel.lpa.profiles.enabled?.iccid == iccid } } if (enabledAnywhere) return@withEuiccChannelManager RESULT_FIRST_USER euiccChannelManagerService.waitForForegroundTask() val success = euiccChannelManagerService.launchProfileDeleteTask(slotId, ports[0], iccid) .waitDone() == null return@withEuiccChannelManager if (success) { RESULT_OK } else { RESULT_FIRST_USER } } @Deprecated("Deprecated in Java") override fun onSwitchToSubscription( slotId: Int, iccid: String?, forceDeactivateSim: Boolean ): Int = // -1 = any port onSwitchToSubscriptionWithPort(slotId, -1, iccid, forceDeactivateSim) override fun onSwitchToSubscriptionWithPort( slotId: Int, portIndex: Int, iccid: String?, forceDeactivateSim: Boolean ): Int = withEuiccChannelManager { Log.i(TAG,"onSwitchToSubscriptionWithPort slotId=$slotId portIndex=$portIndex iccid=$iccid forceDeactivateSim=$forceDeactivateSim") if (shouldIgnoreSlot(slotId)) return@withEuiccChannelManager RESULT_FIRST_USER try { // First, try to find a pair of slotId and portId we can use for the switching operation // retryWithTimeout is needed here because this function may be called just after // AOSP has switched slot mappings, in which case the slots may not be ready yet. val (foundSlotId, foundPortId) = retryWithTimeout(5000) { if (portIndex == -1) { // If port is not indicated, we can use any port val port = euiccChannelManager.findFirstAvailablePort(slotId).let { if (it < 0) { throw IllegalStateException("No mapped port available; may need to try again") } it } Pair(slotId, port) } else { // Else, check until the indicated port is available euiccChannelManager.withEuiccChannel(slotId, portIndex) { channel -> if (!channel.valid) { throw IllegalStateException("Indicated slot / port combination is unavailable; may need to try again") } } Pair(slotId, portIndex) } } ?: run { // Failure case: mapped slots / ports aren't usable per constraints // If we can't find a usable slot / port already mapped, and we aren't allowed to // deactivate a SIM, we can only abort if (!forceDeactivateSim) { return@withEuiccChannelManager RESULT_MUST_DEACTIVATE_SIM } // If port ID is not indicated, we just try to map port 0 // This is because in order to get here, we have to have failed findFirstAvailablePort(), // which means no eUICC port is mapped or connected properly whatsoever. val foundPortId = if (portIndex == -1) { 0 } else { portIndex } // Now we can try to map an unused port try { ensurePortIsMapped(slotId, foundPortId) } catch (_: Exception) { return@withEuiccChannelManager RESULT_FIRST_USER } // Wait for availability again retryWithTimeout(5000) { euiccChannelManager.withEuiccChannel(slotId, foundPortId) { channel -> if (!channel.valid) { throw IllegalStateException("Indicated slot / port combination is unavailable; may need to try again") } } } ?: return@withEuiccChannelManager RESULT_FIRST_USER Pair(slotId, foundPortId) } Log.i(TAG, "Found slotId=$foundSlotId, portId=$foundPortId for switching") // Now, figure out what they want us to do: disabling a profile, or enabling a new one? val (foundIccid, enable) = if (iccid == null) { // iccid == null means disabling val foundIccid = euiccChannelManager.withEuiccChannel(foundSlotId, foundPortId) { channel -> channel.lpa.profiles.enabled?.iccid } ?: return@withEuiccChannelManager RESULT_FIRST_USER Pair(foundIccid, false) } else { Pair(iccid, true) } val res = euiccChannelManagerService.launchProfileSwitchTask( foundSlotId, foundPortId, foundIccid, enable, 30 * 1000 ).waitDone() if (res != null) return@withEuiccChannelManager RESULT_FIRST_USER return@withEuiccChannelManager RESULT_OK } catch (e: Exception) { return@withEuiccChannelManager RESULT_FIRST_USER } finally { euiccChannelManager.invalidate() } } override fun onUpdateSubscriptionNickname(slotId: Int, iccid: String, nickname: String?): Int = withEuiccChannelManager { Log.i( TAG, "onUpdateSubscriptionNickname slotId=$slotId iccid=$iccid nickname=$nickname" ) if (shouldIgnoreSlot(slotId)) return@withEuiccChannelManager RESULT_FIRST_USER val port = euiccChannelManager.findFirstAvailablePort(slotId) if (port < 0) { return@withEuiccChannelManager RESULT_FIRST_USER } euiccChannelManagerService.waitForForegroundTask() val success = (euiccChannelManagerService.launchProfileRenameTask(slotId, port, iccid, nickname!!) .waitDone()) == null euiccChannelManager.withEuiccChannel(slotId, port) { channel -> appContainer.subscriptionManager.tryRefreshCachedEuiccInfo(channel.cardId) } return@withEuiccChannelManager if (success) { RESULT_OK } else { RESULT_FIRST_USER } } @Deprecated("Deprecated in Java") override fun onEraseSubscriptions(slotId: Int): Int { // No-op return RESULT_FIRST_USER } override fun onRetainSubscriptionsForFactoryReset(slotId: Int): Int { // No-op -- we do not care return RESULT_FIRST_USER } }