From 6bb05d910b877b882c2d2ca96782b10481aeecf5 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 18 May 2025 11:46:56 -0400 Subject: [PATCH 01/32] [1/n] Add seId parameter to withEuiccChannel() Defaults to 0 so that it doesn't break everything else. --- .../java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt | 2 ++ .../main/java/im/angry/openeuicc/core/EuiccChannelManager.kt | 2 ++ .../src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt index 6b336cd..1d7a7fb 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt @@ -174,6 +174,7 @@ open class DefaultEuiccChannelManager( override suspend fun withEuiccChannel( physicalSlotId: Int, portId: Int, + seId: Int, fn: suspend (EuiccChannel) -> R ): R { val channel = findEuiccChannelByPort(physicalSlotId, portId) @@ -190,6 +191,7 @@ open class DefaultEuiccChannelManager( override suspend fun withEuiccChannel( logicalSlotId: Int, + seId: Int, fn: suspend (EuiccChannel) -> R ): R { val channel = findEuiccChannelByLogicalSlot(logicalSlotId) 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 17f3130..ffa3606 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 @@ -81,6 +81,7 @@ interface EuiccChannelManager { suspend fun withEuiccChannel( physicalSlotId: Int, portId: Int, + seId: Int = 0, fn: suspend (EuiccChannel) -> R ): R @@ -89,6 +90,7 @@ interface EuiccChannelManager { */ suspend fun withEuiccChannel( logicalSlotId: Int, + seId: Int = 0, fn: suspend (EuiccChannel) -> R ): R diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt index 1d5f37f..f0bff39 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt @@ -92,7 +92,7 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { lifecycleScope.launch { (infoList.adapter!! as EuiccInfoAdapter).euiccInfoItems = - euiccChannelManager.withEuiccChannel(logicalSlotId, ::buildEuiccInfoItems) + euiccChannelManager.withEuiccChannel(logicalSlotId, fn = ::buildEuiccInfoItems) swipeRefresh.isRefreshing = false } From 5dd9e40c25aa890db7949346ef3cfdc17aa151f5 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Mon, 2 Jun 2025 22:24:11 -0400 Subject: [PATCH 02/32] Add seId support to most of EuiccChannelManager components --- .../core/DefaultEuiccChannelFactory.kt | 8 ++- .../core/DefaultEuiccChannelManager.kt | 71 +++++++++---------- .../im/angry/openeuicc/core/EuiccChannel.kt | 10 +++ .../openeuicc/core/EuiccChannelFactory.kt | 5 +- .../angry/openeuicc/core/EuiccChannelImpl.kt | 1 + .../openeuicc/core/EuiccChannelWrapper.kt | 2 + .../core/PrivilegedEuiccChannelFactory.kt | 8 ++- 7 files changed, 62 insertions(+), 43 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt index 0de99b5..5755c02 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt @@ -20,7 +20,8 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha override suspend fun tryOpenEuiccChannel( port: UiccPortInfoCompat, - isdrAid: ByteArray + isdrAid: ByteArray, + seId: Int, ): EuiccChannel? { if (port.portIndex != 0) { Log.w( @@ -46,6 +47,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha context.preferenceRepository.verboseLoggingFlow ), isdrAid, + seId, context.preferenceRepository.verboseLoggingFlow, context.preferenceRepository.ignoreTLSCertificateFlow, ).also { @@ -65,7 +67,8 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha override fun tryOpenUsbEuiccChannel( ccidCtx: UsbCcidContext, - isdrAid: ByteArray + isdrAid: ByteArray, + seId: Int ): EuiccChannel? { try { return EuiccChannelImpl( @@ -76,6 +79,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha ccidCtx ), isdrAid, + seId, context.preferenceRepository.verboseLoggingFlow, context.preferenceRepository.ignoreTLSCertificateFlow, ) diff --git a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt index 1d7a7fb..5f18295 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt @@ -32,7 +32,7 @@ open class DefaultEuiccChannelManager( private val channelCache = mutableListOf() - private var usbChannel: EuiccChannel? = null + private var usbChannels = mutableListOf() private val lock = Mutex() @@ -51,15 +51,17 @@ open class DefaultEuiccChannelManager( protected open val uiccCards: Collection get() = (0.. EuiccChannel?): EuiccChannel? { + private suspend inline fun tryOpenChannelWithKnownAids(openFn: (ByteArray, Int) -> EuiccChannel?): List { val isdrAidList = parseIsdrAidList(appContainer.preferenceRepository.isdrAidListFlow.first()) + var seId = 0 - return isdrAidList.firstNotNullOfOrNull { - Log.i(TAG, "Opening channel, trying ISDR AID ${it.encodeHex()}") + return isdrAidList.mapNotNull { + Log.i(TAG, "Opening channel, trying ISDR AID ${it.encodeHex()}, this will be seId ${seId}") - openFn(it)?.let { channel -> + openFn(it, seId)?.let { channel -> if (channel.valid) { + seId += 1 channel } else { channel.close() @@ -69,19 +71,15 @@ open class DefaultEuiccChannelManager( } } - private suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel? { + private suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat, seId: Int = 0): EuiccChannel? { lock.withLock { if (port.card.physicalSlotIndex == EuiccChannelManager.USB_CHANNEL_ID) { - return if (usbChannel != null && usbChannel!!.valid) { - usbChannel - } else { - usbChannel = null - null - } + // We only compare seId because we assume we can only open 1 card from USB + return usbChannels.find { it.seId == seId } } val existing = - channelCache.find { it.slotId == port.card.physicalSlotIndex && it.portId == port.portIndex } + channelCache.find { it.slotId == port.card.physicalSlotIndex && it.portId == port.portIndex && it.seId == seId } if (existing != null) { if (existing.valid && port.logicalSlotIndex == existing.logicalSlotId) { return existing @@ -96,12 +94,12 @@ open class DefaultEuiccChannelManager( return null } - val channel = - tryOpenChannelFirstValidAid { euiccChannelFactory.tryOpenEuiccChannel(port, it) } + val channels = + tryOpenChannelWithKnownAids { isdrAid, seId -> euiccChannelFactory.tryOpenEuiccChannel(port, isdrAid, seId) } - if (channel != null) { - channelCache.add(channel) - return channel + if (channels.isNotEmpty()) { + channelCache.addAll(channels) + return channels.find { it.seId == seId } } else { Log.i( TAG, @@ -112,10 +110,10 @@ open class DefaultEuiccChannelManager( } } - protected suspend fun findEuiccChannelByLogicalSlot(logicalSlotId: Int): EuiccChannel? = + protected suspend fun findEuiccChannelByLogicalSlot(logicalSlotId: Int, seId: Int = 0): EuiccChannel? = withContext(Dispatchers.IO) { if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { - return@withContext usbChannel + return@withContext usbChannels.find { it.seId == seId } } for (card in uiccCards) { @@ -131,7 +129,7 @@ open class DefaultEuiccChannelManager( private suspend fun findAllEuiccChannelsByPhysicalSlot(physicalSlotId: Int): List? { if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { - return usbChannel?.let { listOf(it) } + return usbChannels.ifEmpty { null } } for (card in uiccCards) { @@ -142,14 +140,14 @@ open class DefaultEuiccChannelManager( return null } - private suspend fun findEuiccChannelByPort(physicalSlotId: Int, portId: Int): EuiccChannel? = + private suspend fun findEuiccChannelByPort(physicalSlotId: Int, portId: Int, seId: Int = 0): EuiccChannel? = withContext(Dispatchers.IO) { if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { - return@withContext usbChannel + return@withContext usbChannels.find { it.seId == seId } } uiccCards.find { it.physicalSlotIndex == physicalSlotId }?.let { card -> - card.ports.find { it.portIndex == portId }?.let { tryOpenEuiccChannel(it) } + card.ports.find { it.portIndex == portId }?.let { tryOpenEuiccChannel(it, seId) } } } @@ -168,7 +166,7 @@ open class DefaultEuiccChannelManager( return@withContext listOf(0) } - findAllEuiccChannelsByPhysicalSlot(physicalSlotId)?.map { it.portId } ?: listOf() + findAllEuiccChannelsByPhysicalSlot(physicalSlotId)?.map { it.portId }?.toSet()?.toList() ?: listOf() } override suspend fun withEuiccChannel( @@ -177,7 +175,7 @@ open class DefaultEuiccChannelManager( seId: Int, fn: suspend (EuiccChannel) -> R ): R { - val channel = findEuiccChannelByPort(physicalSlotId, portId) + val channel = findEuiccChannelByPort(physicalSlotId, portId, seId) ?: throw EuiccChannelManager.EuiccChannelNotFoundException() val wrapper = EuiccChannelWrapper(channel) try { @@ -194,7 +192,7 @@ open class DefaultEuiccChannelManager( seId: Int, fn: suspend (EuiccChannel) -> R ): R { - val channel = findEuiccChannelByLogicalSlot(logicalSlotId) + val channel = findEuiccChannelByLogicalSlot(logicalSlotId, seId) ?: throw EuiccChannelManager.EuiccChannelNotFoundException() val wrapper = EuiccChannelWrapper(channel) try { @@ -208,8 +206,8 @@ open class DefaultEuiccChannelManager( override suspend fun waitForReconnect(physicalSlotId: Int, portId: Int, timeoutMillis: Long) { if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { - usbChannel?.close() - usbChannel = null + usbChannels.forEach { it.close() } + usbChannels.clear() } else { // If there is already a valid channel, we close it proactively // Sometimes the current channel can linger on for a bit even after it should have become invalid @@ -225,7 +223,7 @@ open class DefaultEuiccChannelManager( // tryOpenUsbEuiccChannel() will always try to reopen the channel, even if // a USB channel already exists tryOpenUsbEuiccChannel() - usbChannel!! + usbChannels.getOrNull(0)!! } else { // tryOpenEuiccChannel() will automatically dispose of invalid channels // and recreate when needed @@ -282,12 +280,13 @@ open class DefaultEuiccChannelManager( val ccidCtx = UsbCcidContext.createFromUsbDevice(context, device, iface) ?: return@forEach try { - val channel = tryOpenChannelFirstValidAid { - euiccChannelFactory.tryOpenUsbEuiccChannel(ccidCtx, it) + val channels = tryOpenChannelWithKnownAids { isdrAid, seId -> + euiccChannelFactory.tryOpenUsbEuiccChannel(ccidCtx, isdrAid, seId) } - if (channel != null && channel.lpa.valid) { + if (channels.isNotEmpty() && channels[0].valid) { ccidCtx.allowDisconnect = true - usbChannel = channel + usbChannels.clear() + usbChannels.addAll(channels) return@withContext Pair(device, true) } } catch (e: Exception) { @@ -311,8 +310,8 @@ open class DefaultEuiccChannelManager( channel.close() } - usbChannel?.close() - usbChannel = null + usbChannels.forEach { it.close() } + usbChannels.clear() channelCache.clear() euiccChannelFactory.cleanup() } diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt index b20932f..9854bb2 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt @@ -13,6 +13,16 @@ interface EuiccChannel { val logicalSlotId: Int val portId: Int + /** + * Some chips support multiple SEs on one chip. The seId here is intended + * to distinguish channels opened from these different SEs. + * + * Note that this ID is arbitrary and heavily depends on the order in which + * we attempt to open the ISD-R AIDs. As such, it shall not be treated with + * any significance other than as a transient ID. + */ + val seId: Int + val lpa: LocalProfileAssistant val valid: Boolean diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelFactory.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelFactory.kt index ba587a6..53ac7ef 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelFactory.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelFactory.kt @@ -6,11 +6,12 @@ import im.angry.openeuicc.util.* // This class is here instead of inside DI because it contains a bit more logic than just // "dumb" dependency injection. interface EuiccChannelFactory { - suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat, isdrAid: ByteArray): EuiccChannel? + suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat, isdrAid: ByteArray, seInt: Int): EuiccChannel? fun tryOpenUsbEuiccChannel( ccidCtx: UsbCcidContext, - isdrAid: ByteArray + isdrAid: ByteArray, + seInt: Int ): EuiccChannel? /** diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt index 2a33c20..bf98397 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt @@ -14,6 +14,7 @@ class EuiccChannelImpl( override val intrinsicChannelName: String?, override val apduInterface: ApduInterface, override val isdrAid: ByteArray, + override val seId: Int, verboseLoggingFlow: Flow, ignoreTLSCertificateFlow: Flow ) : EuiccChannel { diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt index 361a943..8496189 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt @@ -26,6 +26,8 @@ class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel { get() = channel.logicalSlotId override val portId: Int get() = channel.portId + override val seId: Int + get() = channel.seId private val lpaDelegate = lazy { LocalProfileAssistantWrapper(channel.lpa) } diff --git a/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt b/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt index 68eddef..cb2bbc3 100644 --- a/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt +++ b/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt @@ -16,13 +16,14 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto @Suppress("NAME_SHADOWING") override suspend fun tryOpenEuiccChannel( port: UiccPortInfoCompat, - isdrAid: ByteArray + isdrAid: ByteArray, + seId: Int, ): EuiccChannel? { val port = port as RealUiccPortInfoCompat if (port.card.isRemovable) { // Attempt unprivileged (OMAPI) before TelephonyManager // but still try TelephonyManager in case OMAPI is broken - super.tryOpenEuiccChannel(port, isdrAid)?.let { return it } + super.tryOpenEuiccChannel(port, isdrAid, seId)?.let { return it } } if (port.card.isEuicc || preferenceRepository.removableTelephonyManagerFlow.first()) { @@ -41,6 +42,7 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto context.preferenceRepository.verboseLoggingFlow ), isdrAid, + seId, context.preferenceRepository.verboseLoggingFlow, context.preferenceRepository.ignoreTLSCertificateFlow, ) @@ -53,6 +55,6 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto } } - return super.tryOpenEuiccChannel(port, isdrAid) + return super.tryOpenEuiccChannel(port, isdrAid, seId) } } \ No newline at end of file From aba844c09c3013195bb0f7d75dc8fee9d3afe185 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 8 Jun 2025 14:57:33 -0400 Subject: [PATCH 03/32] refactor: Use an opaque wrapper for eSE IDs to avoid calling the wrong function(s) --- .../core/DefaultEuiccChannelFactory.kt | 4 +- .../core/DefaultEuiccChannelManager.kt | 43 ++++++++++++++----- .../im/angry/openeuicc/core/EuiccChannel.kt | 35 ++++++++++++--- .../openeuicc/core/EuiccChannelFactory.kt | 4 +- .../angry/openeuicc/core/EuiccChannelImpl.kt | 2 +- .../openeuicc/core/EuiccChannelManager.kt | 6 +-- .../openeuicc/core/EuiccChannelWrapper.kt | 2 +- .../util/EuiccChannelFragmentUtils.kt | 14 +++++- .../core/PrivilegedEuiccChannelFactory.kt | 2 +- 9 files changed, 84 insertions(+), 28 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt index 5755c02..b975313 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt @@ -21,7 +21,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha override suspend fun tryOpenEuiccChannel( port: UiccPortInfoCompat, isdrAid: ByteArray, - seId: Int, + seId: EuiccChannel.SecureElementId, ): EuiccChannel? { if (port.portIndex != 0) { Log.w( @@ -68,7 +68,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha override fun tryOpenUsbEuiccChannel( ccidCtx: UsbCcidContext, isdrAid: ByteArray, - seId: Int + seId: EuiccChannel.SecureElementId ): EuiccChannel? { try { return EuiccChannelImpl( diff --git a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt index 5f18295..31febe2 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt @@ -51,15 +51,18 @@ open class DefaultEuiccChannelManager( protected open val uiccCards: Collection get() = (0.. EuiccChannel?): List { + private suspend inline fun tryOpenChannelWithKnownAids(openFn: (ByteArray, EuiccChannel.SecureElementId) -> EuiccChannel?): List { val isdrAidList = parseIsdrAidList(appContainer.preferenceRepository.isdrAidListFlow.first()) var seId = 0 return isdrAidList.mapNotNull { - Log.i(TAG, "Opening channel, trying ISDR AID ${it.encodeHex()}, this will be seId ${seId}") + Log.i( + TAG, + "Opening channel, trying ISDR AID ${it.encodeHex()}, this will be seId $seId" + ) - openFn(it, seId)?.let { channel -> + openFn(it, EuiccChannel.SecureElementId.createFromInt(seId))?.let { channel -> if (channel.valid) { seId += 1 channel @@ -71,7 +74,10 @@ open class DefaultEuiccChannelManager( } } - private suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat, seId: Int = 0): EuiccChannel? { + private suspend fun tryOpenEuiccChannel( + port: UiccPortInfoCompat, + seId: EuiccChannel.SecureElementId = EuiccChannel.SecureElementId.DEFAULT + ): EuiccChannel? { lock.withLock { if (port.card.physicalSlotIndex == EuiccChannelManager.USB_CHANNEL_ID) { // We only compare seId because we assume we can only open 1 card from USB @@ -95,7 +101,13 @@ open class DefaultEuiccChannelManager( } val channels = - tryOpenChannelWithKnownAids { isdrAid, seId -> euiccChannelFactory.tryOpenEuiccChannel(port, isdrAid, seId) } + tryOpenChannelWithKnownAids { isdrAid, seId -> + euiccChannelFactory.tryOpenEuiccChannel( + port, + isdrAid, + seId + ) + } if (channels.isNotEmpty()) { channelCache.addAll(channels) @@ -110,7 +122,10 @@ open class DefaultEuiccChannelManager( } } - protected suspend fun findEuiccChannelByLogicalSlot(logicalSlotId: Int, seId: Int = 0): EuiccChannel? = + protected suspend fun findEuiccChannelByLogicalSlot( + logicalSlotId: Int, + seId: EuiccChannel.SecureElementId = EuiccChannel.SecureElementId.DEFAULT + ): EuiccChannel? = withContext(Dispatchers.IO) { if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { return@withContext usbChannels.find { it.seId == seId } @@ -140,7 +155,11 @@ open class DefaultEuiccChannelManager( return null } - private suspend fun findEuiccChannelByPort(physicalSlotId: Int, portId: Int, seId: Int = 0): EuiccChannel? = + private suspend fun findEuiccChannelByPort( + physicalSlotId: Int, + portId: Int, + seId: EuiccChannel.SecureElementId = EuiccChannel.SecureElementId.DEFAULT + ): EuiccChannel? = withContext(Dispatchers.IO) { if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { return@withContext usbChannels.find { it.seId == seId } @@ -166,13 +185,14 @@ open class DefaultEuiccChannelManager( return@withContext listOf(0) } - findAllEuiccChannelsByPhysicalSlot(physicalSlotId)?.map { it.portId }?.toSet()?.toList() ?: listOf() + findAllEuiccChannelsByPhysicalSlot(physicalSlotId)?.map { it.portId }?.toSet()?.toList() + ?: listOf() } override suspend fun withEuiccChannel( physicalSlotId: Int, portId: Int, - seId: Int, + seId: EuiccChannel.SecureElementId, fn: suspend (EuiccChannel) -> R ): R { val channel = findEuiccChannelByPort(physicalSlotId, portId, seId) @@ -189,7 +209,7 @@ open class DefaultEuiccChannelManager( override suspend fun withEuiccChannel( logicalSlotId: Int, - seId: Int, + seId: EuiccChannel.SecureElementId, fn: suspend (EuiccChannel) -> R ): R { val channel = findEuiccChannelByLogicalSlot(logicalSlotId, seId) @@ -277,7 +297,8 @@ open class DefaultEuiccChannelManager( "Found CCID interface on ${device.deviceId}:${device.vendorId}, and has permission; trying to open channel" ) - val ccidCtx = UsbCcidContext.createFromUsbDevice(context, device, iface) ?: return@forEach + val ccidCtx = + UsbCcidContext.createFromUsbDevice(context, device, iface) ?: return@forEach try { val channels = tryOpenChannelWithKnownAids { isdrAid, seId -> diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt index 9854bb2..f0cf329 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt @@ -13,15 +13,40 @@ interface EuiccChannel { val logicalSlotId: Int val portId: Int + /** + * A semi-obscure wrapper over the integer ID of a secure element on a card. + * + * Because the ID is arbitrary, this is intended to discourage the use of the + * integer value directly. Additionally, it prevents accidentally calling the + * wrong function in EuiccChannelManager with a ton of integer parameters. + */ + class SecureElementId private constructor(val id: Int) { + companion object { + val DEFAULT = SecureElementId(0) + + /** + * Create a SecureElementId from an integer ID. You should not + */ + fun createFromInt(id: Int): SecureElementId = + SecureElementId(id) + } + + override fun hashCode(): Int = + id.hashCode() + + override fun equals(other: Any?): Boolean = + if (other is SecureElementId) { + this.id == other.id + } else { + super.equals(other) + } + } + /** * Some chips support multiple SEs on one chip. The seId here is intended * to distinguish channels opened from these different SEs. - * - * Note that this ID is arbitrary and heavily depends on the order in which - * we attempt to open the ISD-R AIDs. As such, it shall not be treated with - * any significance other than as a transient ID. */ - val seId: Int + val seId: SecureElementId val lpa: LocalProfileAssistant diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelFactory.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelFactory.kt index 53ac7ef..a8051af 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelFactory.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelFactory.kt @@ -6,12 +6,12 @@ import im.angry.openeuicc.util.* // This class is here instead of inside DI because it contains a bit more logic than just // "dumb" dependency injection. interface EuiccChannelFactory { - suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat, isdrAid: ByteArray, seInt: Int): EuiccChannel? + suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat, isdrAid: ByteArray, seId: EuiccChannel.SecureElementId): EuiccChannel? fun tryOpenUsbEuiccChannel( ccidCtx: UsbCcidContext, isdrAid: ByteArray, - seInt: Int + seId: EuiccChannel.SecureElementId ): EuiccChannel? /** diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt index bf98397..491f6d2 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt @@ -14,7 +14,7 @@ class EuiccChannelImpl( override val intrinsicChannelName: String?, override val apduInterface: ApduInterface, override val isdrAid: ByteArray, - override val seId: Int, + override val seId: EuiccChannel.SecureElementId, verboseLoggingFlow: Flow, ignoreTLSCertificateFlow: Flow ) : EuiccChannel { 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 ffa3606..d1bff86 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 @@ -81,16 +81,16 @@ interface EuiccChannelManager { suspend fun withEuiccChannel( physicalSlotId: Int, portId: Int, - seId: Int = 0, + seId: EuiccChannel.SecureElementId = EuiccChannel.SecureElementId.DEFAULT, fn: suspend (EuiccChannel) -> R ): R /** - * Same as withEuiccChannel(Int, Int, (EuiccChannel) -> R) but instead uses logical slot ID + * Same as withEuiccChannel(Int, Int, SecureElementId, (EuiccChannel) -> R) but instead uses logical slot ID */ suspend fun withEuiccChannel( logicalSlotId: Int, - seId: Int = 0, + seId: EuiccChannel.SecureElementId = EuiccChannel.SecureElementId.DEFAULT, fn: suspend (EuiccChannel) -> R ): R diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt index 8496189..a36c1b6 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt @@ -26,7 +26,7 @@ class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel { get() = channel.logicalSlotId override val portId: Int get() = channel.portId - override val seId: Int + override val seId: EuiccChannel.SecureElementId get() = channel.seId private val lpaDelegate = lazy { LocalProfileAssistantWrapper(channel.lpa) diff --git a/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt index b44bef8..f662ad0 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt @@ -17,7 +17,12 @@ private typealias BundleSetter = Bundle.() -> Unit // We must use extension functions because there is no way to add bounds to the type of "self" // in the definition of an interface, so the only way is to limit where the extension functions // can be applied. -fun newInstanceEuicc(clazz: Class, slotId: Int, portId: Int, addArguments: BundleSetter = {}): T +fun newInstanceEuicc( + clazz: Class, + slotId: Int, + portId: Int, + addArguments: BundleSetter = {} +): T where T : Fragment, T : EuiccChannelFragmentMarker = clazz.getDeclaredConstructor().newInstance().apply { arguments = Bundle() @@ -54,7 +59,12 @@ val T.euiccChannelManagerService: EuiccChannelManagerService suspend fun T.withEuiccChannel(fn: suspend (EuiccChannel) -> R): R where T : Fragment, T : EuiccChannelFragmentMarker { ensureEuiccChannelManager() - return euiccChannelManager.withEuiccChannel(slotId, portId, fn) + return euiccChannelManager.withEuiccChannel( + slotId, + portId, + EuiccChannel.SecureElementId.DEFAULT, + fn + ) } suspend fun T.ensureEuiccChannelManager() where T : Fragment, T : OpenEuiccContextMarker = diff --git a/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt b/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt index cb2bbc3..dcf2328 100644 --- a/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt +++ b/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt @@ -17,7 +17,7 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto override suspend fun tryOpenEuiccChannel( port: UiccPortInfoCompat, isdrAid: ByteArray, - seId: Int, + seId: EuiccChannel.SecureElementId, ): EuiccChannel? { val port = port as RealUiccPortInfoCompat if (port.card.isRemovable) { From 5695f81e0d47e751cbe2faac46c85decf9a12b83 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 8 Jun 2025 15:03:47 -0400 Subject: [PATCH 04/32] Manual Parcelable implementation for SecureElementId --- .../im/angry/openeuicc/core/EuiccChannel.kt | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt index f0cf329..19dd682 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt @@ -1,5 +1,7 @@ package im.angry.openeuicc.core +import android.os.Parcel +import android.os.Parcelable import im.angry.openeuicc.util.* import net.typeblog.lpac_jni.ApduInterface import net.typeblog.lpac_jni.LocalProfileAssistant @@ -20,7 +22,7 @@ interface EuiccChannel { * integer value directly. Additionally, it prevents accidentally calling the * wrong function in EuiccChannelManager with a ton of integer parameters. */ - class SecureElementId private constructor(val id: Int) { + class SecureElementId private constructor(val id: Int) : Parcelable { companion object { val DEFAULT = SecureElementId(0) @@ -29,6 +31,15 @@ interface EuiccChannel { */ fun createFromInt(id: Int): SecureElementId = SecureElementId(id) + + @Suppress("unused") + @JvmField + val CREATOR = object : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): SecureElementId = + createFromInt(parcel.readInt()) + + override fun newArray(size: Int): Array = arrayOfNulls(size) + } } override fun hashCode(): Int = @@ -40,6 +51,12 @@ interface EuiccChannel { } else { super.equals(other) } + + override fun describeContents(): Int = id + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeInt(id) + } } /** From be69c882280e2555f4a2aff4832392bac5d939ba Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 8 Jun 2025 15:26:32 -0400 Subject: [PATCH 05/32] Add support for multi-SE cards in most UI components --- .../openeuicc/di/DefaultUiComponentFactory.kt | 9 ++- .../angry/openeuicc/di/UiComponentFactory.kt | 8 ++- .../angry/openeuicc/ui/EuiccInfoActivity.kt | 41 ++++++++++-- .../openeuicc/ui/EuiccManagementFragment.kt | 47 +++++++++++--- .../openeuicc/ui/EuiccMemoryResetFragment.kt | 5 +- .../im/angry/openeuicc/ui/MainActivity.kt | 3 +- .../openeuicc/ui/NotificationsActivity.kt | 64 +++++++++++++------ .../openeuicc/ui/ProfileDeleteFragment.kt | 5 +- .../openeuicc/ui/ProfileRenameFragment.kt | 5 +- .../openeuicc/ui/UsbCcidReaderFragment.kt | 5 +- .../util/EuiccChannelFragmentUtils.kt | 18 +++++- .../di/PrivilegedUiComponentFactory.kt | 9 ++- .../ui/PrivilegedEuiccManagementFragment.kt | 11 +++- 13 files changed, 179 insertions(+), 51 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/di/DefaultUiComponentFactory.kt b/app-common/src/main/java/im/angry/openeuicc/di/DefaultUiComponentFactory.kt index 52a501a..d268da8 100644 --- a/app-common/src/main/java/im/angry/openeuicc/di/DefaultUiComponentFactory.kt +++ b/app-common/src/main/java/im/angry/openeuicc/di/DefaultUiComponentFactory.kt @@ -2,13 +2,18 @@ package im.angry.openeuicc.di import androidx.fragment.app.Fragment import androidx.preference.PreferenceFragmentCompat +import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.ui.EuiccManagementFragment import im.angry.openeuicc.ui.NoEuiccPlaceholderFragment import im.angry.openeuicc.ui.SettingsFragment open class DefaultUiComponentFactory : UiComponentFactory { - override fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment = - EuiccManagementFragment.newInstance(slotId, portId) + override fun createEuiccManagementFragment( + slotId: Int, + portId: Int, + seId: EuiccChannel.SecureElementId + ): EuiccManagementFragment = + EuiccManagementFragment.newInstance(slotId, portId, seId) override fun createNoEuiccPlaceholderFragment(): Fragment = NoEuiccPlaceholderFragment() diff --git a/app-common/src/main/java/im/angry/openeuicc/di/UiComponentFactory.kt b/app-common/src/main/java/im/angry/openeuicc/di/UiComponentFactory.kt index 2c3c72b..6a4d13f 100644 --- a/app-common/src/main/java/im/angry/openeuicc/di/UiComponentFactory.kt +++ b/app-common/src/main/java/im/angry/openeuicc/di/UiComponentFactory.kt @@ -2,10 +2,16 @@ package im.angry.openeuicc.di import androidx.fragment.app.Fragment import androidx.preference.PreferenceFragmentCompat +import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.ui.EuiccManagementFragment interface UiComponentFactory { - fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment + fun createEuiccManagementFragment( + slotId: Int, + portId: Int, + seId: EuiccChannel.SecureElementId + ): EuiccManagementFragment + fun createNoEuiccPlaceholderFragment(): Fragment fun createSettingsFragment(): Fragment } \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt index f0bff39..4eb3500 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt @@ -36,6 +36,7 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { private lateinit var infoList: RecyclerView private var logicalSlotId: Int = -1 + private var seId: EuiccChannel.SecureElementId = EuiccChannel.SecureElementId.DEFAULT data class Item( @StringRes @@ -60,6 +61,12 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { } logicalSlotId = intent.getIntExtra("logicalSlotId", 0) + seId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableExtra("seId", EuiccChannel.SecureElementId::class.java) + } else { + @Suppress("DEPRECATION") + intent.getParcelableExtra("seId")!! + } ?: EuiccChannel.SecureElementId.DEFAULT val channelTitle = if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { getString(R.string.usb) @@ -100,19 +107,43 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { private fun buildEuiccInfoItems(channel: EuiccChannel) = buildList { add(Item(R.string.euicc_info_access_mode, channel.type)) - add(Item(R.string.euicc_info_removable, formatByBoolean(channel.port.card.isRemovable, YES_NO))) - add(Item(R.string.euicc_info_eid, channel.lpa.eID, copiedToastResId = R.string.toast_eid_copied)) + add( + Item( + R.string.euicc_info_removable, + formatByBoolean(channel.port.card.isRemovable, YES_NO) + ) + ) + add( + Item( + R.string.euicc_info_eid, + channel.lpa.eID, + copiedToastResId = R.string.toast_eid_copied + ) + ) add(Item(R.string.euicc_info_isdr_aid, channel.isdrAid.encodeHex())) channel.tryParseEuiccVendorInfo()?.let { vendorInfo -> vendorInfo.skuName?.let { add(Item(R.string.euicc_info_sku, it)) } - vendorInfo.serialNumber?.let { add(Item(R.string.euicc_info_sn, it, copiedToastResId = R.string.toast_sn_copied)) } + vendorInfo.serialNumber?.let { + add( + Item( + R.string.euicc_info_sn, + it, + copiedToastResId = R.string.toast_sn_copied + ) + ) + } vendorInfo.firmwareVersion?.let { add(Item(R.string.euicc_info_fw_ver, it)) } vendorInfo.bootloaderVersion?.let { add(Item(R.string.euicc_info_bl_ver, it)) } } channel.lpa.euiccInfo2.let { info -> add(Item(R.string.euicc_info_sgp22_version, info?.sgp22Version.toString())) add(Item(R.string.euicc_info_firmware_version, info?.euiccFirmwareVersion.toString())) - add(Item(R.string.euicc_info_globalplatform_version, info?.globalPlatformVersion.toString())) + add( + Item( + R.string.euicc_info_globalplatform_version, + info?.globalPlatformVersion.toString() + ) + ) add(Item(R.string.euicc_info_pp_version, info?.ppVersion.toString())) add(Item(R.string.euicc_info_sas_accreditation_number, info?.sasAccreditationNumber)) add(Item(R.string.euicc_info_free_nvram, info?.freeNvram?.let(::formatFreeSpace))) @@ -130,7 +161,7 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { } add(Item(R.string.euicc_info_ci_type, getString(resId))) } - val atr = channel.atr?.encodeHex() ?: getString(R.string.information_unavailable) + val atr = channel.atr?.encodeHex() ?: getString(R.string.information_unavailable) add(Item(R.string.euicc_info_atr, atr, copiedToastResId = R.string.toast_atr_copied)) } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt index 12995ff..4be3ebc 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt @@ -31,6 +31,7 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.google.android.material.floatingactionbutton.FloatingActionButton import net.typeblog.lpac_jni.LocalProfileInfo import im.angry.openeuicc.common.R +import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.service.EuiccChannelManagerService import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone import im.angry.openeuicc.ui.wizard.DownloadWizardActivity @@ -49,8 +50,12 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, companion object { const val TAG = "EuiccManagementFragment" - fun newInstance(slotId: Int, portId: Int): EuiccManagementFragment = - newInstanceEuicc(EuiccManagementFragment::class.java, slotId, portId) + fun newInstance( + slotId: Int, + portId: Int, + seId: EuiccChannel.SecureElementId + ): EuiccManagementFragment = + newInstanceEuicc(EuiccManagementFragment::class.java, slotId, portId, seId) } private lateinit var swipeRefresh: SwipeRefreshLayout @@ -148,6 +153,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, R.id.show_notifications -> { Intent(requireContext(), NotificationsActivity::class.java).apply { putExtra("logicalSlotId", logicalSlotId) + putExtra("seId", seId) startActivity(this) } true @@ -156,13 +162,14 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, R.id.euicc_info -> { Intent(requireContext(), EuiccInfoActivity::class.java).apply { putExtra("logicalSlotId", logicalSlotId) + putExtra("seId", seId) startActivity(this) } true } R.id.euicc_memory_reset -> { - EuiccMemoryResetFragment.newInstance(slotId, portId, eid) + EuiccMemoryResetFragment.newInstance(slotId, portId, seId, eid) .show(childFragmentManager, EuiccMemoryResetFragment.TAG) true } @@ -294,7 +301,10 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, } } - protected open fun populatePopupWithProfileActions(popup: PopupMenu, profile: LocalProfileInfo) { + protected open fun populatePopupWithProfileActions( + popup: PopupMenu, + profile: LocalProfileInfo + ) { popup.inflate(R.menu.profile_options) if (profile.isEnabled) { popup.menu.findItem(R.id.enable).isVisible = false @@ -321,7 +331,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, } } - inner class FooterViewHolder: ViewHolder(FrameLayout(requireContext())) { + inner class FooterViewHolder : ViewHolder(FrameLayout(requireContext())) { init { itemView.layoutParams = ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, @@ -413,20 +423,36 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, enableOrDisableProfile(profile.iccid, true) true } + R.id.disable -> { enableOrDisableProfile(profile.iccid, false) true } + R.id.rename -> { - ProfileRenameFragment.newInstance(slotId, portId, profile.iccid, profile.displayName) + ProfileRenameFragment.newInstance( + slotId, + portId, + seId, + profile.iccid, + profile.displayName + ) .show(childFragmentManager, ProfileRenameFragment.TAG) true } + R.id.delete -> { - ProfileDeleteFragment.newInstance(slotId, portId, profile.iccid, profile.displayName) + ProfileDeleteFragment.newInstance( + slotId, + portId, + seId, + profile.iccid, + profile.displayName + ) .show(childFragmentManager, ProfileDeleteFragment.TAG) true } + else -> false } } @@ -438,9 +464,11 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = when (ViewHolder.Type.fromInt(viewType)) { ViewHolder.Type.PROFILE -> { - val view = LayoutInflater.from(parent.context).inflate(R.layout.euicc_profile, parent, false) + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.euicc_profile, parent, false) ProfileViewHolder(view) } + ViewHolder.Type.FOOTER -> { FooterViewHolder() } @@ -451,9 +479,11 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, position < profiles.size -> { ViewHolder.Type.PROFILE.value } + position >= profiles.size && position < profiles.size + footerViews.size -> { ViewHolder.Type.FOOTER.value } + else -> -1 } @@ -462,6 +492,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, is ProfileViewHolder -> { holder.setProfile(profiles[position]) } + is FooterViewHolder -> { holder.attach(footerViews[position - profiles.size]) } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccMemoryResetFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccMemoryResetFragment.kt index 086a849..79a8f0a 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccMemoryResetFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccMemoryResetFragment.kt @@ -11,6 +11,7 @@ import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import im.angry.openeuicc.common.R +import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone import im.angry.openeuicc.util.EuiccChannelFragmentMarker import im.angry.openeuicc.util.EuiccProfilesChangedListener @@ -29,8 +30,8 @@ class EuiccMemoryResetFragment : DialogFragment(), EuiccChannelFragmentMarker { private const val FIELD_EID = "eid" - fun newInstance(slotId: Int, portId: Int, eid: String) = - newInstanceEuicc(EuiccMemoryResetFragment::class.java, slotId, portId) { + fun newInstance(slotId: Int, portId: Int, seId: EuiccChannel.SecureElementId, eid: String) = + newInstanceEuicc(EuiccMemoryResetFragment::class.java, slotId, portId, seId) { putString(FIELD_EID, eid) } } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt index 01d0ab2..819a3ed 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt @@ -23,6 +23,7 @@ import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator import im.angry.openeuicc.common.R +import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.util.* import kotlinx.coroutines.Dispatchers @@ -166,7 +167,7 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { val channelName = appContainer.customizableTextProvider.formatInternalChannelName(channel.logicalSlotId) newPages.add(Page(channel.logicalSlotId, channelName) { - appContainer.uiComponentFactory.createEuiccManagementFragment(slotId, portId) + appContainer.uiComponentFactory.createEuiccManagementFragment(slotId, portId, EuiccChannel.SecureElementId.DEFAULT) }) } }.collect() diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt index 21a2d40..594fdd1 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt @@ -1,6 +1,7 @@ package im.angry.openeuicc.ui import android.annotation.SuppressLint +import android.os.Build import android.os.Bundle import android.text.Html import android.view.ContextMenu @@ -20,6 +21,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import im.angry.openeuicc.common.R +import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.util.* import kotlinx.coroutines.Dispatchers @@ -27,12 +29,13 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import net.typeblog.lpac_jni.LocalProfileNotification -class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker { +class NotificationsActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { private lateinit var swipeRefresh: SwipeRefreshLayout private lateinit var notificationList: RecyclerView private val notificationAdapter = NotificationAdapter() private var logicalSlotId = -1 + private var seId = EuiccChannel.SecureElementId.DEFAULT override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() @@ -51,11 +54,22 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker { override fun onInit() { notificationList.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) - notificationList.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL)) + notificationList.addItemDecoration( + DividerItemDecoration( + this, + LinearLayoutManager.VERTICAL + ) + ) notificationList.adapter = notificationAdapter registerForContextMenu(notificationList) logicalSlotId = intent.getIntExtra("logicalSlotId", 0) + seId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableExtra("seId", EuiccChannel.SecureElementId::class.java) + } else { + @Suppress("DEPRECATION") + intent.getParcelableExtra("seId")!! + } ?: EuiccChannel.SecureElementId.DEFAULT // This is slightly different from the MainActivity logic // due to the length (we don't want to display the full USB product name) @@ -86,6 +100,7 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker { finish() true } + R.id.help -> { AlertDialog.Builder(this, R.style.AlertDialogTheme).apply { setMessage(R.string.profile_notifications_help) @@ -96,6 +111,7 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker { } true } + else -> super.onOptionsItemSelected(item) } @@ -114,20 +130,20 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker { } private fun refresh() { - launchTask { - notificationAdapter.notifications = - euiccChannelManager.withEuiccChannel(logicalSlotId) { channel -> - val nameMap = buildMap { - for (profile in channel.lpa.profiles) { - put(profile.iccid, profile.displayName) - } - } + launchTask { + notificationAdapter.notifications = + euiccChannelManager.withEuiccChannel(logicalSlotId) { channel -> + val nameMap = buildMap { + for (profile in channel.lpa.profiles) { + put(profile.iccid, profile.displayName) + } + } - channel.lpa.notifications.map { - LocalProfileNotificationWrapper(it, nameMap[it.iccid] ?: "???") - } - } - } + channel.lpa.notifications.map { + LocalProfileNotificationWrapper(it, nameMap[it.iccid] ?: "???") + } + } + } } data class LocalProfileNotificationWrapper( @@ -136,7 +152,7 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker { ) @SuppressLint("ClickableViewAccessibility") - inner class NotificationViewHolder(private val root: View): + inner class NotificationViewHolder(private val root: View) : RecyclerView.ViewHolder(root), View.OnCreateContextMenuListener, OnMenuItemClickListener { private val address: TextView = root.requireViewById(R.id.notification_address) private val sequenceNumber: TextView = @@ -170,7 +186,8 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker { LocalProfileNotification.Operation.Delete -> R.string.profile_notification_operation_delete LocalProfileNotification.Operation.Enable -> R.string.profile_notification_operation_enable LocalProfileNotification.Operation.Disable -> R.string.profile_notification_operation_disable - }) + } + ) fun updateNotification(value: LocalProfileNotificationWrapper) { notification = value @@ -181,10 +198,13 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker { value.inner.seqNumber ) profileName.text = Html.fromHtml( - root.context.getString(R.string.profile_notification_name_format, + root.context.getString( + R.string.profile_notification_name_format, operationToLocalizedText(value.inner.profileManagementOperation), - value.profileName, value.inner.iccid), - Html.FROM_HTML_MODE_COMPACT) + value.profileName, value.inner.iccid + ), + Html.FROM_HTML_MODE_COMPACT + ) } override fun onCreateContextMenu( @@ -213,6 +233,7 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker { } true } + R.id.notification_delete -> { launchTask { withContext(Dispatchers.IO) { @@ -225,11 +246,12 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker { } true } + else -> false } } - inner class NotificationAdapter: RecyclerView.Adapter() { + inner class NotificationAdapter : RecyclerView.Adapter() { var notifications: List = listOf() @SuppressLint("NotifyDataSetChanged") set(value) { diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt index 38d1bc6..d5cf496 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt @@ -9,6 +9,7 @@ import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment import androidx.lifecycle.lifecycleScope import im.angry.openeuicc.common.R +import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone import im.angry.openeuicc.util.* import kotlinx.coroutines.flow.onStart @@ -20,8 +21,8 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker { private const val FIELD_ICCID = "iccid" private const val FIELD_NAME = "name" - fun newInstance(slotId: Int, portId: Int, iccid: String, name: String) = - newInstanceEuicc(ProfileDeleteFragment::class.java, slotId, portId) { + fun newInstance(slotId: Int, portId: Int, seId: EuiccChannel.SecureElementId, iccid: String, name: String) = + newInstanceEuicc(ProfileDeleteFragment::class.java, slotId, portId, seId) { putString(FIELD_ICCID, iccid) putString(FIELD_NAME, name) } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt index c588254..e5f8251 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt @@ -12,6 +12,7 @@ import androidx.appcompat.widget.Toolbar import androidx.lifecycle.lifecycleScope import com.google.android.material.textfield.TextInputLayout import im.angry.openeuicc.common.R +import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone import im.angry.openeuicc.util.* import kotlinx.coroutines.launch @@ -24,8 +25,8 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment const val TAG = "ProfileRenameFragment" - fun newInstance(slotId: Int, portId: Int, iccid: String, currentName: String) = - newInstanceEuicc(ProfileRenameFragment::class.java, slotId, portId) { + fun newInstance(slotId: Int, portId: Int, seId: EuiccChannel.SecureElementId, iccid: String, currentName: String) = + newInstanceEuicc(ProfileRenameFragment::class.java, slotId, portId, seId) { putString(FIELD_ICCID, iccid) putString(FIELD_CURRENT_NAME, currentName) } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/UsbCcidReaderFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/UsbCcidReaderFragment.kt index 7a52ca0..a14e47d 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/UsbCcidReaderFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/UsbCcidReaderFragment.kt @@ -20,6 +20,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.commit import androidx.lifecycle.lifecycleScope import im.angry.openeuicc.common.R +import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.util.* import kotlinx.coroutines.Dispatchers @@ -156,7 +157,9 @@ class UsbCcidReaderFragment : Fragment(), OpenEuiccContextMarker { R.id.child_container, appContainer.uiComponentFactory.createEuiccManagementFragment( slotId = EuiccChannelManager.USB_CHANNEL_ID, - portId = 0 + portId = 0, + // TODO: What if a USB card has multiple SEs? + seId = EuiccChannel.SecureElementId.DEFAULT ) ) } diff --git a/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt index f662ad0..d2d8c4b 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt @@ -1,5 +1,6 @@ package im.angry.openeuicc.util +import android.os.Build import android.os.Bundle import androidx.fragment.app.Fragment import im.angry.openeuicc.core.EuiccChannel @@ -9,6 +10,7 @@ import im.angry.openeuicc.ui.BaseEuiccAccessActivity private const val FIELD_SLOT_ID = "slotId" private const val FIELD_PORT_ID = "portId" +private const val FIELD_SE_ID = "seId" interface EuiccChannelFragmentMarker : OpenEuiccContextMarker @@ -21,6 +23,7 @@ fun newInstanceEuicc( clazz: Class, slotId: Int, portId: Int, + seId: EuiccChannel.SecureElementId, addArguments: BundleSetter = {} ): T where T : Fragment, T : EuiccChannelFragmentMarker = @@ -28,6 +31,7 @@ fun newInstanceEuicc( arguments = Bundle() arguments!!.putInt(FIELD_SLOT_ID, slotId) arguments!!.putInt(FIELD_PORT_ID, portId) + arguments!!.putParcelable(FIELD_SE_ID, seId) arguments!!.addArguments() } @@ -40,6 +44,18 @@ val T.slotId: Int val T.portId: Int where T : Fragment, T : EuiccChannelFragmentMarker get() = requireArguments().getInt(FIELD_PORT_ID) +val T.seId: EuiccChannel.SecureElementId + where T : Fragment, T : EuiccChannelFragmentMarker + get() = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + requireArguments().getParcelable( + FIELD_SE_ID, + EuiccChannel.SecureElementId::class.java + )!! + } else { + @Suppress("DEPRECATION") + requireArguments().getParcelable(FIELD_SE_ID)!! + } val T.isUsb: Boolean where T : Fragment, T : EuiccChannelFragmentMarker get() = slotId == EuiccChannelManager.USB_CHANNEL_ID @@ -62,7 +78,7 @@ suspend fun T.withEuiccChannel(fn: suspend (EuiccChannel) -> R): R return euiccChannelManager.withEuiccChannel( slotId, portId, - EuiccChannel.SecureElementId.DEFAULT, + seId, fn ) } diff --git a/app/src/main/java/im/angry/openeuicc/di/PrivilegedUiComponentFactory.kt b/app/src/main/java/im/angry/openeuicc/di/PrivilegedUiComponentFactory.kt index e5b747a..8af5833 100644 --- a/app/src/main/java/im/angry/openeuicc/di/PrivilegedUiComponentFactory.kt +++ b/app/src/main/java/im/angry/openeuicc/di/PrivilegedUiComponentFactory.kt @@ -1,13 +1,18 @@ package im.angry.openeuicc.di import androidx.fragment.app.Fragment +import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.ui.EuiccManagementFragment import im.angry.openeuicc.ui.PrivilegedEuiccManagementFragment import im.angry.openeuicc.ui.PrivilegedSettingsFragment class PrivilegedUiComponentFactory : DefaultUiComponentFactory() { - override fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment = - PrivilegedEuiccManagementFragment.newInstance(slotId, portId) + override fun createEuiccManagementFragment( + slotId: Int, + portId: Int, + seId: EuiccChannel.SecureElementId + ): EuiccManagementFragment = + PrivilegedEuiccManagementFragment.newInstance(slotId, portId, seId) override fun createSettingsFragment(): Fragment = PrivilegedSettingsFragment() diff --git a/app/src/main/java/im/angry/openeuicc/ui/PrivilegedEuiccManagementFragment.kt b/app/src/main/java/im/angry/openeuicc/ui/PrivilegedEuiccManagementFragment.kt index 12b60bd..bc38fb5 100644 --- a/app/src/main/java/im/angry/openeuicc/ui/PrivilegedEuiccManagementFragment.kt +++ b/app/src/main/java/im/angry/openeuicc/ui/PrivilegedEuiccManagementFragment.kt @@ -5,13 +5,18 @@ import android.view.ViewGroup import android.widget.Button import android.widget.PopupMenu import im.angry.openeuicc.R +import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.util.* import net.typeblog.lpac_jni.LocalProfileInfo -class PrivilegedEuiccManagementFragment: EuiccManagementFragment() { +class PrivilegedEuiccManagementFragment : EuiccManagementFragment() { companion object { - fun newInstance(slotId: Int, portId: Int): EuiccManagementFragment = - newInstanceEuicc(PrivilegedEuiccManagementFragment::class.java, slotId, portId) + fun newInstance( + slotId: Int, + portId: Int, + seId: EuiccChannel.SecureElementId + ): EuiccManagementFragment = + newInstanceEuicc(PrivilegedEuiccManagementFragment::class.java, slotId, portId, seId) } private var isMEP = false From 970dc1946257f2ecee6425da18593785c086404a Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 8 Jun 2025 17:27:10 -0400 Subject: [PATCH 06/32] Support displaying multiple SEs --- .../core/DefaultEuiccChannelManager.kt | 14 ++++++++ .../openeuicc/core/EuiccChannelManager.kt | 8 +++++ .../im/angry/openeuicc/ui/MainActivity.kt | 36 +++++++++++-------- 3 files changed, 44 insertions(+), 14 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt index 31febe2..2d0493b 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt @@ -284,6 +284,20 @@ open class DefaultEuiccChannelManager( } }) + override fun flowEuiccSecureElements( + slotId: Int, + portId: Int + ): Flow = flow { + // Emit the "default" channel first + // TODO: This function below should really return a list, not just one SE + findEuiccChannelByPort(slotId, portId, seId = EuiccChannel.SecureElementId.DEFAULT)?.let { + emit(EuiccChannel.SecureElementId.DEFAULT) + + channelCache.filter { it.slotId == slotId && it.portId == portId && it.seId != EuiccChannel.SecureElementId.DEFAULT } + .forEach { emit(it.seId) } + } + } + override suspend fun tryOpenUsbEuiccChannel(): Pair = withContext(Dispatchers.IO) { usbManager.deviceList.values.forEach { device -> 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 d1bff86..eb8e87c 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 @@ -37,6 +37,14 @@ interface EuiccChannelManager { */ fun flowAllOpenEuiccPorts(): Flow> + /** + * Iterate over all the Secure Elements available on one eUICC. + * + * This is going to almost always return only 1 result, except in the case where + * a card has multiple SEs. + */ + fun flowEuiccSecureElements(slotId: Int, portId: Int): Flow + /** * Scan all possible USB devices for CCID readers that may contain eUICC cards. * If found, try to open it for access, and add it to the internal EuiccChannel cache diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt index 819a3ed..93f51dd 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt @@ -113,10 +113,12 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { startActivity(Intent(this, SettingsActivity::class.java)) true } + R.id.reload -> { refresh() true } + else -> super.onOptionsItemSelected(item) } @@ -155,21 +157,27 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { euiccChannelManager.flowInternalEuiccPorts().onEach { (slotId, portId) -> Log.d(TAG, "slot $slotId port $portId") - euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> - if (preferenceRepository.verboseLoggingFlow.first()) { - Log.d(TAG, channel.lpa.eID) - } - // Request the system to refresh the list of profiles every time we start - // Note that this is currently supposed to be no-op when unprivileged, - // but it could change in the future - euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId) + euiccChannelManager.flowEuiccSecureElements(slotId, portId).onEach { seId -> + euiccChannelManager.withEuiccChannel(slotId, portId, seId) { channel -> + if (preferenceRepository.verboseLoggingFlow.first()) { + Log.d(TAG, channel.lpa.eID) + } + // Request the system to refresh the list of profiles every time we start + // Note that this is currently supposed to be no-op when unprivileged, + // but it could change in the future + euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId) - val channelName = - appContainer.customizableTextProvider.formatInternalChannelName(channel.logicalSlotId) - newPages.add(Page(channel.logicalSlotId, channelName) { - appContainer.uiComponentFactory.createEuiccManagementFragment(slotId, portId, EuiccChannel.SecureElementId.DEFAULT) - }) - } + val channelName = + appContainer.customizableTextProvider.formatInternalChannelName(channel.logicalSlotId) + newPages.add(Page(channel.logicalSlotId, channelName) { + appContainer.uiComponentFactory.createEuiccManagementFragment( + slotId, + portId, + seId + ) + }) + } + }.collect() }.collect() // If USB readers exist, add them at the very last From 2f1c17c58a5a23bb099d3ae1b6e9d1a51557f80e Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Tue, 17 Jun 2025 08:32:05 -0400 Subject: [PATCH 07/32] ui: Fix unpriv compilation --- .../angry/openeuicc/di/UnprivilegedUiComponentFactory.kt | 9 +++++++-- .../openeuicc/ui/UnprivilegedEuiccManagementFragment.kt | 9 +++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/app-unpriv/src/main/java/im/angry/openeuicc/di/UnprivilegedUiComponentFactory.kt b/app-unpriv/src/main/java/im/angry/openeuicc/di/UnprivilegedUiComponentFactory.kt index 06c489c..2c03d35 100644 --- a/app-unpriv/src/main/java/im/angry/openeuicc/di/UnprivilegedUiComponentFactory.kt +++ b/app-unpriv/src/main/java/im/angry/openeuicc/di/UnprivilegedUiComponentFactory.kt @@ -1,6 +1,7 @@ package im.angry.openeuicc.di import androidx.fragment.app.Fragment +import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.ui.EuiccManagementFragment import im.angry.openeuicc.ui.SettingsFragment import im.angry.openeuicc.ui.UnprivilegedEuiccManagementFragment @@ -8,8 +9,12 @@ import im.angry.openeuicc.ui.UnprivilegedNoEuiccPlaceholderFragment import im.angry.openeuicc.ui.UnprivilegedSettingsFragment class UnprivilegedUiComponentFactory : DefaultUiComponentFactory() { - override fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment = - UnprivilegedEuiccManagementFragment.newInstance(slotId, portId) + override fun createEuiccManagementFragment( + slotId: Int, + portId: Int, + seId: EuiccChannel.SecureElementId + ): EuiccManagementFragment = + UnprivilegedEuiccManagementFragment.newInstance(slotId, portId, seId) override fun createNoEuiccPlaceholderFragment(): Fragment = UnprivilegedNoEuiccPlaceholderFragment() diff --git a/app-unpriv/src/main/java/im/angry/openeuicc/ui/UnprivilegedEuiccManagementFragment.kt b/app-unpriv/src/main/java/im/angry/openeuicc/ui/UnprivilegedEuiccManagementFragment.kt index 7cf300c..764287c 100644 --- a/app-unpriv/src/main/java/im/angry/openeuicc/ui/UnprivilegedEuiccManagementFragment.kt +++ b/app-unpriv/src/main/java/im/angry/openeuicc/ui/UnprivilegedEuiccManagementFragment.kt @@ -7,6 +7,7 @@ import android.view.MenuInflater import android.view.MenuItem import android.widget.Toast import im.angry.easyeuicc.R +import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.util.SIMToolkit import im.angry.openeuicc.util.newInstanceEuicc import im.angry.openeuicc.util.slotId @@ -16,8 +17,12 @@ class UnprivilegedEuiccManagementFragment : EuiccManagementFragment() { companion object { const val TAG = "UnprivilegedEuiccManagementFragment" - fun newInstance(slotId: Int, portId: Int): EuiccManagementFragment = - newInstanceEuicc(UnprivilegedEuiccManagementFragment::class.java, slotId, portId) + fun newInstance( + slotId: Int, + portId: Int, + seId: EuiccChannel.SecureElementId + ): EuiccManagementFragment = + newInstanceEuicc(UnprivilegedEuiccManagementFragment::class.java, slotId, portId, seId) } private val stk by lazy { From c0dc8ac19d38dcc899553ee66446ba3cf2dc7ffe Mon Sep 17 00:00:00 2001 From: septs Date: Tue, 8 Jul 2025 15:51:36 +0800 Subject: [PATCH 08/32] feat: profile sequence number --- .../angry/openeuicc/ui/EuiccManagementFragment.kt | 13 ++++++++++++- app-common/src/main/res/layout/euicc_profile.xml | 8 ++++++++ app-common/src/main/res/layout/fragment_euicc.xml | 1 + app-common/src/main/res/values/strings.xml | 1 + 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt index 12995ff..3c94c6c 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt @@ -347,6 +347,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, private val profileClassLabel: TextView = root.requireViewById(R.id.profile_class_label) private val profileClass: TextView = root.requireViewById(R.id.profile_class) private val profileMenu: ImageButton = root.requireViewById(R.id.profile_menu) + private val profileSeqNumber: TextView = root.requireViewById(R.id.profile_sequence_number) init { iccid.setOnClickListener { @@ -366,7 +367,9 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, true } - profileMenu.setOnClickListener { showOptionsMenu() } + profileMenu.setOnClickListener { + showOptionsMenu() + } } private lateinit var profile: LocalProfileInfo @@ -396,6 +399,13 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, iccid.transformationMethod = PasswordTransformationMethod.getInstance() } + fun setProfileSequenceNumber(index: Int) { + profileSeqNumber.text = root.context.getString( + R.string.profile_sequence_number_format, + index, + ) + } + private fun showOptionsMenu() { // Prevent users from doing multiple things at once if (invalid || swipeRefresh.isRefreshing) return @@ -461,6 +471,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, when (holder) { is ProfileViewHolder -> { holder.setProfile(profiles[position]) + holder.setProfileSequenceNumber(position + 1) } is FooterViewHolder -> { holder.attach(footerViews[position - profiles.size]) diff --git a/app-common/src/main/res/layout/euicc_profile.xml b/app-common/src/main/res/layout/euicc_profile.xml index 58d55ab..74c1d7a 100644 --- a/app-common/src/main/res/layout/euicc_profile.xml +++ b/app-common/src/main/res/layout/euicc_profile.xml @@ -129,6 +129,14 @@ app:layout_constraintTop_toBottomOf="@id/profile_class" app:layout_constraintBottom_toBottomOf="parent"/> + + diff --git a/app-common/src/main/res/layout/fragment_euicc.xml b/app-common/src/main/res/layout/fragment_euicc.xml index 4ae7523..c5fde7b 100644 --- a/app-common/src/main/res/layout/fragment_euicc.xml +++ b/app-common/src/main/res/layout/fragment_euicc.xml @@ -27,6 +27,7 @@ android:layout_height="wrap_content" android:layout_marginEnd="16dp" android:layout_marginBottom="16dp" + android:contentDescription="@string/profile_download" android:src="@drawable/ic_add" app:layout_constraintRight_toRightOf="parent" app:layout_constraintBottom_toBottomOf="parent"/> diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index 05ca15a..38bb976 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -19,6 +19,7 @@ Provisioning Operational ICCID: + #%d Enable Disable From 4ac0820bbfd5d467b4646fe5ff2d5c204b06ae23 Mon Sep 17 00:00:00 2001 From: septs Date: Thu, 10 Jul 2025 02:54:25 +0200 Subject: [PATCH 09/32] fix: improve deep-link compatibility (#198) Reviewed-on: https://gitea.angry.im/PeterCxy/OpenEUICC/pulls/198 Co-authored-by: septs Co-committed-by: septs --- app-common/src/main/AndroidManifest.xml | 5 +++-- .../im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app-common/src/main/AndroidManifest.xml b/app-common/src/main/AndroidManifest.xml index b0324dc..44c82c0 100644 --- a/app-common/src/main/AndroidManifest.xml +++ b/app-common/src/main/AndroidManifest.xml @@ -45,8 +45,9 @@ - - + + + diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt index 9e312d4..6574645 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt @@ -123,8 +123,8 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { // If we get an LPA string from deep-link intents, extract from there. // Note that `onRestoreInstanceState` could override this with user input, // but that _is_ the desired behavior. - val uri = intent.data - if (uri?.scheme == "lpa") { + val uri = intent.data ?: return + if (uri.scheme.contentEquals("lpa", ignoreCase = true)) { val parsed = LPAString.parse(uri.schemeSpecificPart) state.smdp = parsed.address state.matchingId = parsed.matchingId From 6d43a9207cbfb1c03ccf304a2c3cd117c578df34 Mon Sep 17 00:00:00 2001 From: septs Date: Wed, 16 Jul 2025 14:24:05 +0200 Subject: [PATCH 10/32] chore: simplify pretty print json string (#201) https://developer.android.com/reference/org/json/JSONObject Reviewed-on: https://gitea.angry.im/PeterCxy/OpenEUICC/pulls/201 Co-authored-by: septs Co-committed-by: septs --- .../DownloadWizardDiagnosticsFragment.kt | 4 +- .../im/angry/openeuicc/util/StringUtils.kt | 70 ------------------- 2 files changed, 3 insertions(+), 71 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt index e282196..3841868 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt @@ -8,6 +8,7 @@ import android.view.ViewGroup import android.widget.TextView import im.angry.openeuicc.common.R import im.angry.openeuicc.util.* +import org.json.JSONObject import java.util.Date class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardStepFragment() { @@ -86,9 +87,10 @@ class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardS ret.appendLine() val str = resp.data.decodeToString(throwOnInvalidSequence = false) + ret.appendLine( if (str.startsWith('{')) { - str.prettyPrintJson() + JSONObject(str).toString(2) } else { str } diff --git a/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt index 079853e..57d150b 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt @@ -41,73 +41,3 @@ fun parseIsdrAidList(s: String): List = .filter(String::isNotEmpty) .mapNotNull { runCatching(it::decodeHex).getOrNull() } .ifEmpty { listOf(EUICC_DEFAULT_ISDR_AID.decodeHex()) } - -fun String.prettyPrintJson(): String { - val ret = StringBuilder() - var inQuotes = false - var escaped = false - val indentSymbolStack = ArrayDeque() - - val addNewLine = { - ret.append('\n') - repeat(indentSymbolStack.size) { - ret.append('\t') - } - } - - var lastChar = ' ' - - for (c in this) { - when { - !inQuotes && (c == '{' || c == '[') -> { - ret.append(c) - indentSymbolStack.addLast(c) - addNewLine() - } - - !inQuotes && (c == '}' || c == ']') -> { - indentSymbolStack.removeLast() - if (lastChar != ',') { - addNewLine() - } - ret.append(c) - } - - !inQuotes && c == ',' -> { - ret.append(c) - addNewLine() - } - - !inQuotes && c == ':' -> { - ret.append(c) - ret.append(' ') - } - - inQuotes && c == '\\' -> { - ret.append(c) - escaped = true - continue - } - - !escaped && c == '"' -> { - ret.append(c) - inQuotes = !inQuotes - } - - !inQuotes && c == ' ' -> { - // Do nothing -- we ignore spaces outside of quotes by default - // This is to ensure predictable formatting - } - - else -> ret.append(c) - } - - if (escaped) { - escaped = false - } - - lastChar = c - } - - return ret.toString() -} \ No newline at end of file From 677b69cedfcae37557ca5626bb1741987ffb0a10 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 20 Jul 2025 10:29:27 -0400 Subject: [PATCH 11/32] feat: quick compatibility check Co-authored-by: septs --- app-unpriv/src/main/AndroidManifest.xml | 5 + .../openeuicc/di/UnprivilegedAppContainer.kt | 5 + .../di/UnprivilegedUiComponentFactory.kt | 7 +- .../ui/QuickCompatibilityActivity.kt | 25 ++++ .../ui/QuickCompatibilityFragment.kt | 140 ++++++++++++++++++ .../openeuicc/ui/UnprivilegedMainActivity.kt | 13 +- .../openeuicc/util/CompatibilityCheck.kt | 6 +- .../util/UnprivilegedPreferenceRepository.kt | 14 ++ .../angry/openeuicc/util/UnprivilegedUtils.kt | 6 + .../layout/activity_quick_compatibility.xml | 16 ++ .../layout/fragment_quick_compatibility.xml | 53 +++++++ app-unpriv/src/main/res/values/strings.xml | 10 ++ 12 files changed, 294 insertions(+), 6 deletions(-) create mode 100644 app-unpriv/src/main/java/im/angry/openeuicc/ui/QuickCompatibilityActivity.kt create mode 100644 app-unpriv/src/main/java/im/angry/openeuicc/ui/QuickCompatibilityFragment.kt create mode 100644 app-unpriv/src/main/java/im/angry/openeuicc/util/UnprivilegedPreferenceRepository.kt create mode 100644 app-unpriv/src/main/java/im/angry/openeuicc/util/UnprivilegedUtils.kt create mode 100644 app-unpriv/src/main/res/layout/activity_quick_compatibility.xml create mode 100644 app-unpriv/src/main/res/layout/fragment_quick_compatibility.xml diff --git a/app-unpriv/src/main/AndroidManifest.xml b/app-unpriv/src/main/AndroidManifest.xml index ce985cd..ba37079 100644 --- a/app-unpriv/src/main/AndroidManifest.xml +++ b/app-unpriv/src/main/AndroidManifest.xml @@ -23,6 +23,11 @@ + + = emptyList() + ) + } + + private val conclusion: TextView by lazy { + requireView().requireViewById(R.id.quick_availability_conclusion) + } + + private val resultSlots: TextView by lazy { + requireView().requireViewById(R.id.quick_availability_result_slots) + } + + private val resultNotes: TextView by lazy { + requireView().requireViewById(R.id.quick_availability_result_notes) + } + + private val hidden: CheckBox by lazy { + requireView().requireViewById(R.id.quick_availability_hidden) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = inflater.inflate(R.layout.fragment_quick_compatibility, container, false).apply { + requireViewById(R.id.quick_availability_device_information) + .text = formatDeviceInformation() + requireViewById