OpenEUICC/app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt
Peter Cai 6e590cfd48
All checks were successful
/ build-debug (push) Successful in 4m12s
OpenEuiccService: stop confusing AOSP with multiple eUICCs
Unfortunately, AOSP is not really good at handling more than one eUICC
chips per device, even though the EuiccService interface should
technically allow for such a situation.

Let's do the next best thing -- only ever report one eUICC chip to AOSP.
If the device has an internal one, then only report that one; otherwise,
select the first available eUICC chip to report to the system.

We might make this more configurable in the future, but for now I think
this should work for most of the situations.

Note that this does NOT affect how the rest of OpenEUICC behaves. This
does mean however OpenEUICC will keep hold of some APDU channels that it
will never access via OpenEuiccService. A mitigation is to make
EuiccChannelManager close unused channels automatically after some
timeout.
2024-04-03 20:53:48 -04:00

290 lines
11 KiB
Kotlin

package im.angry.openeuicc.service
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.util.*
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 fun findChannel(physicalSlotId: Int): EuiccChannel? =
euiccChannelManager.findEuiccChannelByPhysicalSlotBlocking(physicalSlotId)
private fun findChannel(slotId: Int, portId: Int): EuiccChannel? =
euiccChannelManager.findEuiccChannelByPortBlocking(slotId, portId)
private fun findAllChannels(physicalSlotId: Int): List<EuiccChannel>? =
euiccChannelManager.findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId)
override fun onGetEid(slotId: Int): String? =
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 {
Log.i(TAG, "onGetEuiccProfileInfoList slotId=$slotId")
if (shouldIgnoreSlot(slotId)) {
Log.i(TAG, "ignoring slot $slotId")
return GetEuiccProfileInfoListResult(RESULT_FIRST_USER, arrayOf(), true)
}
val channel = findChannel(slotId)!!
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 {
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 {
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
channel.lpa.profiles.find {
it.state == LocalProfileInfo.State.Enabled
}?.let { if (!channel.lpa.disableProfile(it.iccid)) 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 {
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
}
}