| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336 |
- 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.EuiccChannel
- import im.angry.openeuicc.core.EuiccChannelManager
- import im.angry.openeuicc.util.*
- import kotlinx.coroutines.runBlocking
- import java.lang.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 euiccChannelManager: EuiccChannelManager
- ) {
- fun findChannel(physicalSlotId: Int): EuiccChannel? =
- euiccChannelManager.findEuiccChannelByPhysicalSlotBlocking(physicalSlotId)
- fun findChannel(slotId: Int, portId: Int): EuiccChannel? =
- euiccChannelManager.findEuiccChannelByPortBlocking(slotId, portId)
- fun findAllChannels(physicalSlotId: Int): List<EuiccChannel>? =
- euiccChannelManager.findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId)
- }
- /**
- * 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.
- */
- private inline fun <T> withEuiccChannelManager(fn: 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 ret =
- EuiccChannelManagerContext((binder as EuiccChannelManagerService.LocalBinder).service.euiccChannelManager).fn()
- unbind()
- return ret
- }
- override fun onGetEid(slotId: Int): String? = withEuiccChannelManager {
- findChannel(slotId)?.lpa?.eID
- }
- // When two eSIM cards are present on one device, the Android settings UI
- // gets confused and sets the incorrect slotId for profiles from one of
- // the cards. This function helps Detect this case and abort early.
- private fun EuiccChannel.profileExists(iccid: String?) =
- lpa.profiles.any { it.iccid == iccid }
- 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) {
- }
- // Sometimes hardware supports one ordering but not the reverse
- telephonyManager.simSlotMapping = mappings.reversed()
- }
- private fun <T> retryWithTimeout(timeoutMillis: Int, backoff: Int = 1000, f: () -> T?): T? {
- val startTimeMillis = System.currentTimeMillis()
- do {
- try {
- f()?.let { return@retryWithTimeout it }
- } catch (_: Exception) {
- // Ignore
- } finally {
- Thread.sleep(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 (shouldIgnoreSlot(slotId)) {
- Log.i(TAG, "ignoring slot $slotId")
- return GetEuiccProfileInfoListResult(RESULT_FIRST_USER, arrayOf(), true)
- }
- // TODO: Temporarily enable the slot to access its profiles if it is currently unmapped
- val channel = findChannel(slotId) ?: return GetEuiccProfileInfoListResult(
- RESULT_FIRST_USER,
- arrayOf(),
- true
- )
- val profiles = channel.lpa.profiles.operational.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()
- }
- return GetEuiccProfileInfoListResult(RESULT_OK, profiles.toTypedArray(), channel.removable)
- }
- 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 RESULT_FIRST_USER
- try {
- val channels = findAllChannels(slotId) ?: return RESULT_FIRST_USER
- if (!channels[0].profileExists(iccid)) {
- return RESULT_FIRST_USER
- }
- // If the profile is enabled by ANY channel (port), we cannot delete it
- channels.forEach { channel ->
- val profile = channel.lpa.profiles.find {
- it.iccid == iccid
- } ?: return RESULT_FIRST_USER
- if (profile.state == LocalProfileInfo.State.Enabled) {
- // Must disable the profile first
- return RESULT_FIRST_USER
- }
- }
- return if (channels[0].lpa.deleteProfile(iccid)) {
- RESULT_OK
- } else {
- RESULT_FIRST_USER
- }
- } catch (e: Exception) {
- return 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 RESULT_FIRST_USER
- try {
- // 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 channel = if (portIndex == -1) {
- retryWithTimeout(5000) { findChannel(slotId) }
- } else {
- retryWithTimeout(5000) { findChannel(slotId, portIndex) }
- } ?: run {
- if (!forceDeactivateSim) {
- // The user must select which SIM to deactivate
- return@onSwitchToSubscriptionWithPort RESULT_MUST_DEACTIVATE_SIM
- } else {
- try {
- // If we are allowed to deactivate any SIM we like, try mapping the indicated port first
- ensurePortIsMapped(slotId, portIndex)
- retryWithTimeout(5000) { findChannel(slotId, portIndex) }
- } catch (e: Exception) {
- // We cannot map the port (or it is already mapped)
- // but we can also use any port available on the card
- retryWithTimeout(5000) { findChannel(slotId) }
- } ?: return@onSwitchToSubscriptionWithPort RESULT_FIRST_USER
- }
- }
- if (iccid != null && !channel.profileExists(iccid)) {
- Log.i(TAG, "onSwitchToSubscriptionWithPort iccid=$iccid not found")
- return RESULT_FIRST_USER
- }
- // Disable any active profile first if present
- if (!channel.lpa.disableActiveProfile(false)) {
- return RESULT_FIRST_USER
- }
- if (iccid != null) {
- if (!channel.lpa.enableProfile(iccid)) {
- return RESULT_FIRST_USER
- }
- }
- return RESULT_OK
- } catch (e: Exception) {
- return 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 RESULT_FIRST_USER
- val channel = findChannel(slotId) ?: return RESULT_FIRST_USER
- if (!channel.profileExists(iccid)) {
- return RESULT_FIRST_USER
- }
- val success = channel.lpa
- .setNickname(iccid, nickname!!)
- appContainer.subscriptionManager.tryRefreshCachedEuiccInfo(channel.cardId)
- return 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
- }
- }
|