Peter Cai
6e590cfd48
All checks were successful
/ build-debug (push) Successful in 4m12s
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.
290 lines
11 KiB
Kotlin
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
|
|
}
|
|
}
|