From 49af0ffee9719497fa439fcde6fa81738a1bbebc Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Mon, 4 Mar 2024 18:23:53 -0500 Subject: [PATCH 01/10] CompatibilityCheck: Return FAILURE_UNKNOWN when no SIM readers are found --- .../java/im/angry/openeuicc/util/CompatibilityCheck.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app-unpriv/src/main/java/im/angry/openeuicc/util/CompatibilityCheck.kt b/app-unpriv/src/main/java/im/angry/openeuicc/util/CompatibilityCheck.kt index 3d9c47b..843c487 100644 --- a/app-unpriv/src/main/java/im/angry/openeuicc/util/CompatibilityCheck.kt +++ b/app-unpriv/src/main/java/im/angry/openeuicc/util/CompatibilityCheck.kt @@ -132,7 +132,13 @@ internal class IsdrChannelAccessCheck(private val context: Context): Compatibili override suspend fun doCheck(): State { val seService = connectSEService(context) - val (validSlotIds, result) = seService.readers.filter { it.isSIM }.map { + val readers = seService.readers.filter { it.isSIM } + if (readers.isEmpty()) { + failureDescription = context.getString(R.string.compatibility_check_isdr_channel_desc_unknown) + return State.FAILURE_UNKNOWN + } + + val (validSlotIds, result) = readers.map { try { it.openSession().openLogicalChannel(ISDR_AID)?.close() Pair(it.slotIndex, State.SUCCESS) From a101ae6805340053c9d783b2a886965070bf34fb Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Mon, 4 Mar 2024 18:38:01 -0500 Subject: [PATCH 02/10] CompatibilityCheck: Improve OMAPI connectivity check Stop failing the test if only some slots can be seen. Display a text warning users of that, but don't appear as a failure. --- .../main/java/im/angry/openeuicc/util/CompatibilityCheck.kt | 6 ++++-- app-unpriv/src/main/res/values/strings.xml | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app-unpriv/src/main/java/im/angry/openeuicc/util/CompatibilityCheck.kt b/app-unpriv/src/main/java/im/angry/openeuicc/util/CompatibilityCheck.kt index 843c487..93d49be 100644 --- a/app-unpriv/src/main/java/im/angry/openeuicc/util/CompatibilityCheck.kt +++ b/app-unpriv/src/main/java/im/angry/openeuicc/util/CompatibilityCheck.kt @@ -47,11 +47,13 @@ abstract class CompatibilityCheck(context: Context) { abstract val title: String protected abstract val defaultDescription: String + protected lateinit var successDescription: String protected lateinit var failureDescription: String val description: String get() = when { (state == State.FAILURE || state == State.FAILURE_UNKNOWN) && this::failureDescription.isInitialized -> failureDescription + state == State.SUCCESS && this::successDescription.isInitialized -> successDescription else -> defaultDescription } @@ -111,9 +113,9 @@ internal class OmapiConnCheck(private val context: Context): CompatibilityCheck( failureDescription = context.getString(R.string.compatibility_check_omapi_connectivity_fail) return State.FAILURE } else if (simReaders.size < tm.activeModemCountCompat) { - failureDescription = context.getString(R.string.compatibility_check_omapi_connectivity_fail_sim_number, + successDescription = context.getString(R.string.compatibility_check_omapi_connectivity_partial_success_sim_number, simReaders.map { it.slotIndex }.joinToString(", ")) - return State.FAILURE + return State.SUCCESS } return State.SUCCESS diff --git a/app-unpriv/src/main/res/values/strings.xml b/app-unpriv/src/main/res/values/strings.xml index 3a20af1..8845a58 100644 --- a/app-unpriv/src/main/res/values/strings.xml +++ b/app-unpriv/src/main/res/values/strings.xml @@ -9,8 +9,8 @@ Your device has no support for accessing SIM cards via OMAPI. OMAPI Connectivity Does your device allow access to Secure Elements on SIM cards via OMAPI? - Unable to detect Secure Element readers for SIM cards via OMAPI. - Only the following SIM slots are accessible via OMAPI: %s. + Unable to detect Secure Element readers for SIM cards via OMAPI. If you have not inserted a SIM in this device, try inserting one and retry this check. + Successfully detected Secure Element access, but only for the following SIM slots: %s. ISD-R Channel Access Does your device support opening an ISD-R (management) channel to eSIMs via OMAPI? Cannot determine whether ISD-R access through OMAPI is supported. You might want to retry with SIM cards inserted (any SIM card will do) if not already. From aef399dad013b53aaaa943cf01a9ab7038e11568 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Mon, 4 Mar 2024 18:39:40 -0500 Subject: [PATCH 03/10] CompatibilityCheck: Explain that the user might want to contact the ROM developer --- app-unpriv/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app-unpriv/src/main/res/values/strings.xml b/app-unpriv/src/main/res/values/strings.xml index 8845a58..588c073 100644 --- a/app-unpriv/src/main/res/values/strings.xml +++ b/app-unpriv/src/main/res/values/strings.xml @@ -6,7 +6,7 @@ System Features Whether your device has all the required features for managing removable eUICC cards. For example, basic telephony and OMAPI support. Your device has no telephony features. - Your device has no support for accessing SIM cards via OMAPI. + Your device has no support for accessing SIM cards via OMAPI. If you are using a custom ROM, consider contacting the developer to determine whether it is due to hardware or a missing feature declaration in the OS. OMAPI Connectivity Does your device allow access to Secure Elements on SIM cards via OMAPI? Unable to detect Secure Element readers for SIM cards via OMAPI. If you have not inserted a SIM in this device, try inserting one and retry this check. From 6356601467f768a5957ecc0c9480b875efb753d4 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Mon, 4 Mar 2024 18:43:37 -0500 Subject: [PATCH 04/10] CompatibilityCheck: Make connectivity fail a "Unknown" failure --- .../src/main/java/im/angry/openeuicc/util/CompatibilityCheck.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app-unpriv/src/main/java/im/angry/openeuicc/util/CompatibilityCheck.kt b/app-unpriv/src/main/java/im/angry/openeuicc/util/CompatibilityCheck.kt index 93d49be..ae5a4da 100644 --- a/app-unpriv/src/main/java/im/angry/openeuicc/util/CompatibilityCheck.kt +++ b/app-unpriv/src/main/java/im/angry/openeuicc/util/CompatibilityCheck.kt @@ -111,7 +111,7 @@ internal class OmapiConnCheck(private val context: Context): CompatibilityCheck( val simReaders = seService.readers.filter { it.isSIM } if (simReaders.isEmpty()) { failureDescription = context.getString(R.string.compatibility_check_omapi_connectivity_fail) - return State.FAILURE + return State.FAILURE_UNKNOWN } else if (simReaders.size < tm.activeModemCountCompat) { successDescription = context.getString(R.string.compatibility_check_omapi_connectivity_partial_success_sim_number, simReaders.map { it.slotIndex }.joinToString(", ")) From 4dd14d23f24d6dd7694571970c56cb98499bd85a Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Mon, 4 Mar 2024 18:51:10 -0500 Subject: [PATCH 05/10] refactor: Add UiComponentFactory to manual DI --- .../main/java/im/angry/openeuicc/di/AppContainer.kt | 1 + .../java/im/angry/openeuicc/di/DefaultAppContainer.kt | 4 ++++ .../im/angry/openeuicc/di/DefaultUiComponentFactory.kt | 9 +++++++++ .../java/im/angry/openeuicc/di/UiComponentFactory.kt | 8 ++++++++ .../main/java/im/angry/openeuicc/ui/MainActivity.kt | 6 +----- .../im/angry/openeuicc/di/PrivilegedAppContainer.kt | 4 ++++ .../angry/openeuicc/di/PrivilegedUiComponentFactory.kt | 10 ++++++++++ .../im/angry/openeuicc/ui/PrivilegedMainActivity.kt | 4 ---- 8 files changed, 37 insertions(+), 9 deletions(-) create mode 100644 app-common/src/main/java/im/angry/openeuicc/di/DefaultUiComponentFactory.kt create mode 100644 app-common/src/main/java/im/angry/openeuicc/di/UiComponentFactory.kt create mode 100644 app/src/main/java/im/angry/openeuicc/di/PrivilegedUiComponentFactory.kt diff --git a/app-common/src/main/java/im/angry/openeuicc/di/AppContainer.kt b/app-common/src/main/java/im/angry/openeuicc/di/AppContainer.kt index 65c2aa9..a29fcf8 100644 --- a/app-common/src/main/java/im/angry/openeuicc/di/AppContainer.kt +++ b/app-common/src/main/java/im/angry/openeuicc/di/AppContainer.kt @@ -10,4 +10,5 @@ interface AppContainer { val euiccChannelManager: IEuiccChannelManager val subscriptionManager: SubscriptionManager val preferenceRepository: PreferenceRepository + val uiComponentFactory: UiComponentFactory } \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/di/DefaultAppContainer.kt b/app-common/src/main/java/im/angry/openeuicc/di/DefaultAppContainer.kt index dd7826c..6a03835 100644 --- a/app-common/src/main/java/im/angry/openeuicc/di/DefaultAppContainer.kt +++ b/app-common/src/main/java/im/angry/openeuicc/di/DefaultAppContainer.kt @@ -23,4 +23,8 @@ open class DefaultAppContainer(context: Context) : AppContainer { override val preferenceRepository by lazy { PreferenceRepository(context) } + + override val uiComponentFactory by lazy { + DefaultUiComponentFactory() + } } \ No newline at end of file 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 new file mode 100644 index 0000000..86af007 --- /dev/null +++ b/app-common/src/main/java/im/angry/openeuicc/di/DefaultUiComponentFactory.kt @@ -0,0 +1,9 @@ +package im.angry.openeuicc.di + +import im.angry.openeuicc.core.EuiccChannel +import im.angry.openeuicc.ui.EuiccManagementFragment + +open class DefaultUiComponentFactory : UiComponentFactory { + override fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment = + EuiccManagementFragment.newInstance(channel.slotId, channel.portId) +} \ 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 new file mode 100644 index 0000000..d311876 --- /dev/null +++ b/app-common/src/main/java/im/angry/openeuicc/di/UiComponentFactory.kt @@ -0,0 +1,8 @@ +package im.angry.openeuicc.di + +import im.angry.openeuicc.core.EuiccChannel +import im.angry.openeuicc.ui.EuiccManagementFragment + +interface UiComponentFactory { + fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment +} \ 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 af6cbf4..ca6715d 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 @@ -88,10 +88,6 @@ open class MainActivity : AppCompatActivity(), OpenEuiccContextMarker { else -> super.onOptionsItemSelected(item) } - - protected open fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment = - EuiccManagementFragment.newInstance(channel.slotId, channel.portId) - private suspend fun init() { withContext(Dispatchers.IO) { euiccChannelManager.enumerateEuiccChannels() @@ -108,7 +104,7 @@ open class MainActivity : AppCompatActivity(), OpenEuiccContextMarker { withContext(Dispatchers.Main) { euiccChannelManager.knownChannels.sortedBy { it.logicalSlotId }.forEach { channel -> spinnerAdapter.add(getString(R.string.channel_name_format, channel.logicalSlotId)) - fragments.add(createEuiccManagementFragment(channel)) + fragments.add(appContainer.uiComponentFactory.createEuiccManagementFragment(channel)) } if (fragments.isNotEmpty()) { diff --git a/app/src/main/java/im/angry/openeuicc/di/PrivilegedAppContainer.kt b/app/src/main/java/im/angry/openeuicc/di/PrivilegedAppContainer.kt index a849a27..64e45eb 100644 --- a/app/src/main/java/im/angry/openeuicc/di/PrivilegedAppContainer.kt +++ b/app/src/main/java/im/angry/openeuicc/di/PrivilegedAppContainer.kt @@ -8,4 +8,8 @@ class PrivilegedAppContainer(context: Context) : DefaultAppContainer(context) { override val euiccChannelManager: IEuiccChannelManager by lazy { PrivilegedEuiccChannelManager(context) } + + override val uiComponentFactory by lazy { + PrivilegedUiComponentFactory() + } } \ No newline at end of file diff --git a/app/src/main/java/im/angry/openeuicc/di/PrivilegedUiComponentFactory.kt b/app/src/main/java/im/angry/openeuicc/di/PrivilegedUiComponentFactory.kt new file mode 100644 index 0000000..d3c5cdb --- /dev/null +++ b/app/src/main/java/im/angry/openeuicc/di/PrivilegedUiComponentFactory.kt @@ -0,0 +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(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/ui/PrivilegedMainActivity.kt b/app/src/main/java/im/angry/openeuicc/ui/PrivilegedMainActivity.kt index 1261932..f3c2b3c 100644 --- a/app/src/main/java/im/angry/openeuicc/ui/PrivilegedMainActivity.kt +++ b/app/src/main/java/im/angry/openeuicc/ui/PrivilegedMainActivity.kt @@ -4,7 +4,6 @@ import android.view.Menu import android.view.MenuItem import android.widget.Toast import im.angry.openeuicc.R -import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.util.* class PrivilegedMainActivity : MainActivity() { @@ -37,7 +36,4 @@ class PrivilegedMainActivity : MainActivity() { } else -> super.onOptionsItemSelected(item) } - - override fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment = - PrivilegedEuiccManagementFragment.newInstance(channel.slotId, channel.portId) } \ No newline at end of file From 7c6b4ebee5cb9dc4690b2336bb8931f63e7820ca Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Mon, 4 Mar 2024 19:06:05 -0500 Subject: [PATCH 06/10] refactor: IEuiccChannelManager -> EuiccChannelManager --- .../core/DefaultEuiccChannelManager.kt | 174 +++++++++++++++ .../openeuicc/core/EuiccChannelManager.kt | 199 ++++-------------- .../openeuicc/core/IEuiccChannelManager.kt | 47 ----- .../im/angry/openeuicc/di/AppContainer.kt | 4 +- .../angry/openeuicc/di/DefaultAppContainer.kt | 6 +- .../java/im/angry/openeuicc/util/Utils.kt | 4 +- .../core/PrivilegedEuiccChannelManager.kt | 2 +- .../openeuicc/di/PrivilegedAppContainer.kt | 4 +- .../util/PrivilegedTelephonyUtils.kt | 6 +- 9 files changed, 223 insertions(+), 223 deletions(-) create mode 100644 app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt delete mode 100644 app-common/src/main/java/im/angry/openeuicc/core/IEuiccChannelManager.kt 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 new file mode 100644 index 0000000..1883a11 --- /dev/null +++ b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt @@ -0,0 +1,174 @@ +package im.angry.openeuicc.core + +import android.content.Context +import android.se.omapi.SEService +import android.telephony.SubscriptionManager +import android.util.Log +import im.angry.openeuicc.OpenEuiccApplication +import im.angry.openeuicc.util.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import java.lang.IllegalArgumentException + +open class DefaultEuiccChannelManager(protected val context: Context) : EuiccChannelManager { + companion object { + const val TAG = "EuiccChannelManager" + } + + private val channels = mutableListOf() + + private var seService: SEService? = null + + private val lock = Mutex() + + protected val tm by lazy { + (context.applicationContext as OpenEuiccApplication).appContainer.telephonyManager + } + + protected open val uiccCards: Collection + get() = (0..? = + runBlocking { + for (card in uiccCards) { + if (card.physicalSlotIndex != physicalSlotId) continue + return@runBlocking card.ports.mapNotNull { tryOpenEuiccChannel(it) } + .ifEmpty { null } + } + return@runBlocking null + } + + override fun findEuiccChannelByPortBlocking(physicalSlotId: Int, portId: Int): EuiccChannel? = + runBlocking { + withContext(Dispatchers.IO) { + uiccCards.find { it.physicalSlotIndex == physicalSlotId }?.let { card -> + card.ports.find { it.portIndex == portId }?.let { tryOpenEuiccChannel(it) } + } + } + } + + override suspend fun enumerateEuiccChannels() { + withContext(Dispatchers.IO) { + ensureSEService() + + for (uiccInfo in uiccCards) { + for (port in uiccInfo.ports) { + if (tryOpenEuiccChannel(port) != null) { + Log.d( + TAG, + "Found eUICC on slot ${uiccInfo.physicalSlotIndex} port ${port.portIndex}" + ) + } + } + } + } + } + + override val knownChannels: List + get() = channels.toList() + + override fun invalidate() { + for (channel in channels) { + channel.close() + } + + channels.clear() + seService?.shutdown() + seService = null + } +} \ No newline at end of file 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 9033d34..b0bce1d 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 @@ -1,174 +1,47 @@ package im.angry.openeuicc.core -import android.content.Context -import android.se.omapi.SEService -import android.telephony.SubscriptionManager -import android.util.Log -import im.angry.openeuicc.OpenEuiccApplication -import im.angry.openeuicc.util.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext -import java.lang.IllegalArgumentException +interface EuiccChannelManager { + val knownChannels: List -open class EuiccChannelManager(protected val context: Context) : IEuiccChannelManager { - companion object { - const val TAG = "EuiccChannelManager" - } + /** + * Scan all possible sources for EuiccChannels and have them cached for future use + */ + suspend fun enumerateEuiccChannels() - private val channels = mutableListOf() + /** + * Returns the EuiccChannel corresponding to a **logical** slot + */ + fun findEuiccChannelBySlotBlocking(logicalSlotId: Int): EuiccChannel? - private var seService: SEService? = null + /** + * Returns the first EuiccChannel corresponding to a **physical** slot + * If the physical slot supports MEP and has multiple ports, it is undefined + * which of the two channels will be returned. + */ + fun findEuiccChannelByPhysicalSlotBlocking(physicalSlotId: Int): EuiccChannel? - private val lock = Mutex() + /** + * Returns all EuiccChannels corresponding to a **physical** slot + * Multiple channels are possible in the case of MEP + */ + fun findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId: Int): List? - protected val tm by lazy { - (context.applicationContext as OpenEuiccApplication).appContainer.telephonyManager - } + /** + * Returns the EuiccChannel corresponding to a **physical** slot and a port ID + */ + fun findEuiccChannelByPortBlocking(physicalSlotId: Int, portId: Int): EuiccChannel? - protected open val uiccCards: Collection - get() = (0..? = - runBlocking { - for (card in uiccCards) { - if (card.physicalSlotIndex != physicalSlotId) continue - return@runBlocking card.ports.mapNotNull { tryOpenEuiccChannel(it) } - .ifEmpty { null } - } - return@runBlocking null - } - - override fun findEuiccChannelByPortBlocking(physicalSlotId: Int, portId: Int): EuiccChannel? = - runBlocking { - withContext(Dispatchers.IO) { - uiccCards.find { it.physicalSlotIndex == physicalSlotId }?.let { card -> - card.ports.find { it.portIndex == portId }?.let { tryOpenEuiccChannel(it) } - } - } - } - - override suspend fun enumerateEuiccChannels() { - withContext(Dispatchers.IO) { - ensureSEService() - - for (uiccInfo in uiccCards) { - for (port in uiccInfo.ports) { - if (tryOpenEuiccChannel(port) != null) { - Log.d( - TAG, - "Found eUICC on slot ${uiccInfo.physicalSlotIndex} port ${port.portIndex}" - ) - } - } - } - } - } - - override val knownChannels: List - get() = channels.toList() - - override fun invalidate() { - for (channel in channels) { - channel.close() - } - - channels.clear() - seService?.shutdown() - seService = null + /** + * If possible, trigger the system to update the cached list of profiles + * This is only expected to be implemented when the application is privileged + * TODO: Remove this from the common interface + */ + fun notifyEuiccProfilesChanged(logicalSlotId: Int) { + // no-op by default } } \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/core/IEuiccChannelManager.kt b/app-common/src/main/java/im/angry/openeuicc/core/IEuiccChannelManager.kt deleted file mode 100644 index 8a6ab54..0000000 --- a/app-common/src/main/java/im/angry/openeuicc/core/IEuiccChannelManager.kt +++ /dev/null @@ -1,47 +0,0 @@ -package im.angry.openeuicc.core - -interface IEuiccChannelManager { - val knownChannels: List - - /** - * Scan all possible sources for EuiccChannels and have them cached for future use - */ - suspend fun enumerateEuiccChannels() - - /** - * Returns the EuiccChannel corresponding to a **logical** slot - */ - fun findEuiccChannelBySlotBlocking(logicalSlotId: Int): EuiccChannel? - - /** - * Returns the first EuiccChannel corresponding to a **physical** slot - * If the physical slot supports MEP and has multiple ports, it is undefined - * which of the two channels will be returned. - */ - fun findEuiccChannelByPhysicalSlotBlocking(physicalSlotId: Int): EuiccChannel? - - /** - * Returns all EuiccChannels corresponding to a **physical** slot - * Multiple channels are possible in the case of MEP - */ - fun findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId: Int): List? - - /** - * Returns the EuiccChannel corresponding to a **physical** slot and a port ID - */ - fun findEuiccChannelByPortBlocking(physicalSlotId: Int, portId: Int): EuiccChannel? - - /** - * Invalidate all EuiccChannels previously known by this Manager - */ - fun invalidate() - - /** - * If possible, trigger the system to update the cached list of profiles - * This is only expected to be implemented when the application is privileged - * TODO: Remove this from the common interface - */ - fun notifyEuiccProfilesChanged(logicalSlotId: Int) { - // no-op by default - } -} \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/di/AppContainer.kt b/app-common/src/main/java/im/angry/openeuicc/di/AppContainer.kt index a29fcf8..a62eff4 100644 --- a/app-common/src/main/java/im/angry/openeuicc/di/AppContainer.kt +++ b/app-common/src/main/java/im/angry/openeuicc/di/AppContainer.kt @@ -2,12 +2,12 @@ package im.angry.openeuicc.di import android.telephony.SubscriptionManager import android.telephony.TelephonyManager -import im.angry.openeuicc.core.IEuiccChannelManager +import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.util.* interface AppContainer { val telephonyManager: TelephonyManager - val euiccChannelManager: IEuiccChannelManager + val euiccChannelManager: EuiccChannelManager val subscriptionManager: SubscriptionManager val preferenceRepository: PreferenceRepository val uiComponentFactory: UiComponentFactory diff --git a/app-common/src/main/java/im/angry/openeuicc/di/DefaultAppContainer.kt b/app-common/src/main/java/im/angry/openeuicc/di/DefaultAppContainer.kt index 6a03835..9f051e8 100644 --- a/app-common/src/main/java/im/angry/openeuicc/di/DefaultAppContainer.kt +++ b/app-common/src/main/java/im/angry/openeuicc/di/DefaultAppContainer.kt @@ -3,8 +3,8 @@ package im.angry.openeuicc.di import android.content.Context import android.telephony.SubscriptionManager import android.telephony.TelephonyManager +import im.angry.openeuicc.core.DefaultEuiccChannelManager import im.angry.openeuicc.core.EuiccChannelManager -import im.angry.openeuicc.core.IEuiccChannelManager import im.angry.openeuicc.util.* open class DefaultAppContainer(context: Context) : AppContainer { @@ -12,8 +12,8 @@ open class DefaultAppContainer(context: Context) : AppContainer { context.getSystemService(TelephonyManager::class.java)!! } - override val euiccChannelManager: IEuiccChannelManager by lazy { - EuiccChannelManager(context) + override val euiccChannelManager: EuiccChannelManager by lazy { + DefaultEuiccChannelManager(context) } override val subscriptionManager by lazy { diff --git a/app-common/src/main/java/im/angry/openeuicc/util/Utils.kt b/app-common/src/main/java/im/angry/openeuicc/util/Utils.kt index 57626e2..33699d7 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/Utils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/Utils.kt @@ -7,7 +7,7 @@ import android.telephony.TelephonyManager import androidx.fragment.app.Fragment import im.angry.openeuicc.OpenEuiccApplication import im.angry.openeuicc.core.EuiccChannel -import im.angry.openeuicc.core.IEuiccChannelManager +import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.di.AppContainer import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking @@ -52,7 +52,7 @@ interface OpenEuiccContextMarker { val appContainer: AppContainer get() = openEuiccApplication.appContainer - val euiccChannelManager: IEuiccChannelManager + val euiccChannelManager: EuiccChannelManager get() = appContainer.euiccChannelManager val telephonyManager: TelephonyManager diff --git a/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelManager.kt b/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelManager.kt index 75b2b22..0ebaa81 100644 --- a/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelManager.kt +++ b/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelManager.kt @@ -7,7 +7,7 @@ import im.angry.openeuicc.util.* import java.lang.Exception import java.lang.IllegalArgumentException -class PrivilegedEuiccChannelManager(context: Context): EuiccChannelManager(context) { +class PrivilegedEuiccChannelManager(context: Context): DefaultEuiccChannelManager(context) { override val uiccCards: Collection get() = tm.uiccCardsInfoCompat diff --git a/app/src/main/java/im/angry/openeuicc/di/PrivilegedAppContainer.kt b/app/src/main/java/im/angry/openeuicc/di/PrivilegedAppContainer.kt index 64e45eb..dc3921e 100644 --- a/app/src/main/java/im/angry/openeuicc/di/PrivilegedAppContainer.kt +++ b/app/src/main/java/im/angry/openeuicc/di/PrivilegedAppContainer.kt @@ -1,11 +1,11 @@ package im.angry.openeuicc.di import android.content.Context -import im.angry.openeuicc.core.IEuiccChannelManager +import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.core.PrivilegedEuiccChannelManager class PrivilegedAppContainer(context: Context) : DefaultAppContainer(context) { - override val euiccChannelManager: IEuiccChannelManager by lazy { + override val euiccChannelManager: EuiccChannelManager by lazy { PrivilegedEuiccChannelManager(context) } 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 4cb4932..4675ab9 100644 --- a/app/src/main/java/im/angry/openeuicc/util/PrivilegedTelephonyUtils.kt +++ b/app/src/main/java/im/angry/openeuicc/util/PrivilegedTelephonyUtils.kt @@ -4,7 +4,7 @@ import android.telephony.SubscriptionManager import android.telephony.TelephonyManager import android.telephony.UiccSlotMapping import im.angry.openeuicc.core.EuiccChannel -import im.angry.openeuicc.core.IEuiccChannelManager +import im.angry.openeuicc.core.EuiccChannelManager import kotlinx.coroutines.runBlocking import java.lang.Exception @@ -14,7 +14,7 @@ val TelephonyManager.supportsDSDS: Boolean val TelephonyManager.dsdsEnabled: Boolean get() = activeModemCount >= 2 -fun TelephonyManager.setDsdsEnabled(euiccManager: IEuiccChannelManager, enabled: Boolean) { +fun TelephonyManager.setDsdsEnabled(euiccManager: EuiccChannelManager, enabled: Boolean) { runBlocking { euiccManager.enumerateEuiccChannels() } @@ -32,7 +32,7 @@ fun TelephonyManager.setDsdsEnabled(euiccManager: IEuiccChannelManager, enabled: // Disable eSIM profiles before switching the slot mapping // This ensures that unmapped eSIM ports never have "ghost" profiles enabled fun TelephonyManager.updateSimSlotMapping( - euiccManager: IEuiccChannelManager, newMapping: Collection, + euiccManager: EuiccChannelManager, newMapping: Collection, currentMapping: Collection = simSlotMapping ) { val unmapped = currentMapping.filterNot { mapping -> From 1a69c5294b56912ce71516f3b6c8be4c490d747b Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Mon, 4 Mar 2024 19:30:04 -0500 Subject: [PATCH 07/10] refactor: Use DI techniques for EuiccChannel's as well --- .../core/DefaultEuiccChannelFactory.kt | 43 ++++++++++++ .../core/DefaultEuiccChannelManager.kt | 65 ++++--------------- .../openeuicc/core/EuiccChannelFactory.kt | 16 +++++ .../im/angry/openeuicc/di/AppContainer.kt | 2 + .../angry/openeuicc/di/DefaultAppContainer.kt | 7 +- .../core/PrivilegedEuiccChannelFactory.kt | 43 ++++++++++++ .../core/PrivilegedEuiccChannelManager.kt | 30 ++------- .../openeuicc/di/PrivilegedAppContainer.kt | 7 +- 8 files changed, 133 insertions(+), 80 deletions(-) create mode 100644 app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt create mode 100644 app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelFactory.kt create mode 100644 app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt diff --git a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt new file mode 100644 index 0000000..15e0191 --- /dev/null +++ b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt @@ -0,0 +1,43 @@ +package im.angry.openeuicc.core + +import android.content.Context +import android.se.omapi.SEService +import android.util.Log +import im.angry.openeuicc.util.* +import java.lang.IllegalArgumentException + +open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccChannelFactory { + private var seService: SEService? = null + + private suspend fun ensureSEService() { + if (seService == null) { + seService = connectSEService(context) + } + } + + override suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel? { + if (port.portIndex != 0) { + Log.w(DefaultEuiccChannelManager.TAG, "OMAPI channel attempted on non-zero portId, this may or may not work.") + } + + ensureSEService() + + Log.i(DefaultEuiccChannelManager.TAG, "Trying OMAPI for physical slot ${port.card.physicalSlotIndex}") + try { + return OmapiChannel(seService!!, port) + } catch (e: IllegalArgumentException) { + // Failed + Log.w( + DefaultEuiccChannelManager.TAG, + "OMAPI APDU interface unavailable for physical slot ${port.card.physicalSlotIndex}." + ) + } + + return null + } + + override fun cleanup() { + seService?.shutdown() + seService = null + } +} \ No newline at end of file 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 1883a11..a978677 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 @@ -1,69 +1,41 @@ package im.angry.openeuicc.core import android.content.Context -import android.se.omapi.SEService import android.telephony.SubscriptionManager import android.util.Log -import im.angry.openeuicc.OpenEuiccApplication +import im.angry.openeuicc.di.AppContainer import im.angry.openeuicc.util.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext -import java.lang.IllegalArgumentException -open class DefaultEuiccChannelManager(protected val context: Context) : EuiccChannelManager { +open class DefaultEuiccChannelManager( + protected val appContainer: AppContainer, + protected val context: Context +) : EuiccChannelManager { companion object { const val TAG = "EuiccChannelManager" } private val channels = mutableListOf() - private var seService: SEService? = null - private val lock = Mutex() protected val tm by lazy { - (context.applicationContext as OpenEuiccApplication).appContainer.telephonyManager + appContainer.telephonyManager + } + + private val euiccChannelFactory by lazy { + appContainer.euiccChannelFactory } protected open val uiccCards: Collection get() = (0.. get() = tm.uiccCardsInfoCompat - @Suppress("NAME_SHADOWING") - override fun tryOpenEuiccChannelPrivileged(port: UiccPortInfoCompat): EuiccChannel? { - val port = port as RealUiccPortInfoCompat - if (port.card.isRemovable) { - // Attempt unprivileged (OMAPI) before TelephonyManager - // but still try TelephonyManager in case OMAPI is broken - super.tryOpenEuiccChannelUnprivileged(port)?.let { return it } - } - - if (port.card.isEuicc) { - Log.i(TAG, "Trying TelephonyManager for slot ${port.card.physicalSlotIndex} port ${port.portIndex}") - try { - return TelephonyManagerChannel(port, tm) - } catch (e: IllegalArgumentException) { - // Failed - Log.w(TAG, "TelephonyManager APDU interface unavailable for slot ${port.card.physicalSlotIndex} port ${port.portIndex}, falling back") - } - } - return null - } - // Clean up channels left open in TelephonyManager // due to a (potentially) forced restart // This should be called every time the application is restarted @@ -48,7 +26,7 @@ class PrivilegedEuiccChannelManager(context: Context): DefaultEuiccChannelManage } override fun notifyEuiccProfilesChanged(logicalSlotId: Int) { - (context.applicationContext as OpenEuiccApplication).appContainer.subscriptionManager.apply { + appContainer.subscriptionManager.apply { findEuiccChannelBySlotBlocking(logicalSlotId)?.let { tryRefreshCachedEuiccInfo(it.cardId) } diff --git a/app/src/main/java/im/angry/openeuicc/di/PrivilegedAppContainer.kt b/app/src/main/java/im/angry/openeuicc/di/PrivilegedAppContainer.kt index dc3921e..5158352 100644 --- a/app/src/main/java/im/angry/openeuicc/di/PrivilegedAppContainer.kt +++ b/app/src/main/java/im/angry/openeuicc/di/PrivilegedAppContainer.kt @@ -2,14 +2,19 @@ package im.angry.openeuicc.di import android.content.Context import im.angry.openeuicc.core.EuiccChannelManager +import im.angry.openeuicc.core.PrivilegedEuiccChannelFactory import im.angry.openeuicc.core.PrivilegedEuiccChannelManager class PrivilegedAppContainer(context: Context) : DefaultAppContainer(context) { override val euiccChannelManager: EuiccChannelManager by lazy { - PrivilegedEuiccChannelManager(context) + PrivilegedEuiccChannelManager(this, context) } override val uiComponentFactory by lazy { PrivilegedUiComponentFactory() } + + override val euiccChannelFactory by lazy { + PrivilegedEuiccChannelFactory(context) + } } \ No newline at end of file From 6c2b1675bd5f4ae3562d067d3a5d44cc9d507b44 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Mon, 4 Mar 2024 19:35:17 -0500 Subject: [PATCH 08/10] fixup: Infinite loop in PreferenceUtils after adopting DI --- .../src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt index accbbb5..b9a854e 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt @@ -14,7 +14,7 @@ import kotlinx.coroutines.flow.map private val Context.dataStore: DataStore by preferencesDataStore(name = "prefs") val Context.preferenceRepository: PreferenceRepository - get() = (applicationContext as OpenEuiccApplication).preferenceRepository + get() = (applicationContext as OpenEuiccApplication).appContainer.preferenceRepository val Fragment.preferenceRepository: PreferenceRepository get() = requireContext().preferenceRepository From 09e19412e31330261dc1c112e1a6dac34ac9aaf6 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Mon, 4 Mar 2024 19:59:10 -0500 Subject: [PATCH 09/10] fix: Show less logs in UI than what we will save ...to avoid the UI getting stuck due to the sheer amount of lines. --- .../main/java/im/angry/openeuicc/ui/LogsActivity.kt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/LogsActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/LogsActivity.kt index 1a7e50f..70f6992 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/LogsActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/LogsActivity.kt @@ -23,13 +23,15 @@ class LogsActivity : AppCompatActivity() { private lateinit var swipeRefresh: SwipeRefreshLayout private lateinit var scrollView: ScrollView private lateinit var logText: TextView + private lateinit var logStr: String private val saveLogs = registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri -> if (uri == null) return@registerForActivityResult + if (!this::logStr.isInitialized) return@registerForActivityResult contentResolver.openFileDescriptor(uri, "w")?.use { FileOutputStream(it.fileDescriptor).use { os -> - os.write(logText.text.toString().encodeToByteArray()) + os.write(logStr.encodeToByteArray()) } } } @@ -76,7 +78,12 @@ class LogsActivity : AppCompatActivity() { private suspend fun reload() = withContext(Dispatchers.Main) { swipeRefresh.isRefreshing = true - logText.text = intent.extras?.getString("log") ?: readSelfLog() + logStr = intent.extras?.getString("log") ?: readSelfLog() + + logText.text = withContext(Dispatchers.IO) { + // Limit the UI to display only 256 lines + logStr.lines().takeLast(256).joinToString("\n") + } swipeRefresh.isRefreshing = false From ca0085e14733906ad20474b8246633b91df272b3 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Mon, 4 Mar 2024 20:07:26 -0500 Subject: [PATCH 10/10] fix: Do not crash if a certificate key ID is not known Fixes #17. --- .../main/java/net/typeblog/lpac_jni/impl/RootCertificates.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/RootCertificates.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/RootCertificates.kt index f3e5e90..13848b6 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/RootCertificates.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/RootCertificates.kt @@ -8,7 +8,7 @@ import java.security.cert.CertificateFactory const val DEFAULT_PKID_GSMA_RSP2_ROOT_CI1 = "81370f5125d0b1d408d4c3b232e6d25e795bebfb" private fun getCertificate(keyId: String): Certificate? = - KNOWN_CI_CERTS[keyId]?.toByteArray().let { cert -> + KNOWN_CI_CERTS[keyId]?.toByteArray()?.let { cert -> ByteArrayInputStream(cert).use { stream -> val cf = CertificateFactory.getInstance("X.509") cf.generateCertificate(stream)