ソースを参照

feat: multi-SE support for USB channels (#309)

All channels now support multi-SE. UsbCcidReaderFragment now no longer wraps inner EuiccManagementFragment's but instead calls MainActivity to instantiate tabs for USB channels. Also, we now no longer use the USB product name as channel titles but instead use "USB Reader" and "USB Reader, SE x" for all of them.

Reviewed-on: https://gitea.angry.im/PeterCxy/OpenEUICC/pulls/309
Reviewed-by: septs <github@septs.pw>
Peter Cai 4 週間 前
コミット
713ffec26d

+ 34 - 0
.idea/codeStyles/Project.xml

@@ -1,5 +1,39 @@
 <component name="ProjectCodeStyleConfiguration">
 <component name="ProjectCodeStyleConfiguration">
   <code_scheme name="Project" version="173">
   <code_scheme name="Project" version="173">
+    <JavaCodeStyleSettings>
+      <option name="IMPORT_LAYOUT_TABLE">
+        <value>
+          <package name="android" withSubpackages="true" static="true" />
+          <package name="androidx" withSubpackages="true" static="true" />
+          <package name="com" withSubpackages="true" static="true" />
+          <package name="junit" withSubpackages="true" static="true" />
+          <package name="net" withSubpackages="true" static="true" />
+          <package name="org" withSubpackages="true" static="true" />
+          <package name="java" withSubpackages="true" static="true" />
+          <package name="javax" withSubpackages="true" static="true" />
+          <package name="" withSubpackages="true" static="true" />
+          <emptyLine />
+          <package name="android" withSubpackages="true" static="false" />
+          <emptyLine />
+          <package name="androidx" withSubpackages="true" static="false" />
+          <emptyLine />
+          <package name="com" withSubpackages="true" static="false" />
+          <emptyLine />
+          <package name="junit" withSubpackages="true" static="false" />
+          <emptyLine />
+          <package name="net" withSubpackages="true" static="false" />
+          <emptyLine />
+          <package name="org" withSubpackages="true" static="false" />
+          <emptyLine />
+          <package name="java" withSubpackages="true" static="false" />
+          <emptyLine />
+          <package name="javax" withSubpackages="true" static="false" />
+          <emptyLine />
+          <package name="" withSubpackages="true" static="false" />
+          <emptyLine />
+        </value>
+      </option>
+    </JavaCodeStyleSettings>
     <JetCodeStyleSettings>
     <JetCodeStyleSettings>
       <option name="PACKAGES_TO_USE_STAR_IMPORTS">
       <option name="PACKAGES_TO_USE_STAR_IMPORTS">
         <value>
         <value>

+ 0 - 2
app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt

@@ -38,7 +38,6 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
         EuiccChannelImpl(
         EuiccChannelImpl(
             context.getString(R.string.channel_type_omapi),
             context.getString(R.string.channel_type_omapi),
             port,
             port,
-            intrinsicChannelName = null,
             OmapiApduInterface(
             OmapiApduInterface(
                 seService!!,
                 seService!!,
                 port,
                 port,
@@ -67,7 +66,6 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
         EuiccChannelImpl(
         EuiccChannelImpl(
             context.getString(R.string.channel_type_usb),
             context.getString(R.string.channel_type_usb),
             FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)),
             FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)),
-            intrinsicChannelName = ccidCtx.productName,
             UsbApduInterface(
             UsbApduInterface(
                 ccidCtx
                 ccidCtx
             ),
             ),

+ 6 - 9
app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt

@@ -52,7 +52,6 @@ open class DefaultEuiccChannelManager(
         get() = (0..<tm.activeModemCountCompat).map { FakeUiccCardInfoCompat(it) }
         get() = (0..<tm.activeModemCountCompat).map { FakeUiccCardInfoCompat(it) }
 
 
     private suspend inline fun tryOpenChannelWithKnownAids(
     private suspend inline fun tryOpenChannelWithKnownAids(
-        supportsMultiSE: Boolean,
         openFn: (ByteArray, EuiccChannel.SecureElementId) -> EuiccChannel?
         openFn: (ByteArray, EuiccChannel.SecureElementId) -> EuiccChannel?
     ): List<EuiccChannel> {
     ): List<EuiccChannel> {
         var isdrAidList =
         var isdrAidList =
@@ -100,10 +99,9 @@ open class DefaultEuiccChannelManager(
                     ret.add(channel)
                     ret.add(channel)
                     openedAids.add(aid)
                     openedAids.add(aid)
 
 
-                    // Don't try opening more than 1 channel unless we support multi SE or
-                    // there is a vendor implementation for deciding when we should stop
-                    // opening more channels
-                    if (!supportsMultiSE || vendorDecider == null) {
+                    // Don't try opening more than 1 channel unless there is a vendor
+                    // implementation for deciding when we should stop opening more channels
+                    if (vendorDecider == null) {
                         break@outer
                         break@outer
                     }
                     }
                 }
                 }
@@ -149,9 +147,9 @@ open class DefaultEuiccChannelManager(
                 return null
                 return null
             }
             }
 
 
-            // This function is not responsible for managing USB channels (see the initial check), so supportsMultiSE is true.
+            // This function is not responsible for managing USB channels (see the initial check)
             val channels =
             val channels =
-                tryOpenChannelWithKnownAids(supportsMultiSE = true) { isdrAid, seId ->
+                tryOpenChannelWithKnownAids { isdrAid, seId ->
                     euiccChannelFactory.tryOpenEuiccChannel(
                     euiccChannelFactory.tryOpenEuiccChannel(
                         port,
                         port,
                         isdrAid,
                         isdrAid,
@@ -379,8 +377,7 @@ open class DefaultEuiccChannelManager(
                     UsbCcidContext.createFromUsbDevice(context, device, iface) ?: return@forEach
                     UsbCcidContext.createFromUsbDevice(context, device, iface) ?: return@forEach
 
 
                 try {
                 try {
-                    // TODO: We should also support multiple SEs over USB readers (the code here already does, UI doesn't yet)
-                    val channels = tryOpenChannelWithKnownAids(supportsMultiSE = false) { isdrAid, seId ->
+                    val channels = tryOpenChannelWithKnownAids { isdrAid, seId ->
                         euiccChannelFactory.tryOpenUsbEuiccChannel(ccidCtx, isdrAid, seId)
                         euiccChannelFactory.tryOpenUsbEuiccChannel(ccidCtx, isdrAid, seId)
                     }
                     }
                     if (channels.isNotEmpty() && channels[0].valid) {
                     if (channels.isNotEmpty() && channels[0].valid) {

+ 0 - 7
app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt

@@ -85,13 +85,6 @@ interface EuiccChannel {
      */
      */
     val atr: ByteArray?
     val atr: ByteArray?
 
 
-    /**
-     * Intrinsic name of this channel. For device-internal SIM slots,
-     * this should be null; for USB readers, this should be the name of
-     * the reader device.
-     */
-    val intrinsicChannelName: String?
-
     /**
     /**
      * The underlying APDU interface for this channel
      * The underlying APDU interface for this channel
      */
      */

+ 0 - 1
app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt

@@ -12,7 +12,6 @@ import net.typeblog.lpac_jni.impl.LocalProfileAssistantImpl
 class EuiccChannelImpl(
 class EuiccChannelImpl(
     override val type: String,
     override val type: String,
     override val port: UiccPortInfoCompat,
     override val port: UiccPortInfoCompat,
-    override val intrinsicChannelName: String?,
     override val apduInterface: ApduInterface,
     override val apduInterface: ApduInterface,
     override val isdrAid: ByteArray,
     override val isdrAid: ByteArray,
     override val seId: EuiccChannel.SecureElementId,
     override val seId: EuiccChannel.SecureElementId,

+ 0 - 2
app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt

@@ -34,8 +34,6 @@ class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel {
     override val lpa: LocalProfileAssistant by lpaDelegate
     override val lpa: LocalProfileAssistant by lpaDelegate
     override val valid: Boolean
     override val valid: Boolean
         get() = channel.valid
         get() = channel.valid
-    override val intrinsicChannelName: String?
-        get() = channel.intrinsicChannelName
     override val apduInterface: ApduInterface
     override val apduInterface: ApduInterface
         get() = channel.apduInterface
         get() = channel.apduInterface
     override val atr: ByteArray?
     override val atr: ByteArray?

+ 0 - 2
app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidContext.kt

@@ -20,7 +20,6 @@ class UsbCcidContext private constructor(
     private val conn: UsbDeviceConnection,
     private val conn: UsbDeviceConnection,
     private val bulkIn: UsbEndpoint,
     private val bulkIn: UsbEndpoint,
     private val bulkOut: UsbEndpoint,
     private val bulkOut: UsbEndpoint,
-    val productName: String,
     val verboseLoggingFlow: Flow<Boolean>
     val verboseLoggingFlow: Flow<Boolean>
 ) {
 ) {
     companion object {
     companion object {
@@ -38,7 +37,6 @@ class UsbCcidContext private constructor(
                 conn,
                 conn,
                 bulkIn,
                 bulkIn,
                 bulkOut,
                 bulkOut,
-                usbDevice.productName ?: "USB",
                 context.preferenceRepository.verboseLoggingFlow
                 context.preferenceRepository.verboseLoggingFlow
             )
             )
         }.getOrNull()
         }.getOrNull()

+ 7 - 7
app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt

@@ -78,7 +78,7 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
 
 
         setChannelTitle(
         setChannelTitle(
             if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID)
             if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID)
-                getString(R.string.channel_type_usb) else
+                getString(R.string.channel_name_format_usb) else
                 appContainer.customizableTextProvider.formatNonUsbChannelName(logicalSlotId)
                 appContainer.customizableTextProvider.formatNonUsbChannelName(logicalSlotId)
         )
         )
 
 
@@ -114,13 +114,13 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
 
 
         lifecycleScope.launch {
         lifecycleScope.launch {
             euiccChannelManager.withEuiccChannel(logicalSlotId, seId) { channel ->
             euiccChannelManager.withEuiccChannel(logicalSlotId, seId) { channel ->
-                // When the chip multi-SE, we need to include seId in the title (because we don't have access
-                // to hasMultipleSE in the onCreate() function, we need to do it here).
-                // TODO: Move channel formatting to somewhere centralized and remove this hack. (And also, of course, add support for USB)
-                if (channel.hasMultipleSE && logicalSlotId != EuiccChannelManager.USB_CHANNEL_ID) {
+                if (channel.hasMultipleSE) {
                     withContext(Dispatchers.Main) {
                     withContext(Dispatchers.Main) {
-                        val title = appContainer.customizableTextProvider
-                            .formatNonUsbChannelNameWithSeId(logicalSlotId, seId)
+                        val title = if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
+                            getString(R.string.channel_name_format_usb_se, seId.id)
+                        } else {
+                            appContainer.customizableTextProvider.formatNonUsbChannelNameWithSeId(logicalSlotId, seId)
+                        }
                         setChannelTitle(title)
                         setChannelTitle(title)
                     }
                     }
                 }
                 }

+ 48 - 5
app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt

@@ -26,6 +26,7 @@ import androidx.viewpager2.widget.ViewPager2
 import com.google.android.material.tabs.TabLayout
 import com.google.android.material.tabs.TabLayout
 import com.google.android.material.tabs.TabLayoutMediator
 import com.google.android.material.tabs.TabLayoutMediator
 import im.angry.openeuicc.common.R
 import im.angry.openeuicc.common.R
+import im.angry.openeuicc.core.EuiccChannel
 import im.angry.openeuicc.core.EuiccChannelManager
 import im.angry.openeuicc.core.EuiccChannelManager
 import im.angry.openeuicc.ui.wizard.DownloadWizardActivity
 import im.angry.openeuicc.ui.wizard.DownloadWizardActivity
 import im.angry.openeuicc.util.*
 import im.angry.openeuicc.util.*
@@ -51,18 +52,30 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
     private var refreshing = false
     private var refreshing = false
 
 
     private data class Page(
     private data class Page(
+        val id: Long,
         val logicalSlotId: Int,
         val logicalSlotId: Int,
         val title: String,
         val title: String,
         val createFragment: () -> Fragment
         val createFragment: () -> Fragment
     )
     )
 
 
     private val pages: MutableList<Page> = mutableListOf()
     private val pages: MutableList<Page> = mutableListOf()
+    private var nextPageId = 0L
+
+    private fun newPage(
+        logicalSlotId: Int,
+        title: String,
+        createFragment: () -> Fragment
+    ): Page = Page(nextPageId++, logicalSlotId, title, createFragment)
 
 
     private val pagerAdapter by lazy {
     private val pagerAdapter by lazy {
         object : FragmentStateAdapter(this) {
         object : FragmentStateAdapter(this) {
             override fun getItemCount() = pages.size
             override fun getItemCount() = pages.size
 
 
             override fun createFragment(position: Int): Fragment = pages[position].createFragment()
             override fun createFragment(position: Int): Fragment = pages[position].createFragment()
+
+            override fun getItemId(position: Int): Long = pages[position].id
+
+            override fun containsItem(itemId: Long): Boolean = pages.any { it.id == itemId }
         }
         }
     }
     }
 
 
@@ -178,7 +191,7 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
                     } else {
                     } else {
                         appContainer.customizableTextProvider.formatNonUsbChannelName(channel.logicalSlotId)
                         appContainer.customizableTextProvider.formatNonUsbChannelName(channel.logicalSlotId)
                     }
                     }
-                    newPages.add(Page(channel.logicalSlotId, channelName) {
+                    newPages.add(newPage(channel.logicalSlotId, channelName) {
                         appContainer.uiComponentFactory.createEuiccManagementFragment(
                         appContainer.uiComponentFactory.createEuiccManagementFragment(
                             slotId,
                             slotId,
                             portId,
                             portId,
@@ -192,9 +205,8 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
         // If USB readers exist, add them at the very last
         // If USB readers exist, add them at the very last
         // We use a wrapper fragment to handle logic specific to USB readers
         // We use a wrapper fragment to handle logic specific to USB readers
         usbDevice?.let {
         usbDevice?.let {
-            val productName = it.productName ?: getString(R.string.channel_type_usb)
-            newPages.add(Page(EuiccChannelManager.USB_CHANNEL_ID, productName) {
-                UsbCcidReaderFragment()
+            newPages.add(newPage(EuiccChannelManager.USB_CHANNEL_ID, getString(R.string.channel_name_format_usb)) {
+                UsbCcidReaderPermissionFragment()
             })
             })
         }
         }
         viewPager.visibility = View.VISIBLE
         viewPager.visibility = View.VISIBLE
@@ -202,7 +214,7 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
         if (newPages.size > 1) {
         if (newPages.size > 1) {
             tabs.visibility = View.VISIBLE
             tabs.visibility = View.VISIBLE
         } else if (newPages.isEmpty()) {
         } else if (newPages.isEmpty()) {
-            newPages.add(Page(-1, "") {
+            newPages.add(newPage(-1, "") {
                 appContainer.uiComponentFactory.createNoEuiccPlaceholderFragment()
                 appContainer.uiComponentFactory.createNoEuiccPlaceholderFragment()
             })
             })
         }
         }
@@ -260,4 +272,35 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
             .build()
             .build()
         return listOf(downloadShortcut)
         return listOf(downloadShortcut)
     }
     }
+
+    fun instantiateUsbTabs(seIds: List<EuiccChannel.SecureElementId>) {
+        val existingUsbPageIndex = pages.indexOfFirst { it.logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID }
+        if (existingUsbPageIndex == -1) return
+
+        val usbPages =
+            seIds.map { seId ->
+                val name = if (seIds.size == 1) {
+                    getString(R.string.channel_name_format_usb)
+                } else {
+                    getString(R.string.channel_name_format_usb_se, seId.id)
+                }
+                newPage(EuiccChannelManager.USB_CHANNEL_ID, name) {
+                    appContainer.uiComponentFactory.createEuiccManagementFragment(
+                        EuiccChannelManager.USB_CHANNEL_ID,
+                        0,
+                        seId
+                    )
+                }
+            }
+
+        // Add before removing to avoid out-of-bounds problems
+        pages.addAll(existingUsbPageIndex, usbPages)
+        // Remove the old USB reader page
+        pages.removeAt(existingUsbPageIndex + usbPages.size)
+
+        if (pages.size > 1) {
+            tabs.visibility = View.VISIBLE
+        }
+        pagerAdapter.notifyDataSetChanged()
+    }
 }
 }

+ 7 - 6
app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt

@@ -72,11 +72,9 @@ class NotificationsActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker
             intent.getParcelableExtra("seId")
             intent.getParcelableExtra("seId")
         } ?: EuiccChannel.SecureElementId.DEFAULT
         } ?: 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)
         setChannelTitle(
         setChannelTitle(
             if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID)
             if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID)
-                getString(R.string.channel_type_usb) else
+                getString(R.string.channel_name_format_usb) else
                 appContainer.customizableTextProvider.formatNonUsbChannelName(logicalSlotId)
                 appContainer.customizableTextProvider.formatNonUsbChannelName(logicalSlotId)
         )
         )
 
 
@@ -135,10 +133,13 @@ class NotificationsActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker
     private fun refresh() {
     private fun refresh() {
         launchTask {
         launchTask {
             notificationAdapter.notifications = withEuiccChannel { channel ->
             notificationAdapter.notifications = withEuiccChannel { channel ->
-                if (channel.hasMultipleSE && logicalSlotId != EuiccChannelManager.USB_CHANNEL_ID) {
+                if (channel.hasMultipleSE) {
                     withContext(Dispatchers.Main) {
                     withContext(Dispatchers.Main) {
-                        val channelTitle = appContainer.customizableTextProvider
-                            .formatNonUsbChannelNameWithSeId(logicalSlotId, seId)
+                        val channelTitle = if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
+                            getString(R.string.channel_name_format_usb_se, seId.id)
+                        } else {
+                            appContainer.customizableTextProvider.formatNonUsbChannelNameWithSeId(logicalSlotId, seId)
+                        }
                         setChannelTitle(channelTitle)
                         setChannelTitle(channelTitle)
                     }
                     }
                 }
                 }

+ 12 - 21
app-common/src/main/java/im/angry/openeuicc/ui/UsbCcidReaderFragment.kt → app-common/src/main/java/im/angry/openeuicc/ui/UsbCcidReaderPermissionFragment.kt

@@ -14,24 +14,22 @@ import android.view.LayoutInflater
 import android.view.View
 import android.view.View
 import android.view.ViewGroup
 import android.view.ViewGroup
 import android.widget.Button
 import android.widget.Button
-import android.widget.ProgressBar
 import android.widget.TextView
 import android.widget.TextView
 import androidx.fragment.app.Fragment
 import androidx.fragment.app.Fragment
-import androidx.fragment.app.commit
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.lifecycleScope
 import im.angry.openeuicc.common.R
 import im.angry.openeuicc.common.R
-import im.angry.openeuicc.core.EuiccChannel
 import im.angry.openeuicc.core.EuiccChannelManager
 import im.angry.openeuicc.core.EuiccChannelManager
 import im.angry.openeuicc.util.*
 import im.angry.openeuicc.util.*
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.toList
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import kotlinx.coroutines.withContext
 
 
 /**
 /**
- * A wrapper fragment over EuiccManagementFragment where we handle
- * logic specific to USB devices. This is mainly USB permission
- * requests, and the fact that USB devices may or may not be
- * available by the time the user selects it from MainActivity.
+ * A fragment to handle USB reader-specific permission flow. If/after
+ * permission is granted, this fragment simply calls back to MainActivity
+ * to instantiate the corresponding EuiccManagementFragment(s) for the USB
+ * reader.
  *
  *
  * Having this fragment allows MainActivity to be (mostly) agnostic
  * Having this fragment allows MainActivity to be (mostly) agnostic
  * of the underlying implementation of different types of channels.
  * of the underlying implementation of different types of channels.
@@ -41,7 +39,7 @@ import kotlinx.coroutines.withContext
  * Note that for now we assume there will only be one USB card reader
  * Note that for now we assume there will only be one USB card reader
  * device. This is also an implicit assumption in EuiccChannelManager.
  * device. This is also an implicit assumption in EuiccChannelManager.
  */
  */
-class UsbCcidReaderFragment : Fragment(), OpenEuiccContextMarker {
+class UsbCcidReaderPermissionFragment : Fragment(), OpenEuiccContextMarker {
     companion object {
     companion object {
         const val ACTION_USB_PERMISSION = "im.angry.openeuicc.USB_PERMISSION"
         const val ACTION_USB_PERMISSION = "im.angry.openeuicc.USB_PERMISSION"
     }
     }
@@ -70,7 +68,7 @@ class UsbCcidReaderFragment : Fragment(), OpenEuiccContextMarker {
 
 
     private lateinit var text: TextView
     private lateinit var text: TextView
     private lateinit var permissionButton: Button
     private lateinit var permissionButton: Button
-    private lateinit var loadingProgress: ProgressBar
+    private lateinit var loadingProgress: View
 
 
     private var usbDevice: UsbDevice? = null
     private var usbDevice: UsbDevice? = null
 
 
@@ -143,27 +141,20 @@ class UsbCcidReaderFragment : Fragment(), OpenEuiccContextMarker {
             euiccChannelManager.tryOpenUsbEuiccChannel()
             euiccChannelManager.tryOpenUsbEuiccChannel()
         }
         }
 
 
-        loadingProgress.visibility = View.GONE
-
         usbDevice = device
         usbDevice = device
 
 
         if (device != null && !canOpen && !usbManager.hasPermission(device)) {
         if (device != null && !canOpen && !usbManager.hasPermission(device)) {
+            loadingProgress.visibility = View.GONE
             text.text = getString(R.string.usb_permission_needed)
             text.text = getString(R.string.usb_permission_needed)
             text.visibility = View.VISIBLE
             text.visibility = View.VISIBLE
             permissionButton.visibility = View.VISIBLE
             permissionButton.visibility = View.VISIBLE
         } else if (device != null && canOpen) {
         } else if (device != null && canOpen) {
-            childFragmentManager.commit {
-                replace(
-                    R.id.child_container,
-                    appContainer.uiComponentFactory.createEuiccManagementFragment(
-                        slotId = EuiccChannelManager.USB_CHANNEL_ID,
-                        portId = 0,
-                        // TODO: What if a USB card has multiple SEs?
-                        seId = EuiccChannel.SecureElementId.DEFAULT
-                    )
-                )
+            val seIds = withContext(Dispatchers.IO) {
+                euiccChannelManager.flowEuiccSecureElements(EuiccChannelManager.USB_CHANNEL_ID, 0).toList()
             }
             }
+            (requireActivity() as MainActivity).instantiateUsbTabs(seIds)
         } else {
         } else {
+            loadingProgress.visibility = View.GONE
             text.text = getString(R.string.usb_failed)
             text.text = getString(R.string.usb_failed)
             text.visibility = View.VISIBLE
             text.visibility = View.VISIBLE
             permissionButton.visibility = View.GONE
             permissionButton.visibility = View.GONE

+ 5 - 3
app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt

@@ -41,7 +41,6 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
         val freeSpace: Int,
         val freeSpace: Int,
         val imei: String,
         val imei: String,
         val enabledProfileName: String?,
         val enabledProfileName: String?,
-        val intrinsicChannelName: String?,
     ) {
     ) {
         // A synthetic slot ID used to uniquely identify this slot + SE chip in the download process
         // 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...
         // We assume we don't have anywhere near 2^16 ports...
@@ -115,7 +114,6 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
                             ""
                             ""
                         },
                         },
                         channel.lpa.profiles.enabled?.displayName,
                         channel.lpa.profiles.enabled?.displayName,
-                        channel.intrinsicChannelName,
                     )
                     )
                 }
                 }
             }
             }
@@ -188,7 +186,11 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
             }
             }
 
 
             title.text = if (item.logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
             title.text = if (item.logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
-                item.intrinsicChannelName ?: root.context.getString(R.string.channel_type_usb)
+                if (item.hasMultipleSEs) {
+                    root.context.getString(R.string.channel_name_format_usb_se, item.seId.id)
+                } else {
+                    root.context.getString(R.string.channel_name_format_usb)
+                }
             } else if (item.hasMultipleSEs) {
             } else if (item.hasMultipleSEs) {
                 appContainer.customizableTextProvider.formatNonUsbChannelNameWithSeId(
                 appContainer.customizableTextProvider.formatNonUsbChannelNameWithSeId(
                     item.logicalSlotId,
                     item.logicalSlotId,

+ 13 - 14
app-common/src/main/res/layout/fragment_usb_ccid_reader.xml

@@ -4,15 +4,21 @@
     android:layout_width="match_parent"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
     android:layout_height="match_parent">
 
 
-    <ProgressBar
+    <androidx.constraintlayout.widget.ConstraintLayout
         android:id="@+id/loading"
         android:id="@+id/loading"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:indeterminate="true"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintEnd_toEndOf="parent"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="parent" />
+        app:layout_constraintTop_toTopOf="parent">
+        <ProgressBar
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:indeterminate="true"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
+    </androidx.constraintlayout.widget.ConstraintLayout>
 
 
     <TextView
     <TextView
         android:id="@+id/usb_reader_text"
         android:id="@+id/usb_reader_text"
@@ -37,11 +43,4 @@
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toBottomOf="@id/usb_reader_text" />
         app:layout_constraintTop_toBottomOf="@id/usb_reader_text" />
 
 
-    <FrameLayout
-        android:id="@+id/child_container"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="parent" />
-
 </androidx.constraintlayout.widget.ConstraintLayout>
 </androidx.constraintlayout.widget.ConstraintLayout>

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

@@ -195,4 +195,6 @@
     <string name="pref_info_website">公式サイト</string>
     <string name="pref_info_website">公式サイト</string>
     <string name="pref_info_source_code">ソースコード</string>
     <string name="pref_info_source_code">ソースコード</string>
     <string name="channel_name_format_se">論理スロット %1$d, SE %2$d</string>
     <string name="channel_name_format_se">論理スロット %1$d, SE %2$d</string>
+    <string name="channel_name_format_usb">USB リーダー</string>
+    <string name="channel_name_format_usb_se">USB リーダー, SE %d</string>
 </resources>
 </resources>

+ 2 - 0
app-common/src/main/res/values-zh-rCN/strings.xml

@@ -193,4 +193,6 @@
     <string name="download_wizard_error_suggest_contact_carrier">请联系您的运营商寻求帮助。</string>
     <string name="download_wizard_error_suggest_contact_carrier">请联系您的运营商寻求帮助。</string>
     <string name="download_wizard_error_suggest_contact_reissue">请联系您的运营商重新签发此 eSIM 配置文件。</string>
     <string name="download_wizard_error_suggest_contact_reissue">请联系您的运营商重新签发此 eSIM 配置文件。</string>
     <string name="channel_name_format_se">逻辑卡槽 %1$d, SE %2$d</string>
     <string name="channel_name_format_se">逻辑卡槽 %1$d, SE %2$d</string>
+    <string name="channel_name_format_usb">USB 读卡器</string>
+    <string name="channel_name_format_usb_se">USB 读卡器, SE %d</string>
 </resources>
 </resources>

+ 2 - 0
app-common/src/main/res/values-zh-rTW/strings.xml

@@ -189,4 +189,6 @@
     <string name="download_wizard_error_suggest_contact_carrier">請聯絡您的電信業者尋求協助。</string>
     <string name="download_wizard_error_suggest_contact_carrier">請聯絡您的電信業者尋求協助。</string>
     <string name="download_wizard_error_suggest_contact_reissue">請聯絡您的電信業者重新簽發此 eSIM 設定檔。</string>
     <string name="download_wizard_error_suggest_contact_reissue">請聯絡您的電信業者重新簽發此 eSIM 設定檔。</string>
     <string name="channel_name_format_se">虛擬卡槽 %1$d, SE %2$d</string>
     <string name="channel_name_format_se">虛擬卡槽 %1$d, SE %2$d</string>
+    <string name="channel_name_format_usb">USB 讀卡機</string>
+    <string name="channel_name_format_usb_se">USB 讀卡機, SE %d</string>
 </resources>
 </resources>

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

@@ -11,6 +11,8 @@
     <string name="channel_name_format">Logical Slot %d</string>
     <string name="channel_name_format">Logical Slot %d</string>
     <string name="channel_name_format_se">Logical Slot %1$d, SE %2$d</string>
     <string name="channel_name_format_se">Logical Slot %1$d, SE %2$d</string>
     <string name="channel_type_usb" translatable="false">USB</string>
     <string name="channel_type_usb" translatable="false">USB</string>
+    <string name="channel_name_format_usb">USB Reader</string>
+    <string name="channel_name_format_usb_se">USB Reader, SE %d</string>
     <string name="channel_type_omapi" translatable="false">OpenMobile API (OMAPI)</string>
     <string name="channel_type_omapi" translatable="false">OpenMobile API (OMAPI)</string>
 
 
     <!-- Profile -->
     <!-- Profile -->

+ 0 - 1
app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt

@@ -33,7 +33,6 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto
                 return EuiccChannelImpl(
                 return EuiccChannelImpl(
                     context.getString(R.string.channel_type_telephony_manager),
                     context.getString(R.string.channel_type_telephony_manager),
                     port,
                     port,
-                    intrinsicChannelName = null,
                     TelephonyManagerApduInterface(
                     TelephonyManagerApduInterface(
                         port,
                         port,
                         telephonyManager,
                         telephonyManager,