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 78a8c3f..93b002e 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: EuiccChannel.SecureElementId, ): EuiccChannel? = try { if (port.portIndex != 0) { Log.w( @@ -45,6 +46,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha context.preferenceRepository.verboseLoggingFlow ), isdrAid, + seId, context.preferenceRepository.verboseLoggingFlow, context.preferenceRepository.ignoreTLSCertificateFlow, context.preferenceRepository.es10xMssFlow, @@ -60,7 +62,8 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha override fun tryOpenUsbEuiccChannel( ccidCtx: UsbCcidContext, - isdrAid: ByteArray + isdrAid: ByteArray, + seId: EuiccChannel.SecureElementId ): EuiccChannel? = try { EuiccChannelImpl( context.getString(R.string.channel_type_usb), @@ -70,6 +73,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha ccidCtx ), isdrAid, + seId, context.preferenceRepository.verboseLoggingFlow, context.preferenceRepository.ignoreTLSCertificateFlow, context.preferenceRepository.es10xMssFlow, 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..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 @@ -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,20 @@ open class DefaultEuiccChannelManager( protected open val uiccCards: Collection get() = (0.. EuiccChannel?): EuiccChannel? { + private suspend inline fun tryOpenChannelWithKnownAids(openFn: (ByteArray, EuiccChannel.SecureElementId) -> 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, EuiccChannel.SecureElementId.createFromInt(seId))?.let { channel -> if (channel.valid) { + seId += 1 channel } else { channel.close() @@ -69,19 +74,18 @@ open class DefaultEuiccChannelManager( } } - private suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel? { + private suspend fun tryOpenEuiccChannel( + port: UiccPortInfoCompat, + seId: EuiccChannel.SecureElementId = EuiccChannel.SecureElementId.DEFAULT + ): 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 +100,18 @@ 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 +122,13 @@ open class DefaultEuiccChannelManager( } } - protected suspend fun findEuiccChannelByLogicalSlot(logicalSlotId: Int): 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 usbChannel + return@withContext usbChannels.find { it.seId == seId } } for (card in uiccCards) { @@ -131,7 +144,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 +155,18 @@ open class DefaultEuiccChannelManager( return null } - private suspend fun findEuiccChannelByPort(physicalSlotId: Int, portId: Int): 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 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,15 +185,17 @@ 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( physicalSlotId: Int, portId: Int, + seId: EuiccChannel.SecureElementId, fn: suspend (EuiccChannel) -> R ): R { - val channel = findEuiccChannelByPort(physicalSlotId, portId) + val channel = findEuiccChannelByPort(physicalSlotId, portId, seId) ?: throw EuiccChannelManager.EuiccChannelNotFoundException() val wrapper = EuiccChannelWrapper(channel) try { @@ -190,9 +209,10 @@ open class DefaultEuiccChannelManager( override suspend fun withEuiccChannel( logicalSlotId: Int, + seId: EuiccChannel.SecureElementId, fn: suspend (EuiccChannel) -> R ): R { - val channel = findEuiccChannelByLogicalSlot(logicalSlotId) + val channel = findEuiccChannelByLogicalSlot(logicalSlotId, seId) ?: throw EuiccChannelManager.EuiccChannelNotFoundException() val wrapper = EuiccChannelWrapper(channel) try { @@ -206,8 +226,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 @@ -223,7 +243,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 @@ -264,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 -> @@ -277,15 +311,17 @@ 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 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) { @@ -309,8 +345,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..4306855 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 @@ -13,6 +15,59 @@ 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) : Parcelable { + companion object { + val DEFAULT = SecureElementId(0) + + /** + * Create a SecureElementId from an integer ID. You should not call this directly + * unless you know what you're doing. + * + * This is currently only ever used in the download flow. + */ + 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 = + id.hashCode() + + override fun equals(other: Any?): Boolean = + if (other is SecureElementId) { + this.id == other.id + } else { + super.equals(other) + } + + override fun describeContents(): Int = id + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeInt(id) + } + } + + /** + * Some chips support multiple SEs on one chip. The seId here is intended + * to distinguish channels opened from these different SEs. + */ + val seId: SecureElementId + 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..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,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, seId: EuiccChannel.SecureElementId): EuiccChannel? fun tryOpenUsbEuiccChannel( ccidCtx: UsbCcidContext, - isdrAid: ByteArray + isdrAid: ByteArray, + 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 eaec522..18751c9 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 @@ -15,6 +15,7 @@ class EuiccChannelImpl( override val intrinsicChannelName: String?, override val apduInterface: ApduInterface, override val isdrAid: ByteArray, + override val seId: EuiccChannel.SecureElementId, verboseLoggingFlow: Flow, ignoreTLSCertificateFlow: Flow, es10xMssFlow: Flow, 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..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 @@ -81,14 +89,16 @@ interface EuiccChannelManager { suspend fun withEuiccChannel( physicalSlotId: Int, portId: Int, + 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: 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 361a943..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,6 +26,8 @@ class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel { get() = channel.logicalSlotId override val portId: Int get() = channel.portId + 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/di/CustomizableTextProvider.kt b/app-common/src/main/java/im/angry/openeuicc/di/CustomizableTextProvider.kt index 2c86273..14d07b6 100644 --- a/app-common/src/main/java/im/angry/openeuicc/di/CustomizableTextProvider.kt +++ b/app-common/src/main/java/im/angry/openeuicc/di/CustomizableTextProvider.kt @@ -1,5 +1,7 @@ package im.angry.openeuicc.di +import im.angry.openeuicc.core.EuiccChannel + interface CustomizableTextProvider { /** * Explanation string for when no eUICC is found on the device. @@ -13,8 +15,13 @@ interface CustomizableTextProvider { val profileSwitchingTimeoutMessage: String /** - * Format the name of a logical slot; internal only -- not intended for - * other channels such as USB. + * Format the name of a logical slot -- not for USB channels */ - fun formatInternalChannelName(logicalSlotId: Int): String + fun formatNonUsbChannelName(logicalSlotId: Int): String + + /** + * Format the name of a logical slot with a SE ID, in case of multi-SE chips; currently + * this is used in the download flow to distinguish between them on the same chip. + */ + fun formatNonUsbChannelNameWithSeId(logicalSlotId: Int, seId: EuiccChannel.SecureElementId): String } \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/di/DefaultCustomizableTextProvider.kt b/app-common/src/main/java/im/angry/openeuicc/di/DefaultCustomizableTextProvider.kt index 76227fd..516aab2 100644 --- a/app-common/src/main/java/im/angry/openeuicc/di/DefaultCustomizableTextProvider.kt +++ b/app-common/src/main/java/im/angry/openeuicc/di/DefaultCustomizableTextProvider.kt @@ -2,14 +2,22 @@ package im.angry.openeuicc.di import android.content.Context import im.angry.openeuicc.common.R +import im.angry.openeuicc.core.EuiccChannel -open class DefaultCustomizableTextProvider(private val context: Context) : CustomizableTextProvider { +open class DefaultCustomizableTextProvider(private val context: Context) : + CustomizableTextProvider { override val noEuiccExplanation: String get() = context.getString(R.string.no_euicc) override val profileSwitchingTimeoutMessage: String get() = context.getString(R.string.profile_switch_timeout) - override fun formatInternalChannelName(logicalSlotId: Int): String = + override fun formatNonUsbChannelName(logicalSlotId: Int): String = context.getString(R.string.channel_name_format, logicalSlotId) + + override fun formatNonUsbChannelNameWithSeId( + logicalSlotId: Int, + seId: EuiccChannel.SecureElementId + ): String = + context.getString(R.string.channel_name_format_se, logicalSlotId, seId.id) } \ No newline at end of file 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/service/EuiccChannelManagerService.kt b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt index 4744321..556b346 100644 --- a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt +++ b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt @@ -12,6 +12,7 @@ import androidx.core.app.NotificationManagerCompat import androidx.lifecycle.LifecycleService 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 @@ -380,6 +381,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { fun launchProfileDownloadTask( slotId: Int, portId: Int, + seId: EuiccChannel.SecureElementId, smdp: String, matchingId: String?, confirmationCode: String?, @@ -390,8 +392,8 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { getString(R.string.task_profile_download_failure), R.drawable.ic_task_sim_card_download ) { - euiccChannelManager.beginTrackedOperation(slotId, portId) { - euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> + euiccChannelManager.beginTrackedOperation(slotId, portId, seId) { + euiccChannelManager.withEuiccChannel(slotId, portId, seId) { channel -> channel.lpa.downloadProfile( smdp, matchingId, @@ -413,6 +415,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { fun launchProfileRenameTask( slotId: Int, portId: Int, + seId: EuiccChannel.SecureElementId, iccid: String, name: String ): ForegroundTaskSubscriberFlow = @@ -421,7 +424,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { getString(R.string.task_profile_rename_failure), R.drawable.ic_task_rename ) { - euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> + euiccChannelManager.withEuiccChannel(slotId, portId, seId) { channel -> channel.lpa.setNickname( iccid, name @@ -432,6 +435,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { fun launchProfileDeleteTask( slotId: Int, portId: Int, + seId: EuiccChannel.SecureElementId, iccid: String ): ForegroundTaskSubscriberFlow = launchForegroundTask( @@ -439,8 +443,8 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { getString(R.string.task_profile_delete_failure), R.drawable.ic_task_delete ) { - euiccChannelManager.beginTrackedOperation(slotId, portId) { - euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> + euiccChannelManager.beginTrackedOperation(slotId, portId, seId) { + euiccChannelManager.withEuiccChannel(slotId, portId, seId) { channel -> channel.lpa.deleteProfile(iccid) } @@ -453,6 +457,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { fun launchProfileSwitchTask( slotId: Int, portId: Int, + seId: EuiccChannel.SecureElementId, iccid: String, enable: Boolean, // Enable or disable the profile indicated in iccid reconnectTimeoutMillis: Long = 0 // 0 = do not wait for reconnect @@ -462,9 +467,9 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { getString(R.string.task_profile_switch_failure), R.drawable.ic_task_switch ) { - euiccChannelManager.beginTrackedOperation(slotId, portId) { + euiccChannelManager.beginTrackedOperation(slotId, portId, seId) { val (response, refreshed) = - euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> + euiccChannelManager.withEuiccChannel(slotId, portId, seId) { channel -> val refresh = preferenceRepository.refreshAfterSwitchFlow.first() val response = channel.lpa.switchProfile(iccid, enable, refresh) if (response || !refresh) { @@ -510,13 +515,17 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { } } - fun launchMemoryReset(slotId: Int, portId: Int): ForegroundTaskSubscriberFlow = + fun launchMemoryReset( + slotId: Int, + portId: Int, + seId: EuiccChannel.SecureElementId + ): ForegroundTaskSubscriberFlow = launchForegroundTask( getString(R.string.task_euicc_memory_reset), getString(R.string.task_euicc_memory_reset_failure), R.drawable.ic_euicc_memory_reset ) { - euiccChannelManager.beginTrackedOperation(slotId, portId) { + euiccChannelManager.beginTrackedOperation(slotId, portId, seId) { euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> channel.lpa.euiccMemoryReset() } 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 248afaf..930fa1e 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 @@ -43,6 +43,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 @@ -67,11 +68,17 @@ 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.channel_type_usb) } else { - appContainer.customizableTextProvider.formatInternalChannelName(logicalSlotId) + appContainer.customizableTextProvider.formatNonUsbChannelName(logicalSlotId) } title = getString(R.string.euicc_info_activity_title, channelTitle) @@ -99,7 +106,7 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { lifecycleScope.launch { (infoList.adapter!! as EuiccInfoAdapter).euiccInfoItems = - euiccChannelManager.withEuiccChannel(logicalSlotId, ::buildEuiccInfoItems) + euiccChannelManager.withEuiccChannel(logicalSlotId, fn = ::buildEuiccInfoItems) swipeRefresh.isRefreshing = false } @@ -107,12 +114,31 @@ 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)) } } 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 016e96f..1cb898e 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 } @@ -241,6 +248,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, val err = euiccChannelManagerService.launchProfileSwitchTask( slotId, portId, + seId, iccid, enable, reconnectTimeoutMillis = 30 * 1000 @@ -294,7 +302,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 +332,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, @@ -423,20 +434,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 } } @@ -448,9 +475,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() } @@ -461,9 +490,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 } @@ -473,6 +504,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, holder.setProfile(profiles[position]) holder.setProfileSequenceNumber(position + 1) } + 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..b123c2e 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 @@ -19,6 +20,7 @@ import im.angry.openeuicc.util.euiccChannelManagerService import im.angry.openeuicc.util.newInstanceEuicc import im.angry.openeuicc.util.notifyEuiccProfilesChanged import im.angry.openeuicc.util.portId +import im.angry.openeuicc.util.seId import im.angry.openeuicc.util.slotId import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch @@ -29,8 +31,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) } } @@ -103,7 +105,7 @@ class EuiccMemoryResetFragment : DialogFragment(), EuiccChannelFragmentMarker { ensureEuiccChannelManager() euiccChannelManagerService.waitForForegroundTask() - euiccChannelManagerService.launchMemoryReset(slotId, portId) + euiccChannelManagerService.launchMemoryReset(slotId, portId, seId) .onStart { parentFragment?.notifyEuiccProfilesChanged() 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 b42f4cf..0d96448 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 @@ -112,10 +112,12 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { startActivity(Intent(this, SettingsActivity::class.java)) true } + R.id.reload -> { refresh() true } + else -> super.onOptionsItemSelected(item) } @@ -154,21 +156,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) - }) - } + val channelName = + appContainer.customizableTextProvider.formatNonUsbChannelName(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 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 07d5f13..bd808bb 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,18 +54,29 @@ 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) val channelTitle = if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { getString(R.string.channel_type_usb) } else { - appContainer.customizableTextProvider.formatInternalChannelName(logicalSlotId) + appContainer.customizableTextProvider.formatNonUsbChannelName(logicalSlotId) } title = getString(R.string.profile_notifications_detailed_format, channelTitle) @@ -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..15424aa 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) } @@ -88,7 +89,7 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker { requireParentFragment().lifecycleScope.launch { ensureEuiccChannelManager() euiccChannelManagerService.waitForForegroundTask() - euiccChannelManagerService.launchProfileDeleteTask(slotId, portId, iccid) + euiccChannelManagerService.launchProfileDeleteTask(slotId, portId, seId, iccid) .onStart { parentFragment?.notifyEuiccProfilesChanged() runCatching(::dismiss) 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 281e625..3f23286 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) } @@ -105,7 +106,7 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment ensureEuiccChannelManager() euiccChannelManagerService.waitForForegroundTask() val response = euiccChannelManagerService - .launchProfileRenameTask(slotId, portId, iccid, newName).waitDone() + .launchProfileRenameTask(slotId, portId, seId, iccid, newName).waitDone() when (response) { is LocalProfileAssistant.ProfileNameTooLongException -> { 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/ui/wizard/DownloadWizardActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt index 6574645..b1d7e15 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 @@ -17,6 +17,7 @@ import androidx.core.view.updatePadding 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.core.EuiccChannelManager import im.angry.openeuicc.ui.BaseEuiccAccessActivity import im.angry.openeuicc.util.* @@ -24,10 +25,10 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import net.typeblog.lpac_jni.LocalProfileAssistant -class DownloadWizardActivity: BaseEuiccAccessActivity() { +class DownloadWizardActivity : BaseEuiccAccessActivity() { data class DownloadWizardState( var currentStepFragmentClassName: String?, - var selectedLogicalSlot: Int, + var selectedSyntheticSlotId: Int, var smdp: String, var matchingId: String?, var confirmationCode: String?, @@ -66,7 +67,7 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { state = DownloadWizardState( currentStepFragmentClassName = null, - selectedLogicalSlot = intent.getIntExtra("selectedLogicalSlot", 0), + selectedSyntheticSlotId = intent.getIntExtra("selectedLogicalSlot", 0), smdp = "", matchingId = null, confirmationCode = null, @@ -151,7 +152,7 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putString("currentStepFragmentClassName", state.currentStepFragmentClassName) - outState.putInt("selectedLogicalSlot", state.selectedLogicalSlot) + outState.putInt("selectedLogicalSlot", state.selectedSyntheticSlotId) outState.putString("smdp", state.smdp) outState.putString("matchingId", state.matchingId) outState.putString("confirmationCode", state.confirmationCode) @@ -167,16 +168,20 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { "currentStepFragmentClassName", state.currentStepFragmentClassName ) - state.selectedLogicalSlot = - savedInstanceState.getInt("selectedLogicalSlot", state.selectedLogicalSlot) + state.selectedSyntheticSlotId = + savedInstanceState.getInt("selectedSyntheticSlotId", state.selectedSyntheticSlotId) state.smdp = savedInstanceState.getString("smdp", state.smdp) state.matchingId = savedInstanceState.getString("matchingId", state.matchingId) state.imei = savedInstanceState.getString("imei", state.imei) state.downloadStarted = savedInstanceState.getBoolean("downloadStarted", state.downloadStarted) state.downloadTaskID = savedInstanceState.getLong("downloadTaskID", state.downloadTaskID) - state.confirmationCode = savedInstanceState.getString("confirmationCode", state.confirmationCode) - state.confirmationCodeRequired = savedInstanceState.getBoolean("confirmationCodeRequired", state.confirmationCodeRequired) + state.confirmationCode = + savedInstanceState.getString("confirmationCode", state.confirmationCode) + state.confirmationCodeRequired = savedInstanceState.getBoolean( + "confirmationCodeRequired", + state.confirmationCodeRequired + ) } private fun onPrevPressed() { @@ -200,10 +205,13 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { progressBar.isIndeterminate = true lifecycleScope.launch(Dispatchers.Main) { - if (state.selectedLogicalSlot >= 0) { + if (state.selectedSyntheticSlotId >= 0) { try { + val (slotId, seId) = DownloadWizardSlotSelectFragment.decodeSyntheticSlotId( + state.selectedSyntheticSlotId + ) // This is run on IO by default - euiccChannelManager.withEuiccChannel(state.selectedLogicalSlot) { channel -> + euiccChannelManager.withEuiccChannel(slotId, seId) { channel -> // Be _very_ sure that the channel we got is valid if (!channel.valid) throw EuiccChannelManager.EuiccChannelNotFoundException() } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt index 0048190..f8cb781 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt @@ -153,7 +153,12 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep } else { euiccChannelManagerService.waitForForegroundTask() - val (slotId, portId) = euiccChannelManager.withEuiccChannel(state.selectedLogicalSlot) { channel -> + val (logicalSlotId, seId) = DownloadWizardSlotSelectFragment.decodeSyntheticSlotId(state.selectedSyntheticSlotId) + + val (slotId, portId) = euiccChannelManager.withEuiccChannel( + logicalSlotId, + seId + ) { channel -> Pair(channel.slotId, channel.portId) } @@ -163,6 +168,7 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep val ret = euiccChannelManagerService.launchProfileDownloadTask( slotId, portId, + seId, state.smdp, state.matchingId, state.confirmationCode, diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt index 8097058..fef90ba 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt @@ -14,8 +14,11 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.ViewHolder 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.flow.asFlow +import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch @@ -24,19 +27,28 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt companion object { const val LOW_NVRAM_THRESHOLD = 30 * 1024 // < 30 KiB, alert about potential download failure + + fun decodeSyntheticSlotId(id: Int): Pair = + Pair(id shr 16, EuiccChannel.SecureElementId.createFromInt(id and 0xFF)) } private data class SlotInfo( val logicalSlotId: Int, val isRemovable: Boolean, val hasMultiplePorts: Boolean, + val hasMultipleSEs: Boolean, val portId: Int, + val seId: EuiccChannel.SecureElementId, val eID: String, val freeSpace: Int, val imei: String, val enabledProfileName: String?, val intrinsicChannelName: String?, - ) + ) { + // A synthetic slot ID used to uniquely identify this slot + SE chip in the download process + // We assume we don't have anywhere near 2^16 ports... + val syntheticSlotId: Int = (logicalSlotId shl 16) + seId.id + } private var loaded = false @@ -85,7 +97,12 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt recyclerView.adapter = adapter recyclerView.layoutManager = LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false) - recyclerView.addItemDecoration(DividerItemDecoration(requireContext(), LinearLayoutManager.VERTICAL)) + recyclerView.addItemDecoration( + DividerItemDecoration( + requireContext(), + LinearLayoutManager.VERTICAL + ) + ) return view } @@ -97,37 +114,43 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt } @SuppressLint("NotifyDataSetChanged", "MissingPermission") + @OptIn(kotlinx.coroutines.FlowPreview::class) private suspend fun init() { ensureEuiccChannelManager() showProgressBar(-1) - val slots = euiccChannelManager.flowAllOpenEuiccPorts().map { (slotId, portId) -> - euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> - SlotInfo( - channel.logicalSlotId, - channel.port.card.isRemovable, - channel.port.card.ports.size > 1, - channel.portId, - channel.lpa.eID, - channel.lpa.euiccInfo2?.freeNvram ?: 0, - try { - telephonyManager.getImei(channel.logicalSlotId) ?: "" - } catch (e: Exception) { - "" - }, - channel.lpa.profiles.enabled?.displayName, - channel.intrinsicChannelName, - ) + val slots = euiccChannelManager.flowAllOpenEuiccPorts().flatMapConcat { (slotId, portId) -> + val ses = euiccChannelManager.flowEuiccSecureElements(slotId, portId).toList() + ses.asFlow().map { seId -> + euiccChannelManager.withEuiccChannel(slotId, portId, seId) { channel -> + SlotInfo( + channel.logicalSlotId, + channel.port.card.isRemovable, + channel.port.card.ports.size > 1, + ses.size > 1, + channel.portId, + channel.seId, + channel.lpa.eID, + channel.lpa.euiccInfo2?.freeNvram ?: 0, + try { + telephonyManager.getImei(channel.logicalSlotId) ?: "" + } catch (e: Exception) { + "" + }, + channel.lpa.profiles.enabled?.displayName, + channel.intrinsicChannelName, + ) + } } - }.toList().sortedBy { it.logicalSlotId } + }.toList().sortedBy { it.syntheticSlotId } adapter.slots = slots // Ensure we always have a selected slot by default - val selectedIdx = slots.indexOfFirst { it.logicalSlotId == state.selectedLogicalSlot } + val selectedIdx = slots.indexOfFirst { it.syntheticSlotId == state.selectedSyntheticSlotId } adapter.currentSelectedIdx = if (selectedIdx > 0) { selectedIdx } else { if (slots.isNotEmpty()) { - state.selectedLogicalSlot = slots[0].logicalSlotId + state.selectedSyntheticSlotId = slots[0].syntheticSlotId } 0 } @@ -167,7 +190,8 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt adapter.notifyItemChanged(lastIdx) adapter.notifyItemChanged(curIdx) // Selected index isn't logical slot ID directly, needs a conversion - state.selectedLogicalSlot = adapter.slots[adapter.currentSelectedIdx].logicalSlotId + state.selectedSyntheticSlotId = + adapter.slots[adapter.currentSelectedIdx].syntheticSlotId state.imei = adapter.slots[adapter.currentSelectedIdx].imei } @@ -187,11 +211,17 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt title.text = if (item.logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { item.intrinsicChannelName ?: root.context.getString(R.string.channel_type_usb) + } else if (item.hasMultipleSEs) { + appContainer.customizableTextProvider.formatNonUsbChannelNameWithSeId( + item.logicalSlotId, + item.seId + ) } else { - appContainer.customizableTextProvider.formatInternalChannelName(item.logicalSlotId) + appContainer.customizableTextProvider.formatNonUsbChannelName(item.logicalSlotId) } eID.text = item.eID - activeProfile.text = item.enabledProfileName ?: root.context.getString(R.string.profile_no_enabled_profile) + activeProfile.text = item.enabledProfileName + ?: root.context.getString(R.string.profile_no_enabled_profile) freeSpace.text = formatFreeSpace(item.freeSpace) checkBox.isChecked = adapter.currentSelectedIdx == idx } @@ -205,7 +235,8 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt get() = slots[currentSelectedIdx] override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SlotItemHolder { - val root = LayoutInflater.from(parent.context).inflate(R.layout.download_slot_item, parent, false) + val root = LayoutInflater.from(parent.context) + .inflate(R.layout.download_slot_item, parent, false) return SlotItemHolder(root) } 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..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 @@ -17,12 +19,19 @@ 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, + seId: EuiccChannel.SecureElementId, + addArguments: BundleSetter = {} +): T where T : Fragment, T : EuiccChannelFragmentMarker = clazz.getDeclaredConstructor().newInstance().apply { arguments = Bundle() arguments!!.putInt(FIELD_SLOT_ID, slotId) arguments!!.putInt(FIELD_PORT_ID, portId) + arguments!!.putParcelable(FIELD_SE_ID, seId) arguments!!.addArguments() } @@ -35,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 @@ -54,7 +75,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, + seId, + fn + ) } suspend fun T.ensureEuiccChannelManager() where T : Fragment, T : OpenEuiccContextMarker = diff --git a/app-common/src/main/java/im/angry/openeuicc/util/LPAUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/LPAUtils.kt index 9f95412..c4e1ba1 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/LPAUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/LPAUtils.kt @@ -79,9 +79,10 @@ fun LocalProfileAssistant.disableActiveProfileKeepIccId(refresh: Boolean): Strin suspend inline fun EuiccChannelManager.beginTrackedOperation( slotId: Int, portId: Int, + seId: EuiccChannel.SecureElementId, op: () -> Boolean ) { - val latestSeq = withEuiccChannel(slotId, portId) { channel -> + val latestSeq = withEuiccChannel(slotId, portId, seId) { channel -> channel.lpa.notifications.firstOrNull()?.seqNumber ?: 0 } @@ -91,7 +92,7 @@ suspend inline fun EuiccChannelManager.beginTrackedOperation( try { // Note that the exact instance of "channel" might have changed here if reconnected; // this is why we need to use two distinct calls to withEuiccChannel() - withEuiccChannel(slotId, portId) { channel -> + withEuiccChannel(slotId, portId, seId) { channel -> channel.lpa.notifications.filter { it.seqNumber > latestSeq }.forEach { Log.d(TAG, "Handling notification $it") channel.lpa.handleNotification(it.seqNumber) diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index 39a762f..36a046c 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -9,6 +9,7 @@ Unknown Logical Slot %d + Logical Slot %d, SE %d USB OpenMobile API (OMAPI) diff --git a/app-unpriv/src/main/java/im/angry/openeuicc/di/UnprivilegedCustomizableTextProvider.kt b/app-unpriv/src/main/java/im/angry/openeuicc/di/UnprivilegedCustomizableTextProvider.kt index 929ce84..6310d65 100644 --- a/app-unpriv/src/main/java/im/angry/openeuicc/di/UnprivilegedCustomizableTextProvider.kt +++ b/app-unpriv/src/main/java/im/angry/openeuicc/di/UnprivilegedCustomizableTextProvider.kt @@ -2,9 +2,16 @@ package im.angry.openeuicc.di import android.content.Context import im.angry.easyeuicc.R +import im.angry.openeuicc.core.EuiccChannel class UnprivilegedCustomizableTextProvider(private val context: Context) : DefaultCustomizableTextProvider(context) { - override fun formatInternalChannelName(logicalSlotId: Int): String = + override fun formatNonUsbChannelName(logicalSlotId: Int): String = context.getString(R.string.channel_name_format_unpriv, logicalSlotId) + + override fun formatNonUsbChannelNameWithSeId( + logicalSlotId: Int, + seId: EuiccChannel.SecureElementId + ): String = + context.getString(R.string.channel_name_format_unpriv_se, logicalSlotId, seId.id) } \ No newline at end of file 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 3eb09c0..c20a3b9 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.QuickCompatibilityFragment import im.angry.openeuicc.ui.UnprivilegedEuiccManagementFragment @@ -8,8 +9,12 @@ import im.angry.openeuicc.ui.UnprivilegedNoEuiccPlaceholderFragment import im.angry.openeuicc.ui.UnprivilegedSettingsFragment open 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/QuickCompatibilityFragment.kt b/app-unpriv/src/main/java/im/angry/openeuicc/ui/QuickCompatibilityFragment.kt index 9b41730..03647bb 100644 --- a/app-unpriv/src/main/java/im/angry/openeuicc/ui/QuickCompatibilityFragment.kt +++ b/app-unpriv/src/main/java/im/angry/openeuicc/ui/QuickCompatibilityFragment.kt @@ -148,7 +148,7 @@ open class QuickCompatibilityFragment : Fragment(), UnprivilegedEuiccContextMark if (omapiSlots.isEmpty()) { return CompatibilityResult(Compatibility.NOT_COMPATIBLE) } - val formatChannelName = appContainer.customizableTextProvider::formatInternalChannelName + val formatChannelName = appContainer.customizableTextProvider::formatNonUsbChannelName return CompatibilityResult( Compatibility.COMPATIBLE, slotsOmapi = omapiSlots.map(formatChannelName), 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 { diff --git a/app-unpriv/src/main/res/values/strings.xml b/app-unpriv/src/main/res/values/strings.xml index 76d1c76..eafa6c0 100644 --- a/app-unpriv/src/main/res/values/strings.xml +++ b/app-unpriv/src/main/res/values/strings.xml @@ -1,6 +1,7 @@ EasyEUICC SIM %d + SIM %d, SE %d Compatibility Check Open SIM Toolkit 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 876387f..ec4ba22 100644 --- a/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt +++ b/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt @@ -15,13 +15,14 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto @Suppress("NAME_SHADOWING") override suspend fun tryOpenEuiccChannel( port: UiccPortInfoCompat, - isdrAid: ByteArray + isdrAid: ByteArray, + seId: EuiccChannel.SecureElementId, ): 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()) { @@ -40,6 +41,7 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto context.preferenceRepository.verboseLoggingFlow ), isdrAid, + seId, context.preferenceRepository.verboseLoggingFlow, context.preferenceRepository.ignoreTLSCertificateFlow, context.preferenceRepository.es10xMssFlow, @@ -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 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/service/OpenEuiccService.kt b/app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt index 02b3baf..d5d1e39 100644 --- a/app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt +++ b/app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt @@ -8,6 +8,7 @@ import android.telephony.UiccSlotMapping import android.telephony.euicc.DownloadableSubscription import android.telephony.euicc.EuiccInfo import android.util.Log +import im.angry.openeuicc.core.EuiccChannel import net.typeblog.lpac_jni.LocalProfileInfo import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone @@ -165,69 +166,70 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker { return GetDefaultDownloadableSubscriptionListResult(RESULT_OK, arrayOf()) } - override fun onGetEuiccProfileInfoList(slotId: Int): GetEuiccProfileInfoListResult = withEuiccChannelManager { - Log.i(TAG, "onGetEuiccProfileInfoList slotId=$slotId") - if (slotId == -1 || shouldIgnoreSlot(slotId)) { - Log.i(TAG, "ignoring slot $slotId") - return@withEuiccChannelManager GetEuiccProfileInfoListResult( - RESULT_FIRST_USER, - arrayOf(), - true - ) - } - - // TODO: Temporarily enable the slot to access its profiles if it is currently unmapped - val port = euiccChannelManager.findFirstAvailablePort(slotId) - if (port == -1) { - return@withEuiccChannelManager GetEuiccProfileInfoListResult( - RESULT_FIRST_USER, - arrayOf(), - true - ) - } - - return@withEuiccChannelManager try { - euiccChannelManager.withEuiccChannel(slotId, port) { channel -> - val filteredProfiles = - if (preferenceRepository.unfilteredProfileListFlow.first()) - channel.lpa.profiles - else - channel.lpa.profiles.operational - val profiles = filteredProfiles.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() - } - - GetEuiccProfileInfoListResult( - RESULT_OK, - profiles.toTypedArray(), - channel.port.card.isRemovable + override fun onGetEuiccProfileInfoList(slotId: Int): GetEuiccProfileInfoListResult = + withEuiccChannelManager { + Log.i(TAG, "onGetEuiccProfileInfoList slotId=$slotId") + if (slotId == -1 || shouldIgnoreSlot(slotId)) { + Log.i(TAG, "ignoring slot $slotId") + return@withEuiccChannelManager GetEuiccProfileInfoListResult( + RESULT_FIRST_USER, + arrayOf(), + true + ) + } + + // TODO: Temporarily enable the slot to access its profiles if it is currently unmapped + val port = euiccChannelManager.findFirstAvailablePort(slotId) + if (port == -1) { + return@withEuiccChannelManager GetEuiccProfileInfoListResult( + RESULT_FIRST_USER, + arrayOf(), + true + ) + } + + return@withEuiccChannelManager try { + euiccChannelManager.withEuiccChannel(slotId, port) { channel -> + val filteredProfiles = + if (preferenceRepository.unfilteredProfileListFlow.first()) + channel.lpa.profiles + else + channel.lpa.profiles.operational + val profiles = filteredProfiles.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() + } + + GetEuiccProfileInfoListResult( + RESULT_OK, + profiles.toTypedArray(), + channel.port.card.isRemovable + ) + } + } catch (e: EuiccChannelManager.EuiccChannelNotFoundException) { + GetEuiccProfileInfoListResult( + RESULT_FIRST_USER, + arrayOf(), + true ) } - } catch (e: EuiccChannelManager.EuiccChannelNotFoundException) { - GetEuiccProfileInfoListResult( - RESULT_FIRST_USER, - arrayOf(), - true - ) } - } override fun onGetEuiccInfo(slotId: Int): EuiccInfo { return EuiccInfo("Unknown") // TODO: Can we actually implement this? @@ -250,7 +252,12 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker { if (enabledAnywhere) return@withEuiccChannelManager RESULT_FIRST_USER euiccChannelManagerService.waitForForegroundTask() - val success = euiccChannelManagerService.launchProfileDeleteTask(slotId, ports[0], iccid) + val success = euiccChannelManagerService.launchProfileDeleteTask( + slotId, + ports[0], + EuiccChannel.SecureElementId.DEFAULT, + iccid + ) .waitDone() == null return@withEuiccChannelManager if (success) { @@ -275,7 +282,10 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker { iccid: String?, forceDeactivateSim: Boolean ): Int = withEuiccChannelManager { - Log.i(TAG,"onSwitchToSubscriptionWithPort slotId=$slotId portIndex=$portIndex iccid=$iccid forceDeactivateSim=$forceDeactivateSim") + Log.i( + TAG, + "onSwitchToSubscriptionWithPort slotId=$slotId portIndex=$portIndex iccid=$iccid forceDeactivateSim=$forceDeactivateSim" + ) if (shouldIgnoreSlot(slotId)) return@withEuiccChannelManager RESULT_FIRST_USER try { @@ -357,6 +367,7 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker { val res = euiccChannelManagerService.launchProfileSwitchTask( foundSlotId, foundPortId, + EuiccChannel.SecureElementId.DEFAULT, foundIccid, enable, 30 * 1000 @@ -386,7 +397,13 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker { euiccChannelManagerService.waitForForegroundTask() val success = - (euiccChannelManagerService.launchProfileRenameTask(slotId, port, iccid, nickname!!) + (euiccChannelManagerService.launchProfileRenameTask( + slotId, + port, + EuiccChannel.SecureElementId.DEFAULT, + iccid, + nickname!! + ) .waitDone()) == null euiccChannelManager.withEuiccChannel(slotId, port) { channel -> 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