From 87eb497f401bb7f7bf206b05e2fb5dce79ffe7fe Mon Sep 17 00:00:00 2001 From: septs Date: Sat, 2 Nov 2024 19:29:37 +0100 Subject: [PATCH 01/10] feat: open stk from menu (#57) Reviewed-on: https://gitea.angry.im/PeterCxy/OpenEUICC/pulls/57 Co-authored-by: septs Co-committed-by: septs --- app-unpriv/src/main/AndroidManifest.xml | 7 +++- .../di/UnprivilegedUiComponentFactory.kt | 6 +++ .../ui/UnprivilegedEuiccManagementFragment.kt | 40 +++++++++++++++++++ .../im/angry/openeuicc/util/SIMToolkit.kt | 38 ++++++++++++++++++ .../main/res/menu/fragment_sim_toolkit.xml | 9 +++++ app-unpriv/src/main/res/values/strings.xml | 1 + 6 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 app-unpriv/src/main/java/im/angry/openeuicc/ui/UnprivilegedEuiccManagementFragment.kt create mode 100644 app-unpriv/src/main/java/im/angry/openeuicc/util/SIMToolkit.kt create mode 100644 app-unpriv/src/main/res/menu/fragment_sim_toolkit.xml 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..2531676 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,15 @@ 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.UnprivilegedEuiccManagementFragment import im.angry.openeuicc.ui.UnprivilegedNoEuiccPlaceholderFragment class UnprivilegedUiComponentFactory : DefaultUiComponentFactory() { + override fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment = + UnprivilegedEuiccManagementFragment.newInstance(channel.slotId, channel.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..91c4804 --- /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.getComponentName(requireContext(), slotId) != null + } + + 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..b6aed0f --- /dev/null +++ b/app-unpriv/src/main/java/im/angry/openeuicc/util/SIMToolkit.kt @@ -0,0 +1,38 @@ +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 slot1activities = arrayOf( + ComponentName("com.android.stk", "com.android.stk.StkMain1"), + ) + + private val slot2activities = arrayOf( + ComponentName("com.android.stk", "com.android.stk.StkMain2"), + ) + + 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 intent(context: Context, slotId: Int) = Intent(Intent.ACTION_MAIN, null).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK + component = getComponentName(context, slotId) + addCategory(Intent.CATEGORY_LAUNCHER) + } +} 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 From 5e5210ae2da513a3f8e357bb209ad4a10c734055 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 2 Nov 2024 16:54:30 -0400 Subject: [PATCH 02/10] MainActivity: switch to flowEuiccPorts() --- .../openeuicc/di/DefaultUiComponentFactory.kt | 5 +- .../angry/openeuicc/di/UiComponentFactory.kt | 3 +- .../im/angry/openeuicc/ui/MainActivity.kt | 118 +++++++++++------- .../openeuicc/ui/UsbCcidReaderFragment.kt | 5 +- .../di/UnprivilegedUiComponentFactory.kt | 5 +- .../di/PrivilegedUiComponentFactory.kt | 5 +- 6 files changed, 81 insertions(+), 60 deletions(-) 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/MainActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt index e432f6c..eb149a9 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}") - if (preferenceRepository.verboseLoggingFlow.first()) { - 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(it.logicalSlotId) - } - } - val (usbDevice, _) = withContext(Dispatchers.IO) { euiccChannelManager.enumerateUsbEuiccChannel() } - withContext(Dispatchers.Main) { - loadingProgress.visibility = View.GONE + val newPages: MutableList = mutableListOf() - knownChannels.sortedBy { it.logicalSlotId }.forEach { channel -> - pages.add(Page( - getString(R.string.channel_name_format, channel.logicalSlotId) - ) { appContainer.uiComponentFactory.createEuiccManagementFragment(channel) }) - } + euiccChannelManager.flowEuiccPorts().onEach { (slotId, portId) -> + Log.d(TAG, "slot $slotId port $portId") - // 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) + euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> + if (preferenceRepository.verboseLoggingFlow.first()) { + Log.d(TAG, channel.lpa.eID) } - } else { - viewPager.currentItem = 0 - } + // 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) - if (pages.size > 0) { - ensureNotificationPermissions() + newPages.add( + Page( + channel.logicalSlotId, + getString(R.string.channel_name_format, channel.logicalSlotId) + ) { + appContainer.uiComponentFactory.createEuiccManagementFragment( + slotId, + portId + ) + }) } + }.collect() - refreshing = false + // 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( + EuiccChannelManager.USB_CHANNEL_ID, + 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( + -1, + "" + ) { appContainer.uiComponentFactory.createNoEuiccPlaceholderFragment() }) + } + + 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 + } + + 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/UsbCcidReaderFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/UsbCcidReaderFragment.kt index 3988b09..6170fc2 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 @@ -157,7 +157,10 @@ class UsbCcidReaderFragment : Fragment(), OpenEuiccContextMarker { childFragmentManager.commit { replace( R.id.child_container, - appContainer.uiComponentFactory.createEuiccManagementFragment(channel) + appContainer.uiComponentFactory.createEuiccManagementFragment( + channel.slotId, + channel.portId + ) ) } } else { 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 2531676..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,14 +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.UnprivilegedEuiccManagementFragment import im.angry.openeuicc.ui.UnprivilegedNoEuiccPlaceholderFragment class UnprivilegedUiComponentFactory : DefaultUiComponentFactory() { - override fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment = - UnprivilegedEuiccManagementFragment.newInstance(channel.slotId, channel.portId) + override fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment = + UnprivilegedEuiccManagementFragment.newInstance(slotId, portId) override fun createNoEuiccPlaceholderFragment(): Fragment = UnprivilegedNoEuiccPlaceholderFragment() 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 From 6257a0305881802e6fbb18c445e7ee8c4961cbe4 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 2 Nov 2024 17:16:43 -0400 Subject: [PATCH 03/10] DirectProfileDownloadActivity: use flowEuiccPorts() --- .../ui/DirectProfileDownloadActivity.kt | 22 ++++++++++++++----- .../angry/openeuicc/ui/SlotSelectFragment.kt | 8 +++---- 2 files changed, 21 insertions(+), 9 deletions(-) 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/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()) } } } From 272ab953e06503284875033fd432a59943313a8b Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 2 Nov 2024 18:09:12 -0400 Subject: [PATCH 04/10] PrivilegedTelephonyUtils: Switch to flowEuiccPorts() --- .../openeuicc/util/PrivilegedTelephonyUtils.kt | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) 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..2c18321 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.disableActiveProfileWithUndo(false) + } + } } } From 573dce56a64a27685dacb6e52f5a3216de20c331 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 2 Nov 2024 19:06:31 -0400 Subject: [PATCH 05/10] EuiccChannelManager: Stop emitting real EuiccChannel for USB --- .../openeuicc/core/DefaultEuiccChannelManager.kt | 8 ++++---- .../angry/openeuicc/core/EuiccChannelManager.kt | 8 ++++++-- .../java/im/angry/openeuicc/ui/MainActivity.kt | 2 +- .../angry/openeuicc/ui/UsbCcidReaderFragment.kt | 15 ++++++--------- 4 files changed, 17 insertions(+), 16 deletions(-) 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..ae473b7 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 @@ -251,20 +251,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 +272,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..b52f5ba 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 @@ -31,9 +31,13 @@ interface EuiccChannelManager { * 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) 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 eb149a9..8dbe3ca 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 @@ -143,7 +143,7 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { euiccChannelManagerService.waitForForegroundTask() val (usbDevice, _) = withContext(Dispatchers.IO) { - euiccChannelManager.enumerateUsbEuiccChannel() + euiccChannelManager.tryOpenUsbEuiccChannel() } val newPages: MutableList = mutableListOf() 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 6170fc2..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,26 +138,25 @@ 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.slotId, - channel.portId + EuiccChannelManager.USB_CHANNEL_ID, + 0 ) ) } From 0ef435956c9e380535a37862bda9a4ad02094864 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 2 Nov 2024 19:09:57 -0400 Subject: [PATCH 06/10] EuiccChannelManager: retire enumerateEuiccChannels() --- .../openeuicc/core/DefaultEuiccChannelManager.kt | 13 ------------- .../im/angry/openeuicc/core/EuiccChannelManager.kt | 11 ++++++----- 2 files changed, 6 insertions(+), 18 deletions(-) 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 ae473b7..7549f4e 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 @@ -229,13 +223,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 -> 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 b52f5ba..e252e06 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,12 +19,13 @@ 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> /** From 04debd62d58c6d92851ab9e5993a482ce9d5b694 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 2 Nov 2024 20:58:56 -0400 Subject: [PATCH 07/10] MainActivity: Fixup ViewPager update --- .../src/main/java/im/angry/openeuicc/ui/MainActivity.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 8dbe3ca..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 @@ -176,7 +176,7 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { // 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( + newPages.add( Page( EuiccChannelManager.USB_CHANNEL_ID, it.productName ?: getString(R.string.usb) @@ -184,10 +184,10 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { } viewPager.visibility = View.VISIBLE - if (pages.size > 1) { + if (newPages.size > 1) { tabs.visibility = View.VISIBLE - } else if (pages.isEmpty()) { - pages.add( + } else if (newPages.isEmpty()) { + newPages.add( Page( -1, "" From 32f5e3f71a9e31f485801ff23f68b55eddc6ff56 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 2 Nov 2024 21:36:08 -0400 Subject: [PATCH 08/10] PrivilegedTelephonyUtils: Nuke direct EuiccChannel usage --- .../java/im/angry/openeuicc/util/LPAUtils.kt | 19 ++++++++++------ .../util/PrivilegedTelephonyUtils.kt | 22 ++++++++++++++----- 2 files changed, 28 insertions(+), 13 deletions(-) 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/src/main/java/im/angry/openeuicc/util/PrivilegedTelephonyUtils.kt b/app/src/main/java/im/angry/openeuicc/util/PrivilegedTelephonyUtils.kt index 2c18321..edeb49f 100644 --- a/app/src/main/java/im/angry/openeuicc/util/PrivilegedTelephonyUtils.kt +++ b/app/src/main/java/im/angry/openeuicc/util/PrivilegedTelephonyUtils.kt @@ -21,7 +21,7 @@ fun TelephonyManager.setDsdsEnabled(euiccManager: EuiccChannelManager, enabled: euiccManager.flowEuiccPorts().onEach { (slotId, portId) -> euiccManager.withEuiccChannel(slotId, portId) { if (!it.port.card.isRemovable) { - it.lpa.disableActiveProfileWithUndo(false) + it.lpa.disableActiveProfile(false) } } } @@ -32,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 ) { @@ -43,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) + } } } } From 22ec3e3bafce1a1303c5817fe9e4eb46aa8311d7 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 3 Nov 2024 10:53:24 -0500 Subject: [PATCH 09/10] OpenEuiccService: Start migrating to withEuiccChannel() --- .../core/DefaultEuiccChannelManager.kt | 9 ++ .../openeuicc/core/EuiccChannelManager.kt | 6 ++ .../openeuicc/service/OpenEuiccService.kt | 97 ++++++++++++------- 3 files changed, 77 insertions(+), 35 deletions(-) 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 7549f4e..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 @@ -165,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, 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 e252e06..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 @@ -72,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/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 { From 52c60d443f11a20ded42002d8aac236b1b763d9f Mon Sep 17 00:00:00 2001 From: septs Date: Tue, 5 Nov 2024 18:18:54 +0800 Subject: [PATCH 10/10] feat: add commonly stk opening logical --- .../ui/UnprivilegedEuiccManagementFragment.kt | 2 +- .../im/angry/openeuicc/util/SIMToolkit.kt | 35 ++++++++++++++++--- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/app-unpriv/src/main/java/im/angry/openeuicc/ui/UnprivilegedEuiccManagementFragment.kt b/app-unpriv/src/main/java/im/angry/openeuicc/ui/UnprivilegedEuiccManagementFragment.kt index 91c4804..098d6cf 100644 --- a/app-unpriv/src/main/java/im/angry/openeuicc/ui/UnprivilegedEuiccManagementFragment.kt +++ b/app-unpriv/src/main/java/im/angry/openeuicc/ui/UnprivilegedEuiccManagementFragment.kt @@ -23,7 +23,7 @@ class UnprivilegedEuiccManagementFragment : EuiccManagementFragment() { super.onCreateOptionsMenu(menu, inflater) inflater.inflate(R.menu.fragment_sim_toolkit, menu) menu.findItem(R.id.open_sim_toolkit).isVisible = - SIMToolkit.getComponentName(requireContext(), slotId) != null + SIMToolkit.isAvailable(requireContext(), slotId) } override fun onOptionsItemSelected(item: MenuItem): Boolean = 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 index b6aed0f..6fcc6c0 100644 --- a/app-unpriv/src/main/java/im/angry/openeuicc/util/SIMToolkit.kt +++ b/app-unpriv/src/main/java/im/angry/openeuicc/util/SIMToolkit.kt @@ -6,6 +6,11 @@ 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"), ) @@ -14,7 +19,18 @@ object SIMToolkit { ComponentName("com.android.stk", "com.android.stk.StkMain2"), ) - fun getComponentName(context: Context, slotId: Int): ComponentName? { + 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 @@ -30,9 +46,18 @@ object SIMToolkit { } } - fun intent(context: Context, slotId: Int) = Intent(Intent.ACTION_MAIN, null).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK - component = getComponentName(context, slotId) - addCategory(Intent.CATEGORY_LAUNCHER) + 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 } }