From 5948499896095f8209d72fbd0c907625327c8515 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 17 Dec 2023 10:59:24 -0500 Subject: [PATCH 1/4] [10/n] OpenEuiccService: Implemennt onSwitchToSubscriptionWithPort --- .../openeuicc/core/EuiccChannelManager.kt | 14 +++++++++ .../openeuicc/service/OpenEuiccService.kt | 29 ++++++++++++++----- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt index 135cac6..0657c98 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt @@ -121,6 +121,20 @@ open class EuiccChannelManager(protected val context: Context) { } } + fun findEuiccChannelByPhysicalSlotBlocking(physicalSlotId: Int): EuiccChannel? = runBlocking { + if (!checkPrivileges()) return@runBlocking null + withContext(Dispatchers.IO) { + for (card in tm.uiccCardsInfoCompat) { + if (card.physicalSlotIndex != physicalSlotId) continue + for (port in card.ports) { + tryOpenEuiccChannel(port)?.let { return@withContext it } + } + } + + null + } + } + fun findEuiccChannelByPortBlocking(physicalSlotId: Int, portId: Int): EuiccChannel? = runBlocking { if (!checkPrivileges()) return@runBlocking null withContext(Dispatchers.IO) { diff --git a/app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt b/app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt index 4824fbd..5e1b386 100644 --- a/app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt +++ b/app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt @@ -12,9 +12,13 @@ class OpenEuiccService : EuiccService() { private val openEuiccApplication get() = application as OpenEuiccApplication - private fun findChannel(slotId: Int): EuiccChannel? = + private fun findChannel(physicalSlotId: Int): EuiccChannel? = openEuiccApplication.euiccChannelManager - .findEuiccChannelBySlotBlocking(slotId) + .findEuiccChannelByPhysicalSlotBlocking(physicalSlotId) + + private fun findChannel(slotId: Int, portId: Int): EuiccChannel? = + openEuiccApplication.euiccChannelManager + .findEuiccChannelByPortBlocking(slotId, portId) override fun onGetEid(slotId: Int): String? = findChannel(slotId)?.lpa?.eID @@ -99,6 +103,7 @@ class OpenEuiccService : EuiccService() { if (profile.state == LocalProfileInfo.State.Enabled) { // Must disable the profile first + // TODO: Need to check "other port" as well for MEP return RESULT_FIRST_USER } @@ -112,19 +117,29 @@ class OpenEuiccService : EuiccService() { } } - // TODO: on some devices we need to update the mapping (and potentially disable a pSIM) - // for eSIM to be usable, in which case we will have to respect forceDeactivateSim. - // This is the same for our custom LUI. Both have to take this into consideration. @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 { try { - val channel = findChannel(slotId) ?: return RESULT_FIRST_USER + val channel = if (portIndex == -1) { + findChannel(slotId) + } else { + findChannel(slotId, portIndex) + } ?: return RESULT_MUST_DEACTIVATE_SIM // TODO: If forceDeactivateSim = true, apply a default mapping - if (!channel.profileExists(iccid)) { + if (iccid != null && !channel.profileExists(iccid)) { return RESULT_FIRST_USER } From d95341b764111b1aef6cf9382ddace3f6ed68205 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 17 Dec 2023 11:18:45 -0500 Subject: [PATCH 2/4] [11/n] lpac-jni: Fix validity check for LPA none of the properties should be "by lazy" because we depend on whether they error to detect if the LPA is still valid. --- .../typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt index c62e3af..3fc55e7 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt @@ -20,11 +20,10 @@ class LocalProfileAssistantImpl( } override val profiles: List - get() = LpacJni.es10cGetProfilesInfo(contextHandle)!!.asList() // TODO: Maybe we need better error handling + get() = LpacJni.es10cGetProfilesInfo(contextHandle)!!.asList() - override val eID: String by lazy { - LpacJni.es10cGetEid(contextHandle)!! - } + override val eID: String + get() = LpacJni.es10cGetEid(contextHandle)!! override val euiccInfo2: EuiccInfo2? get() = LpacJni.es10cexGetEuiccInfo2(contextHandle) From 470fe5c5453bb3b12ce623059d4f1dd270ba3a6b Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 17 Dec 2023 13:01:14 -0500 Subject: [PATCH 3/4] OpenEuiccService: more logging --- .../im/angry/openeuicc/service/OpenEuiccService.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt b/app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt index 5e1b386..30ff062 100644 --- a/app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt +++ b/app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt @@ -3,12 +3,17 @@ package im.angry.openeuicc.service import android.service.euicc.* import android.telephony.euicc.DownloadableSubscription import android.telephony.euicc.EuiccInfo +import android.util.Log import net.typeblog.lpac_jni.LocalProfileInfo import im.angry.openeuicc.OpenEuiccApplication import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.util.* class OpenEuiccService : EuiccService() { + companion object { + const val TAG = "OpenEuiccService" + } + private val openEuiccApplication get() = application as OpenEuiccApplication @@ -60,6 +65,7 @@ class OpenEuiccService : EuiccService() { } override fun onGetEuiccProfileInfoList(slotId: Int): GetEuiccProfileInfoListResult? { + Log.i(TAG, "onGetEuiccProfileInfoList slotId=$slotId") val channel = findChannel(slotId) ?: return null val profiles = channel.lpa.profiles.operational.map { EuiccProfileInfo.Builder(it.iccid).apply { @@ -90,6 +96,7 @@ class OpenEuiccService : EuiccService() { } override fun onDeleteSubscription(slotId: Int, iccid: String): Int { + Log.i(TAG, "onDeleteSubscription slotId=$slotId iccid=$iccid") try { val channel = findChannel(slotId) ?: return RESULT_FIRST_USER @@ -132,6 +139,7 @@ class OpenEuiccService : EuiccService() { iccid: String?, forceDeactivateSim: Boolean ): Int { + Log.i(TAG,"onSwitchToSubscriptionWithPort slotId=$slotId portIndex=$portIndex iccid=$iccid forceDeactivateSim=$forceDeactivateSim") try { val channel = if (portIndex == -1) { findChannel(slotId) @@ -140,6 +148,7 @@ class OpenEuiccService : EuiccService() { } ?: return RESULT_MUST_DEACTIVATE_SIM // TODO: If forceDeactivateSim = true, apply a default mapping if (iccid != null && !channel.profileExists(iccid)) { + Log.i(TAG, "onSwitchToSubscriptionWithPort iccid=$iccid not found") return RESULT_FIRST_USER } @@ -169,6 +178,7 @@ class OpenEuiccService : EuiccService() { } override fun onUpdateSubscriptionNickname(slotId: Int, iccid: String, nickname: String?): Int { + Log.i(TAG, "onUpdateSubscriptionNickname slotId=$slotId iccid=$iccid nickname=$nickname") val channel = findChannel(slotId) ?: return RESULT_FIRST_USER if (!channel.profileExists(iccid)) { return RESULT_FIRST_USER From 100d2e2866ebf2fbc7a2360fad4785a5550703ec Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 17 Dec 2023 14:37:01 -0500 Subject: [PATCH 4/4] [12/n] OpenEuiccService: Improve compatibility with AOSP Settings UI --- .../openeuicc/service/OpenEuiccService.kt | 90 +++++++++++++++---- 1 file changed, 72 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt b/app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt index 30ff062..d98fca8 100644 --- a/app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt +++ b/app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt @@ -1,6 +1,7 @@ package im.angry.openeuicc.service import android.service.euicc.* +import android.telephony.UiccSlotMapping import android.telephony.euicc.DownloadableSubscription import android.telephony.euicc.EuiccInfo import android.util.Log @@ -8,6 +9,7 @@ import net.typeblog.lpac_jni.LocalProfileInfo import im.angry.openeuicc.OpenEuiccApplication import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.util.* +import java.lang.IllegalStateException class OpenEuiccService : EuiccService() { companion object { @@ -34,6 +36,47 @@ class OpenEuiccService : EuiccService() { private fun EuiccChannel.profileExists(iccid: String?) = lpa.profiles.any { it.iccid == iccid } + private fun ensurePortIsMapped(slotId: Int, portId: Int) { + val mappings = openEuiccApplication.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 { + openEuiccApplication.telephonyManager.simSlotMapping = mappings + return + } catch (_: Exception) { + + } + + // Sometimes hardware supports one ordering but not the reverse + openEuiccApplication.telephonyManager.simSlotMapping = mappings.reversed() + } + + private fun 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 @@ -141,35 +184,46 @@ class OpenEuiccService : EuiccService() { ): Int { Log.i(TAG,"onSwitchToSubscriptionWithPort slotId=$slotId portIndex=$portIndex iccid=$iccid forceDeactivateSim=$forceDeactivateSim") 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) { - findChannel(slotId) + retryWithTimeout(5000) { findChannel(slotId) } } else { - findChannel(slotId, portIndex) - } ?: return RESULT_MUST_DEACTIVATE_SIM // TODO: If forceDeactivateSim = true, apply a default mapping + 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 } - if (iccid == null) { - // Disable active profile - val activeProfile = channel.lpa.profiles.find { - it.state == LocalProfileInfo.State.Enabled - } ?: return RESULT_OK + // Disable any active profile first if present + channel.lpa.profiles.find { + it.state == LocalProfileInfo.State.Enabled + }?.let { if (!channel.lpa.disableProfile(it.iccid)) return RESULT_FIRST_USER } - return if (channel.lpa.disableProfile(activeProfile.iccid)) { - RESULT_OK - } else { - RESULT_FIRST_USER - } - } else { - return if (channel.lpa.enableProfile(iccid)) { - RESULT_OK - } else { - 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 {