diff --git a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt index 550cc22..a1cbe86 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt @@ -11,14 +11,8 @@ import im.angry.openeuicc.util.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.count import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.toList import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -171,6 +165,15 @@ open class DefaultEuiccChannelManager( findEuiccChannelByPort(physicalSlotId, portId) } + override suspend fun findFirstAvailablePort(physicalSlotId: Int): Int = + withContext(Dispatchers.IO) { + if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { + return@withContext 0 + } + + findAllEuiccChannelsByPhysicalSlot(physicalSlotId)?.getOrNull(0)?.portId ?: -1 + } + override suspend fun withEuiccChannel( physicalSlotId: Int, portId: Int, @@ -229,13 +232,6 @@ open class DefaultEuiccChannelManager( } } - override suspend fun enumerateEuiccChannels(): List = - withContext(Dispatchers.IO) { - flowEuiccPorts().mapNotNull { (slotId, portId) -> - findEuiccChannelByPort(slotId, portId) - }.toList() - } - override fun flowEuiccPorts(): Flow> = flow { uiccCards.forEach { info -> info.ports.forEach { port -> @@ -251,20 +247,20 @@ open class DefaultEuiccChannelManager( } }.flowOn(Dispatchers.IO) - override suspend fun enumerateUsbEuiccChannel(): Pair = + override suspend fun tryOpenUsbEuiccChannel(): Pair = withContext(Dispatchers.IO) { usbManager.deviceList.values.forEach { device -> Log.i(TAG, "Scanning USB device ${device.deviceId}:${device.vendorId}") val iface = device.getSmartCardInterface() ?: return@forEach // If we don't have permission, tell UI code that we found a candidate device, but we // need permission to be able to do anything with it - if (!usbManager.hasPermission(device)) return@withContext Pair(device, null) + if (!usbManager.hasPermission(device)) return@withContext Pair(device, false) Log.i(TAG, "Found CCID interface on ${device.deviceId}:${device.vendorId}, and has permission; trying to open channel") try { val channel = euiccChannelFactory.tryOpenUsbEuiccChannel(device, iface) if (channel != null && channel.lpa.valid) { usbChannel = channel - return@withContext Pair(device, channel) + return@withContext Pair(device, true) } } catch (e: Exception) { // Ignored -- skip forward @@ -272,7 +268,7 @@ open class DefaultEuiccChannelManager( } Log.i(TAG, "No valid eUICC channel found on USB device ${device.deviceId}:${device.vendorId}") } - return@withContext Pair(null, null) + return@withContext Pair(null, false) } override fun invalidate() { diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt index 2d55179..9378f96 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt @@ -19,21 +19,26 @@ interface EuiccChannelManager { } /** - * Scan all possible _device internal_ sources for EuiccChannels, return them and have all - * scanned channels cached; these channels will remain open for the entire lifetime of - * this EuiccChannelManager object, unless disconnected externally or invalidate()'d + * Scan all possible _device internal_ sources for EuiccChannels, as a flow, return their physical + * (slotId, portId) and have all scanned channels cached; these channels will remain open + * for the entire lifetime of this EuiccChannelManager object, unless disconnected externally + * or invalidate()'d. + * + * To obtain a temporary reference to a EuiccChannel, use `withEuiccChannel()`. */ - suspend fun enumerateEuiccChannels(): List - fun flowEuiccPorts(): Flow> /** * Scan all possible USB devices for CCID readers that may contain eUICC cards. * If found, try to open it for access, and add it to the internal EuiccChannel cache * as a "port" with id 99. When user interaction is required to obtain permission - * to interact with the device, the second return value (EuiccChannel) will be null. + * to interact with the device, the second return value will be false. + * + * Returns (usbDevice, canOpen). canOpen is false if either (1) no usb reader is found; + * or (2) usb reader is found, but user interaction is required for access; + * or (3) usb reader is found, but we are unable to open ISD-R. */ - suspend fun enumerateUsbEuiccChannel(): Pair + suspend fun tryOpenUsbEuiccChannel(): Pair /** * Wait for a slot + port to reconnect (i.e. become valid again) @@ -67,6 +72,12 @@ interface EuiccChannelManager { suspend fun findEuiccChannelByPort(physicalSlotId: Int, portId: Int): EuiccChannel? fun findEuiccChannelByPortBlocking(physicalSlotId: Int, portId: Int): EuiccChannel? + /** + * Returns the first mapped & available port ID for a physical slot, or -1 if + * not found. + */ + suspend fun findFirstAvailablePort(physicalSlotId: Int): Int + class EuiccChannelNotFoundException: Exception("EuiccChannel not found") /** diff --git a/app-common/src/main/java/im/angry/openeuicc/di/DefaultUiComponentFactory.kt b/app-common/src/main/java/im/angry/openeuicc/di/DefaultUiComponentFactory.kt index 32550d6..a080017 100644 --- a/app-common/src/main/java/im/angry/openeuicc/di/DefaultUiComponentFactory.kt +++ b/app-common/src/main/java/im/angry/openeuicc/di/DefaultUiComponentFactory.kt @@ -1,13 +1,12 @@ 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.NoEuiccPlaceholderFragment open class DefaultUiComponentFactory : UiComponentFactory { - override fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment = - EuiccManagementFragment.newInstance(channel.slotId, channel.portId) + override fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment = + EuiccManagementFragment.newInstance(slotId, portId) override fun createNoEuiccPlaceholderFragment(): Fragment = NoEuiccPlaceholderFragment() } \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/di/UiComponentFactory.kt b/app-common/src/main/java/im/angry/openeuicc/di/UiComponentFactory.kt index 4e09a70..eef662c 100644 --- a/app-common/src/main/java/im/angry/openeuicc/di/UiComponentFactory.kt +++ b/app-common/src/main/java/im/angry/openeuicc/di/UiComponentFactory.kt @@ -1,10 +1,9 @@ package im.angry.openeuicc.di import androidx.fragment.app.Fragment -import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.ui.EuiccManagementFragment interface UiComponentFactory { - fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment + fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment fun createNoEuiccPlaceholderFragment(): Fragment } \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/DirectProfileDownloadActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/DirectProfileDownloadActivity.kt index 9e79de6..4baf36b 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/DirectProfileDownloadActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/DirectProfileDownloadActivity.kt @@ -3,6 +3,8 @@ package im.angry.openeuicc.ui import androidx.lifecycle.lifecycleScope import im.angry.openeuicc.util.* import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -10,22 +12,32 @@ class DirectProfileDownloadActivity : BaseEuiccAccessActivity(), SlotSelectFragm override fun onInit() { lifecycleScope.launch { val knownChannels = withContext(Dispatchers.IO) { - euiccChannelManager.enumerateEuiccChannels() + euiccChannelManager.flowEuiccPorts().map { (slotId, portId) -> + euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> + Triple(slotId, channel.logicalSlotId, portId) + } + }.toList().sortedBy { it.second } } when { knownChannels.isEmpty() -> { finish() } - knownChannels.hasMultipleChips -> { - SlotSelectFragment.newInstance(knownChannels.sortedBy { it.logicalSlotId }) + // Detect multiple eUICC chips + knownChannels.distinctBy { it.first }.size > 1 -> { + SlotSelectFragment.newInstance( + knownChannels.map { it.first }, + knownChannels.map { it.second }, + knownChannels.map { it.third }) .show(supportFragmentManager, SlotSelectFragment.TAG) } else -> { // If the device has only one eSIM "chip" (but may be mapped to multiple slots), // we can skip the slot selection dialog since there is only one chip to save to. - onSlotSelected(knownChannels[0].slotId, - knownChannels[0].portId) + onSlotSelected( + knownChannels[0].first, + knownChannels[0].third + ) } } } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt index e432f6c..74f2147 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt @@ -23,9 +23,12 @@ import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator import im.angry.openeuicc.common.R +import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.util.* import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -44,6 +47,7 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { private var refreshing = false private data class Page( + val logicalSlotId: Int, val title: String, val createFragment: () -> Fragment ) @@ -138,65 +142,83 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { // Prevent concurrent access with any running foreground task euiccChannelManagerService.waitForForegroundTask() - val knownChannels = withContext(Dispatchers.IO) { - euiccChannelManager.enumerateEuiccChannels().onEach { - Log.d(TAG, "slot ${it.slotId} port ${it.portId}") + val (usbDevice, _) = withContext(Dispatchers.IO) { + euiccChannelManager.tryOpenUsbEuiccChannel() + } + + val newPages: MutableList = mutableListOf() + + euiccChannelManager.flowEuiccPorts().onEach { (slotId, portId) -> + Log.d(TAG, "slot $slotId port $portId") + + euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> if (preferenceRepository.verboseLoggingFlow.first()) { - Log.d(TAG, it.lpa.eID) + 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(it.logicalSlotId) + euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId) + + newPages.add( + Page( + channel.logicalSlotId, + getString(R.string.channel_name_format, channel.logicalSlotId) + ) { + appContainer.uiComponentFactory.createEuiccManagementFragment( + slotId, + portId + ) + }) } + }.collect() + + // If USB readers exist, add them at the very last + // We use a wrapper fragment to handle logic specific to USB readers + usbDevice?.let { + newPages.add( + Page( + EuiccChannelManager.USB_CHANNEL_ID, + it.productName ?: getString(R.string.usb) + ) { UsbCcidReaderFragment() }) + } + viewPager.visibility = View.VISIBLE + + if (newPages.size > 1) { + tabs.visibility = View.VISIBLE + } else if (newPages.isEmpty()) { + newPages.add( + Page( + -1, + "" + ) { appContainer.uiComponentFactory.createNoEuiccPlaceholderFragment() }) } - val (usbDevice, _) = withContext(Dispatchers.IO) { - euiccChannelManager.enumerateUsbEuiccChannel() + newPages.sortBy { it.logicalSlotId } + + pages.clear() + pages.addAll(newPages) + + loadingProgress.visibility = View.GONE + pagerAdapter.notifyDataSetChanged() + // Reset the adapter so that the current view actually gets cleared + // notifyDataSetChanged() doesn't cause the current view to be removed. + viewPager.adapter = pagerAdapter + + if (fromUsbEvent && usbDevice != null) { + // If this refresh was triggered by a USB insertion while active, scroll to that page + viewPager.post { + viewPager.setCurrentItem(pages.size - 1, true) + } + } else { + viewPager.currentItem = 0 } - withContext(Dispatchers.Main) { - loadingProgress.visibility = View.GONE - - knownChannels.sortedBy { it.logicalSlotId }.forEach { channel -> - pages.add(Page( - getString(R.string.channel_name_format, channel.logicalSlotId) - ) { appContainer.uiComponentFactory.createEuiccManagementFragment(channel) }) - } - - // If USB readers exist, add them at the very last - // We use a wrapper fragment to handle logic specific to USB readers - usbDevice?.let { - pages.add(Page(it.productName ?: getString(R.string.usb)) { UsbCcidReaderFragment() }) - } - viewPager.visibility = View.VISIBLE - - if (pages.size > 1) { - tabs.visibility = View.VISIBLE - } else if (pages.isEmpty()) { - pages.add(Page("") { appContainer.uiComponentFactory.createNoEuiccPlaceholderFragment() }) - } - - pagerAdapter.notifyDataSetChanged() - // Reset the adapter so that the current view actually gets cleared - // notifyDataSetChanged() doesn't cause the current view to be removed. - viewPager.adapter = pagerAdapter - - if (fromUsbEvent && usbDevice != null) { - // If this refresh was triggered by a USB insertion while active, scroll to that page - viewPager.post { - viewPager.setCurrentItem(pages.size - 1, true) - } - } else { - viewPager.currentItem = 0 - } - - if (pages.size > 0) { - ensureNotificationPermissions() - } - - refreshing = false + if (pages.size > 0) { + ensureNotificationPermissions() } + + refreshing = false } private fun refresh(fromUsbEvent: Boolean = false) { diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/SlotSelectFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/SlotSelectFragment.kt index d1239c4..2c4fe3c 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/SlotSelectFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/SlotSelectFragment.kt @@ -16,12 +16,12 @@ class SlotSelectFragment : BaseMaterialDialogFragment(), OpenEuiccContextMarker companion object { const val TAG = "SlotSelectFragment" - fun newInstance(knownChannels: List): SlotSelectFragment { + fun newInstance(slotIds: List, logicalSlotIds: List, portIds: List): SlotSelectFragment { return SlotSelectFragment().apply { arguments = Bundle().apply { - putIntArray("slotIds", knownChannels.map { it.slotId }.toIntArray()) - putIntArray("logicalSlotIds", knownChannels.map { it.logicalSlotId }.toIntArray()) - putIntArray("portIds", knownChannels.map { it.portId }.toIntArray()) + putIntArray("slotIds", slotIds.toIntArray()) + putIntArray("logicalSlotIds", logicalSlotIds.toIntArray()) + putIntArray("portIds", portIds.toIntArray()) } } } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/UsbCcidReaderFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/UsbCcidReaderFragment.kt index 3988b09..d104582 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/UsbCcidReaderFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/UsbCcidReaderFragment.kt @@ -20,7 +20,6 @@ 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 @@ -73,7 +72,6 @@ class UsbCcidReaderFragment : Fragment(), OpenEuiccContextMarker { private lateinit var loadingProgress: ProgressBar private var usbDevice: UsbDevice? = null - private var usbChannel: EuiccChannel? = null override fun onCreateView( inflater: LayoutInflater, @@ -140,24 +138,26 @@ class UsbCcidReaderFragment : Fragment(), OpenEuiccContextMarker { permissionButton.visibility = View.GONE loadingProgress.visibility = View.VISIBLE - val (device, channel) = withContext(Dispatchers.IO) { - euiccChannelManager.enumerateUsbEuiccChannel() + val (device, canOpen) = withContext(Dispatchers.IO) { + euiccChannelManager.tryOpenUsbEuiccChannel() } loadingProgress.visibility = View.GONE usbDevice = device - usbChannel = channel - if (device != null && channel == null && !usbManager.hasPermission(device)) { + if (device != null && !canOpen && !usbManager.hasPermission(device)) { text.text = getString(R.string.usb_permission_needed) text.visibility = View.VISIBLE permissionButton.visibility = View.VISIBLE - } else if (device != null && channel != null) { + } else if (device != null && canOpen) { childFragmentManager.commit { replace( R.id.child_container, - appContainer.uiComponentFactory.createEuiccManagementFragment(channel) + appContainer.uiComponentFactory.createEuiccManagementFragment( + EuiccChannelManager.USB_CHANNEL_ID, + 0 + ) ) } } else { diff --git a/app-common/src/main/java/im/angry/openeuicc/util/LPAUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/LPAUtils.kt index e7a3322..c96361d 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/LPAUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/LPAUtils.kt @@ -48,16 +48,21 @@ fun LocalProfileAssistant.disableActiveProfile(refresh: Boolean): Boolean = } ?: true /** - * Disable the active profile, return a lambda that reverts this action when called. - * If refreshOnDisable is true, also cause a eUICC refresh command. Note that refreshing - * will disconnect the eUICC and might need some time before being operational again. + * Disable the current active profile if any. If refresh is true, also cause a refresh command. * See EuiccManager.waitForReconnect() + * + * Return the iccid of the profile being disabled, or null if no active profile found or failed to + * disable. */ -fun LocalProfileAssistant.disableActiveProfileWithUndo(refreshOnDisable: Boolean): () -> Unit = +fun LocalProfileAssistant.disableActiveProfileKeepIccId(refresh: Boolean): String? = profiles.find { it.isEnabled }?.let { - disableProfile(it.iccid, refreshOnDisable) - return { enableProfile(it.iccid) } - } ?: { } + Log.i(TAG, "Disabling active profile ${it.iccid}") + if (disableProfile(it.iccid, refresh)) { + it.iccid + } else { + null + } + } /** * Begin a "tracked" operation where notifications may be generated by the eSIM diff --git a/app-unpriv/src/main/AndroidManifest.xml b/app-unpriv/src/main/AndroidManifest.xml index e72b112..cb1ef5f 100644 --- a/app-unpriv/src/main/AndroidManifest.xml +++ b/app-unpriv/src/main/AndroidManifest.xml @@ -22,9 +22,12 @@ + android:exported="false" + android:label="@string/compatibility_check" /> + + + \ No newline at end of file diff --git a/app-unpriv/src/main/java/im/angry/openeuicc/di/UnprivilegedUiComponentFactory.kt b/app-unpriv/src/main/java/im/angry/openeuicc/di/UnprivilegedUiComponentFactory.kt index f117038..50e5581 100644 --- a/app-unpriv/src/main/java/im/angry/openeuicc/di/UnprivilegedUiComponentFactory.kt +++ b/app-unpriv/src/main/java/im/angry/openeuicc/di/UnprivilegedUiComponentFactory.kt @@ -1,9 +1,14 @@ package im.angry.openeuicc.di import androidx.fragment.app.Fragment +import im.angry.openeuicc.ui.EuiccManagementFragment +import im.angry.openeuicc.ui.UnprivilegedEuiccManagementFragment import im.angry.openeuicc.ui.UnprivilegedNoEuiccPlaceholderFragment class UnprivilegedUiComponentFactory : DefaultUiComponentFactory() { + override fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment = + UnprivilegedEuiccManagementFragment.newInstance(slotId, portId) + override fun createNoEuiccPlaceholderFragment(): Fragment = UnprivilegedNoEuiccPlaceholderFragment() } \ No newline at end of file diff --git a/app-unpriv/src/main/java/im/angry/openeuicc/ui/UnprivilegedEuiccManagementFragment.kt b/app-unpriv/src/main/java/im/angry/openeuicc/ui/UnprivilegedEuiccManagementFragment.kt new file mode 100644 index 0000000..098d6cf --- /dev/null +++ b/app-unpriv/src/main/java/im/angry/openeuicc/ui/UnprivilegedEuiccManagementFragment.kt @@ -0,0 +1,40 @@ +package im.angry.openeuicc.ui + +import android.util.Log +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import im.angry.easyeuicc.R +import im.angry.openeuicc.util.SIMToolkit +import im.angry.openeuicc.util.isUsb +import im.angry.openeuicc.util.newInstanceEuicc +import im.angry.openeuicc.util.slotId + + +class UnprivilegedEuiccManagementFragment : EuiccManagementFragment() { + companion object { + const val TAG = "UnprivilegedEuiccManagementFragment" + + fun newInstance(slotId: Int, portId: Int): EuiccManagementFragment = + newInstanceEuicc(UnprivilegedEuiccManagementFragment::class.java, slotId, portId) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + super.onCreateOptionsMenu(menu, inflater) + inflater.inflate(R.menu.fragment_sim_toolkit, menu) + menu.findItem(R.id.open_sim_toolkit).isVisible = + SIMToolkit.isAvailable(requireContext(), slotId) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean = + when (item.itemId) { + R.id.open_sim_toolkit -> { + val intent = SIMToolkit.intent(requireContext(), slotId) + Log.d(TAG, "Opening SIM Toolkit for $slotId slot, intent: $intent") + startActivity(intent) + true + } + + else -> super.onOptionsItemSelected(item) + } +} \ No newline at end of file diff --git a/app-unpriv/src/main/java/im/angry/openeuicc/util/SIMToolkit.kt b/app-unpriv/src/main/java/im/angry/openeuicc/util/SIMToolkit.kt new file mode 100644 index 0000000..6fcc6c0 --- /dev/null +++ b/app-unpriv/src/main/java/im/angry/openeuicc/util/SIMToolkit.kt @@ -0,0 +1,63 @@ +package im.angry.openeuicc.util + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager.NameNotFoundException + +object SIMToolkit { + private val packageNames = buildSet { + addAll(slot1activities.map { it.packageName }) + addAll(slot2activities.map { it.packageName }) + } + + private val slot1activities = arrayOf( + ComponentName("com.android.stk", "com.android.stk.StkMain1"), + ) + + private val slot2activities = arrayOf( + ComponentName("com.android.stk", "com.android.stk.StkMain2"), + ) + + private fun getGeneralIntent(context: Context): Intent? { + for (packageName in packageNames) { + try { + return context.packageManager.getLaunchIntentForPackage(packageName) + } catch (_: NameNotFoundException) { + continue + } + } + return null + } + + private fun getComponentName(context: Context, slotId: Int): ComponentName? { + val components = when (slotId) { + 0 -> slot1activities + 1 -> slot2activities + else -> return null + } + return components.find { + try { + context.packageManager.getActivityIcon(it) + true + } catch (_: NameNotFoundException) { + false + } + } + } + + fun isAvailable(context: Context, slotId: Int): Boolean { + if (getComponentName(context, slotId) != null) return true + if (getGeneralIntent(context) != null) return true + return false + } + + fun intent(context: Context, slotId: Int): Intent? { + val intent = Intent(Intent.ACTION_MAIN, null) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + intent.component = getComponentName(context, slotId) + intent.addCategory(Intent.CATEGORY_LAUNCHER) + if (intent.component == null) return getGeneralIntent(context) + return intent + } +} diff --git a/app-unpriv/src/main/res/menu/fragment_sim_toolkit.xml b/app-unpriv/src/main/res/menu/fragment_sim_toolkit.xml new file mode 100644 index 0000000..610b3a1 --- /dev/null +++ b/app-unpriv/src/main/res/menu/fragment_sim_toolkit.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app-unpriv/src/main/res/values/strings.xml b/app-unpriv/src/main/res/values/strings.xml index 548a7b9..fb7dc94 100644 --- a/app-unpriv/src/main/res/values/strings.xml +++ b/app-unpriv/src/main/res/values/strings.xml @@ -2,6 +2,7 @@ EasyEUICC SIM %d Compatibility Check + Open SIM Toolkit System Features diff --git a/app/src/main/java/im/angry/openeuicc/di/PrivilegedUiComponentFactory.kt b/app/src/main/java/im/angry/openeuicc/di/PrivilegedUiComponentFactory.kt index d3c5cdb..701e57d 100644 --- a/app/src/main/java/im/angry/openeuicc/di/PrivilegedUiComponentFactory.kt +++ b/app/src/main/java/im/angry/openeuicc/di/PrivilegedUiComponentFactory.kt @@ -1,10 +1,9 @@ package im.angry.openeuicc.di -import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.ui.EuiccManagementFragment import im.angry.openeuicc.ui.PrivilegedEuiccManagementFragment class PrivilegedUiComponentFactory : DefaultUiComponentFactory() { - override fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment = - PrivilegedEuiccManagementFragment.newInstance(channel.slotId, channel.portId) + override fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment = + PrivilegedEuiccManagementFragment.newInstance(slotId, portId) } \ No newline at end of file diff --git a/app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt b/app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt index c85af36..8ec27ec 100644 --- a/app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt +++ b/app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt @@ -13,6 +13,7 @@ import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.util.* import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.last import kotlinx.coroutines.runBlocking import java.lang.IllegalStateException @@ -37,8 +38,11 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker { } private data class EuiccChannelManagerContext( - val euiccChannelManager: EuiccChannelManager + val euiccChannelManagerService: EuiccChannelManagerService ) { + val euiccChannelManager + get() = euiccChannelManagerService.euiccChannelManager + fun findChannel(physicalSlotId: Int): EuiccChannel? = euiccChannelManager.findEuiccChannelByPhysicalSlotBlocking(physicalSlotId) @@ -59,7 +63,7 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker { * * This function cannot be inline because non-local returns may bypass the unbind */ - private fun withEuiccChannelManager(fn: EuiccChannelManagerContext.() -> T): T { + private fun withEuiccChannelManager(fn: suspend EuiccChannelManagerContext.() -> T): T { val (binder, unbind) = runBlocking { bindServiceSuspended( Intent( @@ -73,8 +77,11 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker { throw RuntimeException("Unable to bind to EuiccChannelManagerService; aborting") } - val ret = - EuiccChannelManagerContext((binder as EuiccChannelManagerService.LocalBinder).service.euiccChannelManager).fn() + val localBinder = binder as EuiccChannelManagerService.LocalBinder + + val ret = runBlocking { + EuiccChannelManagerContext(localBinder.service).fn() + } unbind() return ret @@ -177,38 +184,54 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker { } // TODO: Temporarily enable the slot to access its profiles if it is currently unmapped - val channel = - findChannel(slotId) ?: return@withEuiccChannelManager GetEuiccProfileInfoListResult( + val port = euiccChannelManager.findFirstAvailablePort(slotId) + if (port == -1) { + return@withEuiccChannelManager GetEuiccProfileInfoListResult( RESULT_FIRST_USER, arrayOf(), true ) - val profiles = channel.lpa.profiles.operational.map { - EuiccProfileInfo.Builder(it.iccid).apply { - setProfileName(it.name) - setNickname(it.displayName) - setServiceProviderName(it.providerName) - setState( - when (it.state) { - LocalProfileInfo.State.Enabled -> EuiccProfileInfo.PROFILE_STATE_ENABLED - LocalProfileInfo.State.Disabled -> EuiccProfileInfo.PROFILE_STATE_DISABLED - } - ) - setProfileClass( - when (it.profileClass) { - LocalProfileInfo.Clazz.Testing -> EuiccProfileInfo.PROFILE_CLASS_TESTING - LocalProfileInfo.Clazz.Provisioning -> EuiccProfileInfo.PROFILE_CLASS_PROVISIONING - LocalProfileInfo.Clazz.Operational -> EuiccProfileInfo.PROFILE_CLASS_OPERATIONAL - } - ) - }.build() } - return@withEuiccChannelManager GetEuiccProfileInfoListResult( - RESULT_OK, - profiles.toTypedArray(), - channel.port.card.isRemovable - ) + try { + return@withEuiccChannelManager euiccChannelManager.withEuiccChannel( + slotId, + port + ) { channel -> + val profiles = channel.lpa.profiles.operational.map { + EuiccProfileInfo.Builder(it.iccid).apply { + setProfileName(it.name) + setNickname(it.displayName) + setServiceProviderName(it.providerName) + setState( + when (it.state) { + LocalProfileInfo.State.Enabled -> EuiccProfileInfo.PROFILE_STATE_ENABLED + LocalProfileInfo.State.Disabled -> EuiccProfileInfo.PROFILE_STATE_DISABLED + } + ) + setProfileClass( + when (it.profileClass) { + LocalProfileInfo.Clazz.Testing -> EuiccProfileInfo.PROFILE_CLASS_TESTING + LocalProfileInfo.Clazz.Provisioning -> EuiccProfileInfo.PROFILE_CLASS_PROVISIONING + LocalProfileInfo.Clazz.Operational -> EuiccProfileInfo.PROFILE_CLASS_OPERATIONAL + } + ) + }.build() + } + + GetEuiccProfileInfoListResult( + RESULT_OK, + profiles.toTypedArray(), + channel.port.card.isRemovable + ) + } + } catch (e: EuiccChannelManager.EuiccChannelNotFoundException) { + return@withEuiccChannelManager GetEuiccProfileInfoListResult( + RESULT_FIRST_USER, + arrayOf(), + true + ) + } } override fun onGetEuiccInfo(slotId: Int): EuiccInfo { @@ -335,13 +358,17 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker { "onUpdateSubscriptionNickname slotId=$slotId iccid=$iccid nickname=$nickname" ) if (shouldIgnoreSlot(slotId)) return@withEuiccChannelManager RESULT_FIRST_USER - val channel = findChannel(slotId) ?: return@withEuiccChannelManager RESULT_FIRST_USER - if (!channel.profileExists(iccid)) { + val port = euiccChannelManager.findFirstAvailablePort(slotId) + if (port < 0) { return@withEuiccChannelManager RESULT_FIRST_USER } - val success = channel.lpa - .setNickname(iccid, nickname!!) - appContainer.subscriptionManager.tryRefreshCachedEuiccInfo(channel.cardId) + val success = + (euiccChannelManagerService.launchProfileRenameTask(slotId, port, iccid, nickname!!) + ?.last() as? EuiccChannelManagerService.ForegroundTaskState.Done)?.error == null + + euiccChannelManager.withEuiccChannel(slotId, port) { channel -> + appContainer.subscriptionManager.tryRefreshCachedEuiccInfo(channel.cardId) + } return@withEuiccChannelManager if (success) { RESULT_OK } else { diff --git a/app/src/main/java/im/angry/openeuicc/util/PrivilegedTelephonyUtils.kt b/app/src/main/java/im/angry/openeuicc/util/PrivilegedTelephonyUtils.kt index 0c0f6f8..edeb49f 100644 --- a/app/src/main/java/im/angry/openeuicc/util/PrivilegedTelephonyUtils.kt +++ b/app/src/main/java/im/angry/openeuicc/util/PrivilegedTelephonyUtils.kt @@ -5,6 +5,7 @@ import android.telephony.TelephonyManager import android.telephony.UiccSlotMapping import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.core.EuiccChannelManager +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.runBlocking import java.lang.Exception @@ -15,14 +16,14 @@ val TelephonyManager.dsdsEnabled: Boolean get() = activeModemCount >= 2 fun TelephonyManager.setDsdsEnabled(euiccManager: EuiccChannelManager, enabled: Boolean) { - val knownChannels = runBlocking { - euiccManager.enumerateEuiccChannels() - } - // Disable all eSIM profiles before performing a DSDS switch (only for internal eSIMs) - knownChannels.forEach { - if (!it.port.card.isRemovable) { - it.lpa.disableActiveProfileWithUndo(false) + runBlocking { + euiccManager.flowEuiccPorts().onEach { (slotId, portId) -> + euiccManager.withEuiccChannel(slotId, portId) { + if (!it.port.card.isRemovable) { + it.lpa.disableActiveProfile(false) + } + } } } @@ -31,7 +32,7 @@ fun TelephonyManager.setDsdsEnabled(euiccManager: EuiccChannelManager, enabled: // Disable eSIM profiles before switching the slot mapping // This ensures that unmapped eSIM ports never have "ghost" profiles enabled -fun TelephonyManager.updateSimSlotMapping( +suspend fun TelephonyManager.updateSimSlotMapping( euiccManager: EuiccChannelManager, newMapping: Collection, currentMapping: Collection = simSlotMapping ) { @@ -42,14 +43,24 @@ fun TelephonyManager.updateSimSlotMapping( } } - val undo = unmapped.mapNotNull { mapping -> - euiccManager.findEuiccChannelByPortBlocking(mapping.physicalSlotIndex, mapping.portIndex)?.let { channel -> + val undo: List Unit> = unmapped.mapNotNull { mapping -> + euiccManager.withEuiccChannel(mapping.physicalSlotIndex, mapping.portIndex) { channel -> if (!channel.port.card.isRemovable) { - return@mapNotNull channel.lpa.disableActiveProfileWithUndo(false) + channel.lpa.disableActiveProfileKeepIccId(false) } else { // Do not do anything for external eUICCs -- we can't really trust them to work properly // with no profile enabled. - return@mapNotNull null + null + } + }?.let { iccid -> + // Generate undo closure because we can't keep reference to `channel` in the closure above + { + euiccManager.withEuiccChannel( + mapping.physicalSlotIndex, + mapping.portIndex + ) { channel -> + channel.lpa.enableProfile(iccid) + } } } }