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 a1cbe86..550cc22 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,8 +11,14 @@ 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 @@ -165,15 +171,6 @@ 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, @@ -232,6 +229,13 @@ 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 -> @@ -247,20 +251,20 @@ open class DefaultEuiccChannelManager( } }.flowOn(Dispatchers.IO) - override suspend fun tryOpenUsbEuiccChannel(): Pair = + override suspend fun enumerateUsbEuiccChannel(): 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, false) + if (!usbManager.hasPermission(device)) return@withContext Pair(device, null) 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, true) + return@withContext Pair(device, channel) } } catch (e: Exception) { // Ignored -- skip forward @@ -268,7 +272,7 @@ open class DefaultEuiccChannelManager( } Log.i(TAG, "No valid eUICC channel found on USB device ${device.deviceId}:${device.vendorId}") } - return@withContext Pair(null, false) + return@withContext Pair(null, null) } 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 9378f96..2d55179 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,26 +19,21 @@ interface EuiccChannelManager { } /** - * 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()`. + * 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 */ + 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 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. + * to interact with the device, the second return value (EuiccChannel) will be null. */ - suspend fun tryOpenUsbEuiccChannel(): Pair + suspend fun enumerateUsbEuiccChannel(): Pair /** * Wait for a slot + port to reconnect (i.e. become valid again) @@ -72,12 +67,6 @@ 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 a080017..32550d6 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,12 +1,13 @@ 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(slotId: Int, portId: Int): EuiccManagementFragment = - EuiccManagementFragment.newInstance(slotId, portId) + override fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment = + EuiccManagementFragment.newInstance(channel.slotId, channel.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 eef662c..4e09a70 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,9 +1,10 @@ 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(slotId: Int, portId: Int): EuiccManagementFragment + fun createEuiccManagementFragment(channel: EuiccChannel): 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 4baf36b..9e79de6 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,8 +3,6 @@ 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 @@ -12,32 +10,22 @@ class DirectProfileDownloadActivity : BaseEuiccAccessActivity(), SlotSelectFragm override fun onInit() { lifecycleScope.launch { val knownChannels = withContext(Dispatchers.IO) { - euiccChannelManager.flowEuiccPorts().map { (slotId, portId) -> - euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> - Triple(slotId, channel.logicalSlotId, portId) - } - }.toList().sortedBy { it.second } + euiccChannelManager.enumerateEuiccChannels() } when { knownChannels.isEmpty() -> { finish() } - // Detect multiple eUICC chips - knownChannels.distinctBy { it.first }.size > 1 -> { - SlotSelectFragment.newInstance( - knownChannels.map { it.first }, - knownChannels.map { it.second }, - knownChannels.map { it.third }) + knownChannels.hasMultipleChips -> { + SlotSelectFragment.newInstance(knownChannels.sortedBy { it.logicalSlotId }) .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].first, - knownChannels[0].third - ) + onSlotSelected(knownChannels[0].slotId, + knownChannels[0].portId) } } } 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 74f2147..e432f6c 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,12 +23,9 @@ 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 @@ -47,7 +44,6 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { private var refreshing = false private data class Page( - val logicalSlotId: Int, val title: String, val createFragment: () -> Fragment ) @@ -142,83 +138,65 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { // Prevent concurrent access with any running foreground task euiccChannelManagerService.waitForForegroundTask() - 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 -> + val knownChannels = withContext(Dispatchers.IO) { + euiccChannelManager.enumerateEuiccChannels().onEach { + Log.d(TAG, "slot ${it.slotId} port ${it.portId}") if (preferenceRepository.verboseLoggingFlow.first()) { - Log.d(TAG, channel.lpa.eID) + Log.d(TAG, it.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) - - newPages.add( - Page( - channel.logicalSlotId, - getString(R.string.channel_name_format, channel.logicalSlotId) - ) { - appContainer.uiComponentFactory.createEuiccManagementFragment( - slotId, - portId - ) - }) + euiccChannelManager.notifyEuiccProfilesChanged(it.logicalSlotId) } - }.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() }) } - newPages.sortBy { it.logicalSlotId } + val (usbDevice, _) = withContext(Dispatchers.IO) { + euiccChannelManager.enumerateUsbEuiccChannel() + } - pages.clear() - pages.addAll(newPages) + withContext(Dispatchers.Main) { + loadingProgress.visibility = View.GONE - 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) + knownChannels.sortedBy { it.logicalSlotId }.forEach { channel -> + pages.add(Page( + getString(R.string.channel_name_format, channel.logicalSlotId) + ) { appContainer.uiComponentFactory.createEuiccManagementFragment(channel) }) } - } else { - viewPager.currentItem = 0 - } - if (pages.size > 0) { - ensureNotificationPermissions() - } + // 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 - refreshing = false + 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 + } } 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 2c4fe3c..d1239c4 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(slotIds: List, logicalSlotIds: List, portIds: List): SlotSelectFragment { + fun newInstance(knownChannels: List): SlotSelectFragment { return SlotSelectFragment().apply { arguments = Bundle().apply { - putIntArray("slotIds", slotIds.toIntArray()) - putIntArray("logicalSlotIds", logicalSlotIds.toIntArray()) - putIntArray("portIds", portIds.toIntArray()) + putIntArray("slotIds", knownChannels.map { it.slotId }.toIntArray()) + putIntArray("logicalSlotIds", knownChannels.map { it.logicalSlotId }.toIntArray()) + putIntArray("portIds", knownChannels.map { it.portId }.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 d104582..3988b09 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/UsbCcidReaderFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/UsbCcidReaderFragment.kt @@ -20,6 +20,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.commit import androidx.lifecycle.lifecycleScope import im.angry.openeuicc.common.R +import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.util.* import kotlinx.coroutines.Dispatchers @@ -72,6 +73,7 @@ class UsbCcidReaderFragment : Fragment(), OpenEuiccContextMarker { private lateinit var loadingProgress: ProgressBar private var usbDevice: UsbDevice? = null + private var usbChannel: EuiccChannel? = null override fun onCreateView( inflater: LayoutInflater, @@ -138,26 +140,24 @@ class UsbCcidReaderFragment : Fragment(), OpenEuiccContextMarker { permissionButton.visibility = View.GONE loadingProgress.visibility = View.VISIBLE - val (device, canOpen) = withContext(Dispatchers.IO) { - euiccChannelManager.tryOpenUsbEuiccChannel() + val (device, channel) = withContext(Dispatchers.IO) { + euiccChannelManager.enumerateUsbEuiccChannel() } loadingProgress.visibility = View.GONE usbDevice = device + usbChannel = channel - if (device != null && !canOpen && !usbManager.hasPermission(device)) { + if (device != null && channel == null && !usbManager.hasPermission(device)) { text.text = getString(R.string.usb_permission_needed) text.visibility = View.VISIBLE permissionButton.visibility = View.VISIBLE - } else if (device != null && canOpen) { + } else if (device != null && channel != null) { childFragmentManager.commit { replace( R.id.child_container, - appContainer.uiComponentFactory.createEuiccManagementFragment( - EuiccChannelManager.USB_CHANNEL_ID, - 0 - ) + appContainer.uiComponentFactory.createEuiccManagementFragment(channel) ) } } 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 c96361d..e7a3322 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,21 +48,16 @@ fun LocalProfileAssistant.disableActiveProfile(refresh: Boolean): Boolean = } ?: true /** - * Disable the current active profile if any. If refresh is true, also cause a refresh command. + * 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. * See EuiccManager.waitForReconnect() - * - * Return the iccid of the profile being disabled, or null if no active profile found or failed to - * disable. */ -fun LocalProfileAssistant.disableActiveProfileKeepIccId(refresh: Boolean): String? = +fun LocalProfileAssistant.disableActiveProfileWithUndo(refreshOnDisable: Boolean): () -> Unit = profiles.find { it.isEnabled }?.let { - Log.i(TAG, "Disabling active profile ${it.iccid}") - if (disableProfile(it.iccid, refresh)) { - it.iccid - } else { - null - } - } + disableProfile(it.iccid, refreshOnDisable) + return { enableProfile(it.iccid) } + } ?: { } /** * 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 cb1ef5f..e72b112 100644 --- a/app-unpriv/src/main/AndroidManifest.xml +++ b/app-unpriv/src/main/AndroidManifest.xml @@ -22,12 +22,9 @@ + android:label="@string/compatibility_check" + android:exported="false" /> - - - \ 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 50e5581..f117038 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,14 +1,9 @@ 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 deleted file mode 100644 index 098d6cf..0000000 --- a/app-unpriv/src/main/java/im/angry/openeuicc/ui/UnprivilegedEuiccManagementFragment.kt +++ /dev/null @@ -1,40 +0,0 @@ -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 deleted file mode 100644 index 6fcc6c0..0000000 --- a/app-unpriv/src/main/java/im/angry/openeuicc/util/SIMToolkit.kt +++ /dev/null @@ -1,63 +0,0 @@ -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 deleted file mode 100644 index 610b3a1..0000000 --- a/app-unpriv/src/main/res/menu/fragment_sim_toolkit.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ 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 fb7dc94..548a7b9 100644 --- a/app-unpriv/src/main/res/values/strings.xml +++ b/app-unpriv/src/main/res/values/strings.xml @@ -2,7 +2,6 @@ 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 701e57d..d3c5cdb 100644 --- a/app/src/main/java/im/angry/openeuicc/di/PrivilegedUiComponentFactory.kt +++ b/app/src/main/java/im/angry/openeuicc/di/PrivilegedUiComponentFactory.kt @@ -1,9 +1,10 @@ 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(slotId: Int, portId: Int): EuiccManagementFragment = - PrivilegedEuiccManagementFragment.newInstance(slotId, portId) + override fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment = + PrivilegedEuiccManagementFragment.newInstance(channel.slotId, channel.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 8ec27ec..c85af36 100644 --- a/app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt +++ b/app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt @@ -13,7 +13,6 @@ 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 @@ -38,11 +37,8 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker { } private data class EuiccChannelManagerContext( - val euiccChannelManagerService: EuiccChannelManagerService + val euiccChannelManager: EuiccChannelManager ) { - val euiccChannelManager - get() = euiccChannelManagerService.euiccChannelManager - fun findChannel(physicalSlotId: Int): EuiccChannel? = euiccChannelManager.findEuiccChannelByPhysicalSlotBlocking(physicalSlotId) @@ -63,7 +59,7 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker { * * This function cannot be inline because non-local returns may bypass the unbind */ - private fun withEuiccChannelManager(fn: suspend EuiccChannelManagerContext.() -> T): T { + private fun withEuiccChannelManager(fn: EuiccChannelManagerContext.() -> T): T { val (binder, unbind) = runBlocking { bindServiceSuspended( Intent( @@ -77,11 +73,8 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker { throw RuntimeException("Unable to bind to EuiccChannelManagerService; aborting") } - val localBinder = binder as EuiccChannelManagerService.LocalBinder - - val ret = runBlocking { - EuiccChannelManagerContext(localBinder.service).fn() - } + val ret = + EuiccChannelManagerContext((binder as EuiccChannelManagerService.LocalBinder).service.euiccChannelManager).fn() unbind() return ret @@ -184,54 +177,38 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker { } // 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( + val channel = + findChannel(slotId) ?: return@withEuiccChannelManager GetEuiccProfileInfoListResult( RESULT_FIRST_USER, arrayOf(), true ) - } - - 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 + 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 + } ) - } - } catch (e: EuiccChannelManager.EuiccChannelNotFoundException) { - return@withEuiccChannelManager GetEuiccProfileInfoListResult( - RESULT_FIRST_USER, - arrayOf(), - true - ) + 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 + ) } override fun onGetEuiccInfo(slotId: Int): EuiccInfo { @@ -358,17 +335,13 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker { "onUpdateSubscriptionNickname slotId=$slotId iccid=$iccid nickname=$nickname" ) if (shouldIgnoreSlot(slotId)) return@withEuiccChannelManager RESULT_FIRST_USER - val port = euiccChannelManager.findFirstAvailablePort(slotId) - if (port < 0) { + val channel = findChannel(slotId) ?: return@withEuiccChannelManager RESULT_FIRST_USER + if (!channel.profileExists(iccid)) { return@withEuiccChannelManager RESULT_FIRST_USER } - 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) - } + val success = channel.lpa + .setNickname(iccid, nickname!!) + 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 edeb49f..0c0f6f8 100644 --- a/app/src/main/java/im/angry/openeuicc/util/PrivilegedTelephonyUtils.kt +++ b/app/src/main/java/im/angry/openeuicc/util/PrivilegedTelephonyUtils.kt @@ -5,7 +5,6 @@ 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 @@ -16,14 +15,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) - runBlocking { - euiccManager.flowEuiccPorts().onEach { (slotId, portId) -> - euiccManager.withEuiccChannel(slotId, portId) { - if (!it.port.card.isRemovable) { - it.lpa.disableActiveProfile(false) - } - } + knownChannels.forEach { + if (!it.port.card.isRemovable) { + it.lpa.disableActiveProfileWithUndo(false) } } @@ -32,7 +31,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 -suspend fun TelephonyManager.updateSimSlotMapping( +fun TelephonyManager.updateSimSlotMapping( euiccManager: EuiccChannelManager, newMapping: Collection, currentMapping: Collection = simSlotMapping ) { @@ -43,24 +42,14 @@ suspend fun TelephonyManager.updateSimSlotMapping( } } - val undo: List Unit> = unmapped.mapNotNull { mapping -> - euiccManager.withEuiccChannel(mapping.physicalSlotIndex, mapping.portIndex) { channel -> + val undo = unmapped.mapNotNull { mapping -> + euiccManager.findEuiccChannelByPortBlocking(mapping.physicalSlotIndex, mapping.portIndex)?.let { channel -> if (!channel.port.card.isRemovable) { - channel.lpa.disableActiveProfileKeepIccId(false) + return@mapNotNull channel.lpa.disableActiveProfileWithUndo(false) } else { // Do not do anything for external eUICCs -- we can't really trust them to work properly // with no profile enabled. - 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) - } + return@mapNotNull null } } }