Browse Source

feat: Support for removable eSIM with multiple SEs (#239)

Closes #232

Reviewed-on: https://gitea.angry.im/PeterCxy/OpenEUICC/pulls/239
Peter Cai 2 months ago
parent
commit
c396223a02
38 changed files with 707 additions and 258 deletions
  1. 6 2
      app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt
  2. 111 43
      app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt
  3. 55 0
      app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt
  4. 3 2
      app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelFactory.kt
  5. 1 0
      app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt
  6. 11 1
      app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt
  7. 2 0
      app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt
  8. 10 3
      app-common/src/main/java/im/angry/openeuicc/di/CustomizableTextProvider.kt
  9. 10 2
      app-common/src/main/java/im/angry/openeuicc/di/DefaultCustomizableTextProvider.kt
  10. 7 2
      app-common/src/main/java/im/angry/openeuicc/di/DefaultUiComponentFactory.kt
  11. 7 1
      app-common/src/main/java/im/angry/openeuicc/di/UiComponentFactory.kt
  12. 19 10
      app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt
  13. 18 3
      app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt
  14. 40 9
      app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt
  15. 5 3
      app-common/src/main/java/im/angry/openeuicc/ui/EuiccMemoryResetFragment.kt
  16. 22 14
      app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt
  17. 45 23
      app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt
  18. 4 3
      app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt
  19. 4 3
      app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt
  20. 4 1
      app-common/src/main/java/im/angry/openeuicc/ui/UsbCcidReaderFragment.kt
  21. 18 10
      app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt
  22. 7 1
      app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt
  23. 57 26
      app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt
  24. 28 2
      app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt
  25. 3 2
      app-common/src/main/java/im/angry/openeuicc/util/LPAUtils.kt
  26. 12 6
      app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt
  27. 14 4
      app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt
  28. 63 9
      app-common/src/main/java/im/angry/openeuicc/util/Vendors.kt
  29. 1 0
      app-common/src/main/res/values/strings.xml
  30. 8 1
      app-unpriv/src/main/java/im/angry/openeuicc/di/UnprivilegedCustomizableTextProvider.kt
  31. 7 2
      app-unpriv/src/main/java/im/angry/openeuicc/di/UnprivilegedUiComponentFactory.kt
  32. 1 1
      app-unpriv/src/main/java/im/angry/openeuicc/ui/QuickCompatibilityFragment.kt
  33. 7 2
      app-unpriv/src/main/java/im/angry/openeuicc/ui/UnprivilegedEuiccManagementFragment.kt
  34. 1 0
      app-unpriv/src/main/res/values/strings.xml
  35. 5 3
      app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt
  36. 7 2
      app/src/main/java/im/angry/openeuicc/di/PrivilegedUiComponentFactory.kt
  37. 76 59
      app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt
  38. 8 3
      app/src/main/java/im/angry/openeuicc/ui/PrivilegedEuiccManagementFragment.kt

+ 6 - 2
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,

+ 111 - 43
app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt

@@ -32,7 +32,7 @@ open class DefaultEuiccChannelManager(
 
     private val channelCache = mutableListOf<EuiccChannel>()
 
-    private var usbChannel: EuiccChannel? = null
+    private var usbChannels = mutableListOf<EuiccChannel>()
 
     private val lock = Mutex()
 
@@ -51,37 +51,73 @@ open class DefaultEuiccChannelManager(
     protected open val uiccCards: Collection<UiccCardInfoCompat>
         get() = (0..<tm.activeModemCountCompat).map { FakeUiccCardInfoCompat(it) }
 
-    private suspend inline fun tryOpenChannelFirstValidAid(openFn: (ByteArray) -> EuiccChannel?): EuiccChannel? {
-        val isdrAidList =
+    private suspend inline fun tryOpenChannelWithKnownAids(openFn: (ByteArray, EuiccChannel.SecureElementId) -> EuiccChannel?): List<EuiccChannel> {
+        var isdrAidList =
             parseIsdrAidList(appContainer.preferenceRepository.isdrAidListFlow.first())
+        val ret = mutableListOf<EuiccChannel>()
+        val openedAids = mutableListOf<ByteArray>()
+        var hasReset = false
+        var vendorDecider: VendorAidDecider? = null
+        var seId = 0
+
+        outer@ while (true) {
+            for (aid in isdrAidList) {
+                if (vendorDecider != null && !vendorDecider.shouldOpenMore(openedAids, aid)) {
+                    break@outer
+                }
 
-        return isdrAidList.firstNotNullOfOrNull {
-            Log.i(TAG, "Opening channel, trying ISDR AID ${it.encodeHex()}")
+                val channel =
+                    openFn(aid, EuiccChannel.SecureElementId.createFromInt(seId))?.let { channel ->
+                        if (channel.valid) {
+                            seId += 1
+                            channel
+                        } else {
+                            channel.close()
+                            null
+                        }
+                    }
 
-            openFn(it)?.let { channel ->
-                if (channel.valid) {
-                    channel
-                } else {
-                    channel.close()
-                    null
+                if (!hasReset) {
+                    val res = channel?.queryVendorAidListTransformation(isdrAidList)
+                    if (res != null) {
+                        // Reset the for loop since we needed to replace the AID list due to vendor-specific code
+                        Log.i(TAG, "AID list replaced, resetting open attempt")
+                        isdrAidList = res.first
+                        vendorDecider = res.second
+                        seId = 0
+                        ret.clear()
+                        openedAids.clear()
+                        channel.close()
+                        hasReset = true // Don't let anything reset again
+                        continue@outer
+                    }
+                }
+
+                if (channel != null) {
+                    ret.add(channel)
+                    openedAids.add(aid)
                 }
             }
+
+            // If we get here we should exit, since the inner loop completed without resetting
+            break
         }
+
+        return ret
     }
 
-    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 +132,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 +154,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 +176,7 @@ open class DefaultEuiccChannelManager(
 
     private suspend fun findAllEuiccChannelsByPhysicalSlot(physicalSlotId: Int): List<EuiccChannel>? {
         if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
-            return usbChannel?.let { listOf(it) }
+            return usbChannels.ifEmpty { null }
         }
 
         for (card in uiccCards) {
@@ -142,14 +187,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 +217,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 <R> 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 +241,10 @@ open class DefaultEuiccChannelManager(
 
     override suspend fun <R> 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 +258,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 +275,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 +316,20 @@ open class DefaultEuiccChannelManager(
             }
         })
 
+    override fun flowEuiccSecureElements(
+        slotId: Int,
+        portId: Int
+    ): Flow<EuiccChannel.SecureElementId> = 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<UsbDevice?, Boolean> =
         withContext(Dispatchers.IO) {
             usbManager.deviceList.values.forEach { device ->
@@ -277,15 +343,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 +377,8 @@ open class DefaultEuiccChannelManager(
             channel.close()
         }
 
-        usbChannel?.close()
-        usbChannel = null
+        usbChannels.forEach { it.close() }
+        usbChannels.clear()
         channelCache.clear()
         euiccChannelFactory.cleanup()
     }

+ 55 - 0
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<SecureElementId> {
+                override fun createFromParcel(parcel: Parcel): SecureElementId =
+                    createFromInt(parcel.readInt())
+
+                override fun newArray(size: Int): Array<SecureElementId?> = 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

+ 3 - 2
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?
 
     /**

+ 1 - 0
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<Boolean>,
     ignoreTLSCertificateFlow: Flow<Boolean>,
     es10xMssFlow: Flow<Int>,

+ 11 - 1
app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt

@@ -37,6 +37,14 @@ interface EuiccChannelManager {
      */
     fun flowAllOpenEuiccPorts(): Flow<Pair<Int, Int>>
 
+    /**
+     * 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<EuiccChannel.SecureElementId>
+
     /**
      * 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 <R> 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 <R> withEuiccChannel(
         logicalSlotId: Int,
+        seId: EuiccChannel.SecureElementId = EuiccChannel.SecureElementId.DEFAULT,
         fn: suspend (EuiccChannel) -> R
     ): R
 

+ 2 - 0
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)
     }

+ 10 - 3
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 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 formatInternalChannelName(logicalSlotId: Int): String
+    fun formatNonUsbChannelNameWithSeId(logicalSlotId: Int, seId: EuiccChannel.SecureElementId): String
 }

+ 10 - 2
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)
 }

+ 7 - 2
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()
 

+ 7 - 1
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
 }

+ 19 - 10
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,14 +515,18 @@ 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.withEuiccChannel(slotId, portId) { channel ->
+            euiccChannelManager.beginTrackedOperation(slotId, portId, seId) {
+                euiccChannelManager.withEuiccChannel(slotId, portId, seId) { channel ->
                     channel.lpa.euiccMemoryReset()
                 }
 

+ 18 - 3
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, seId, fn = ::buildEuiccInfoItems)
 
             swipeRefresh.isRefreshing = false
         }
@@ -115,7 +122,15 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
         }
         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)) }
         }
         channel.lpa.euiccInfo2?.let { info ->

+ 40 - 9
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
         }
@@ -237,7 +244,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
 
             val err = euiccChannelManagerService
                 .launchProfileSwitchTask(
-                    slotId, portId, iccid, enable,
+                    slotId, portId, seId, iccid, enable,
                     reconnectTimeoutMillis = 30 * 1000
                 )
                 .waitDone()
@@ -290,7 +297,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) return
         popup.menu.findItem(R.id.enable).isVisible = false
@@ -315,7 +325,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,
@@ -431,20 +441,36 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
                     }
                     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
             }
     }
@@ -456,9 +482,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()
                 }
@@ -469,9 +497,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
             }
 
@@ -482,6 +512,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
                     holder.setEnabledProfile(profiles.enabled)
                     holder.setProfileSequenceNumber(position + 1)
                 }
+
                 is FooterViewHolder -> {
                     holder.attach(footerViews[position - profiles.size])
                 }

+ 5 - 3
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()
 

+ 22 - 14
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)
         }
 
@@ -149,21 +151,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)
+            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.formatNonUsbChannelName(channel.logicalSlotId)
+                    newPages.add(Page(channel.logicalSlotId, channelName) {
+                        appContainer.uiComponentFactory.createEuiccManagementFragment(
+                            slotId,
+                            portId,
+                            seId
+                        )
+                    })
                 }
-                // 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)
-                })
-            }
+            }.collect()
         }.collect()
 
         // If USB readers exist, add them at the very last

+ 45 - 23
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)
-                       }
-                   }
-
-                   channel.lpa.notifications.map {
-                       LocalProfileNotificationWrapper(it, nameMap[it.iccid] ?: "???")
-                   }
-               }
-       }
+        launchTask {
+            notificationAdapter.notifications =
+                euiccChannelManager.withEuiccChannel(logicalSlotId, seId) { channel ->
+                    val nameMap = buildMap {
+                        for (profile in channel.lpa.profiles) {
+                            put(profile.iccid, profile.displayName)
+                        }
+                    }
+
+                    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<NotificationViewHolder>() {
+    inner class NotificationAdapter : RecyclerView.Adapter<NotificationViewHolder>() {
         var notifications: List<LocalProfileNotificationWrapper> = listOf()
             @SuppressLint("NotifyDataSetChanged")
             set(value) {

+ 4 - 3
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)

+ 4 - 3
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 -> {

+ 4 - 1
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
                     )
                 )
             }

+ 18 - 10
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()
                     }

+ 7 - 1
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,

+ 57 - 26
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<Int, EuiccChannel.SecureElementId> =
+            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)
         }
 

+ 28 - 2
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 <T> newInstanceEuicc(clazz: Class<T>, slotId: Int, portId: Int, addArguments: BundleSetter = {}): T
+fun <T> newInstanceEuicc(
+    clazz: Class<T>,
+    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> T.slotId: Int
 val <T> T.portId: Int
         where T : Fragment, T : EuiccChannelFragmentMarker
     get() = requireArguments().getInt(FIELD_PORT_ID)
+val <T> 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> T.isUsb: Boolean
         where T : Fragment, T : EuiccChannelFragmentMarker
     get() = slotId == EuiccChannelManager.USB_CHANNEL_ID
@@ -54,7 +75,12 @@ val <T> T.euiccChannelManagerService: EuiccChannelManagerService
 suspend fun <T, R> 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> T.ensureEuiccChannelManager() where T : Fragment, T : OpenEuiccContextMarker =

+ 3 - 2
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)

+ 12 - 6
app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt

@@ -50,14 +50,9 @@ internal object PreferenceConstants {
         # Refs: <https://euicc-manual.osmocom.org/docs/lpa/applet-id-oem/>
 
         # eUICC standard
+        # Even if this AID is deleted here, it will still be attempted as the last resort.
         $EUICC_DEFAULT_ISDR_AID
 
-        # ESTKme AUX (deprecated, use SE0 instead)
-        A06573746B6D65FFFFFFFF4953442D52
-
-        # ESTKme SE0
-        A06573746B6D65FFFF4953442D522030
-
         # eSIM.me
         A0000005591010000000008900000300
 
@@ -66,6 +61,17 @@ internal object PreferenceConstants {
 
         # Xesim
         A0000005591010FFFFFFFF8900000177
+        
+        # ESTKme SE0
+        # For multi-SE eSTK.me products, this will always be attempted even if removed from the list
+        ${ESTKme.ESTK_SE0_AID.encodeHex()}
+        
+        # ESTKme SE1
+        # For multi-SE eSTK.me products, this will always be attempted even if removed from the list
+        ${ESTKme.ESTK_SE1_AID.encodeHex()}
+        
+        # ESTKme AUX (deprecated, use SE0 instead)
+        A06573746B6D65FFFFFFFF4953442D52
     """.trimIndent()
 }
 

+ 14 - 4
app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt

@@ -31,13 +31,23 @@ fun formatFreeSpace(size: Int): String =
 
 /**
  * Decode a list of potential ISDR AIDs, one per line. Lines starting with '#' are ignored.
- * If none is found, at least EUICC_DEFAULT_ISDR_AID is returned
+ * If none is found, at least EUICC_DEFAULT_ISDR_AID is returned.
+ * If EUICC_DEFAULT_ISDR_AID is not contained in the list, it is always appended as the last
+ * element.
  */
-fun parseIsdrAidList(s: String): List<ByteArray> =
-    s.split('\n')
+fun parseIsdrAidList(s: String): List<ByteArray> {
+    val ret = s.split('\n')
+        .asSequence()
         .map(String::trim)
         .filter { !it.startsWith('#') }
         .map(String::trim)
         .filter(String::isNotEmpty)
         .mapNotNull { runCatching(it::decodeHex).getOrNull() }
-        .ifEmpty { listOf(EUICC_DEFAULT_ISDR_AID.decodeHex()) }
+        .toList()
+
+    return if (!ret.any { it.contentEquals(EUICC_DEFAULT_ISDR_AID.decodeHex()) }) {
+        ret + EUICC_DEFAULT_ISDR_AID.decodeHex()
+    } else {
+        ret
+    }
+}

+ 63 - 9
app-common/src/main/java/im/angry/openeuicc/util/Vendors.kt

@@ -16,19 +16,46 @@ private val EUICC_VENDORS: Array<EuiccVendor> = arrayOf(ESTKme(), SIMLink())
 fun EuiccChannel.tryParseEuiccVendorInfo(): EuiccVendorInfo? =
     EUICC_VENDORS.firstNotNullOfOrNull { it.tryParseEuiccVendorInfo(this) }
 
+fun EuiccChannel.queryVendorAidListTransformation(aidList: List<ByteArray>): Pair<List<ByteArray>, VendorAidDecider>? =
+    EUICC_VENDORS.firstNotNullOfOrNull { it.transformAidListIfNeeded(this, aidList) }
+
+fun interface VendorAidDecider {
+    /**
+     * Given a list of already opened AIDs, should we still attempt to open the next?
+     */
+    fun shouldOpenMore(openedAids: List<ByteArray>, nextAid: ByteArray): Boolean
+}
+
 interface EuiccVendor {
     fun tryParseEuiccVendorInfo(channel: EuiccChannel): EuiccVendorInfo?
+
+    /**
+     * Removable eSIM products from some vendors may prefer a vendor-specific list of AIDs or
+     * a specific ordering. For example, multi-SE products from eSTK.me might prefer us trying
+     * SE0 and SE1 AIDs first instead of the generic GSMA ISD-R AID. This method is intended
+     * to implement these vendor-specific cases.
+     *
+     * This method is called on an already opened `EuiccChannel`. If the method returns a non-null
+     * value, the channel will be closed and the process that attempts to open all channels will
+     * be restarted from the beginning. The method will not be called again for the same chip,
+     * but it should still ensure idempotency when called with an already-transformed input.
+     *
+     * The second return value of this method is used to decide when we should stop attempting more
+     * AIDs from the list.
+     */
+    fun transformAidListIfNeeded(
+        referenceChannel: EuiccChannel,
+        aidList: List<ByteArray>
+    ): Pair<List<ByteArray>, VendorAidDecider>? = null
 }
 
-private class ESTKme : EuiccVendor {
+class ESTKme : EuiccVendor {
     companion object {
         private val PRODUCT_AID = "A06573746B6D65FFFFFFFFFFFF6D6774".decodeHex()
-    }
 
-    private fun checkAtr(channel: EuiccChannel): Boolean =
-        (channel.apduInterface as? ApduInterfaceAtrProvider)
-            ?.atr?.decodeToString()?.contains("estk.me")
-            ?: false
+        val ESTK_SE0_AID = "A06573746B6D65FFFF4953442D522030".decodeHex()
+        val ESTK_SE1_AID = "A06573746B6D65FFFF4953442D522031".decodeHex()
+    }
 
     private fun decodeAsn1String(b: ByteArray): String? {
         if (b.size < 2) return null
@@ -37,8 +64,6 @@ private class ESTKme : EuiccVendor {
     }
 
     override fun tryParseEuiccVendorInfo(channel: EuiccChannel): EuiccVendorInfo? {
-        if (!checkAtr(channel)) return null
-
         val iface = channel.apduInterface
         return try {
             iface.withLogicalChannel(PRODUCT_AID) { transmit ->
@@ -59,9 +84,38 @@ private class ESTKme : EuiccVendor {
             null
         }
     }
+
+    override fun transformAidListIfNeeded(
+        referenceChannel: EuiccChannel,
+        aidList: List<ByteArray>
+    ): Pair<List<ByteArray>, VendorAidDecider>? {
+        try {
+            referenceChannel.apduInterface.withLogicalChannel(PRODUCT_AID) {}
+        } catch (_: Exception) {
+            // Not eSTK!
+            return null
+        }
+
+        // If we get here, this is eSTK, and we need to rearrange aidList such that:
+        //   1. SE0 and SE1 AIDs are _always_ included in the list
+        //   2. SE0 and SE1 AIDs are always sorted at the beginning of the list
+        val expected = listOf(ESTK_SE0_AID, ESTK_SE1_AID, *aidList.filter {
+            !it.contentEquals(ESTK_SE0_AID) && !it.contentEquals(ESTK_SE1_AID)
+        }.toTypedArray())
+
+        return if (expected == aidList) {
+            null
+        } else {
+            Pair(expected, VendorAidDecider { openedAids, nextAid ->
+                // Don't open any more channels if we have reached the GSMA default AID and at least 1
+                // eSTK AID has been opened (note that above we re-sorted them to the top of the list)
+                !(openedAids.isNotEmpty() && nextAid.contentEquals(EUICC_DEFAULT_ISDR_AID.decodeHex()))
+            })
+        }
+    }
 }
 
-private class SIMLink : EuiccVendor {
+class SIMLink : EuiccVendor {
     companion object {
         private val EID_PATTERN = Regex("^89044045(84|21)67274948")
     }

+ 1 - 0
app-common/src/main/res/values/strings.xml

@@ -9,6 +9,7 @@
     <string name="profile_no_enabled_profile">Unknown</string>
 
     <string name="channel_name_format">Logical Slot %d</string>
+    <string name="channel_name_format_se">Logical Slot %d, SE %d</string>
     <string name="channel_type_usb" translatable="false">USB</string>
     <string name="channel_type_omapi" translatable="false">OpenMobile API (OMAPI)</string>
 

+ 8 - 1
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)
 }

+ 7 - 2
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()

+ 1 - 1
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),

+ 7 - 2
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 {

+ 1 - 0
app-unpriv/src/main/res/values/strings.xml

@@ -1,6 +1,7 @@
 <resources>
     <string name="app_name" translatable="false">EasyEUICC</string>
     <string name="channel_name_format_unpriv" translatable="false">SIM %d</string>
+    <string name="channel_name_format_unpriv_se" translatable="false">SIM %d, SE %d</string>
     <string name="compatibility_check">Compatibility Check</string>
     <string name="open_sim_toolkit">Open SIM Toolkit</string>
 

+ 5 - 3
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)
     }
 }

+ 7 - 2
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()

+ 76 - 59
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
-            )
-        }
+    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
-            )
-        }
+            // 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()
-                }
+            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_OK,
-                    profiles.toTypedArray(),
-                    channel.port.card.isRemovable
+                    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 ->

+ 8 - 3
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