From d9d0cf2e7535b1e840e074d7127fb2421cf51781 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Wed, 20 Mar 2024 20:03:45 -0400 Subject: [PATCH 01/11] CompatibilityCheck: Show unknown status if OMAPI feature flag is not found --- .../src/main/java/im/angry/openeuicc/util/CompatibilityCheck.kt | 2 +- app-unpriv/src/main/res/values/strings.xml | 2 +- 2 files changed, 2 insertions(+), 2 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 ae5a4da8..83c1c5d9 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 @@ -87,7 +87,7 @@ internal class HasSystemFeaturesCheck(private val context: Context): Compatibili PackageManager.FEATURE_SE_OMAPI_UICC )) { failureDescription = context.getString(R.string.compatibility_check_system_features_no_omapi) - return State.FAILURE + return State.FAILURE_UNKNOWN } return State.SUCCESS diff --git a/app-unpriv/src/main/res/values/strings.xml b/app-unpriv/src/main/res/values/strings.xml index 588c0735..e6641b3a 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. 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. + Your device / system does not declare support for OMAPI. This could be due to missing support from hardware, or it could be simply due to a missing flag. See the following two checks to determine whether OMAPI is actually supported or not. 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 92d8f9079f00a342cd3c8a80ba375f059c82c86d Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Thu, 21 Mar 2024 21:16:14 -0400 Subject: [PATCH 02/11] EuiccManagementFragment: Show alert dialog if timed out waiting for SIM --- .../angry/openeuicc/ui/EuiccManagementFragment.kt | 15 +++++++++++++++ app-common/src/main/res/values/strings.xml | 2 ++ .../lpac_jni/impl/LocalProfileAssistantImpl.kt | 12 ++---------- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt index 7564cc88..2e162b56 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt @@ -16,6 +16,7 @@ import android.widget.ImageButton import android.widget.PopupMenu import android.widget.TextView import android.widget.Toast +import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager @@ -26,6 +27,7 @@ import net.typeblog.lpac_jni.LocalProfileInfo import im.angry.openeuicc.common.R import im.angry.openeuicc.util.* import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -138,6 +140,19 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, } refresh() fab.isEnabled = true + } catch (e: TimeoutCancellationException) { + // Timed out waiting for SIM to come back online, we can no longer assume that the LPA is still valid + AlertDialog.Builder(requireContext()).apply { + setMessage(R.string.enable_disable_timeout) + setPositiveButton(android.R.string.ok) { dialog, _ -> + dialog.dismiss() + requireActivity().finish() + } + setOnDismissListener { _ -> + requireActivity().finish() + } + show() + } } catch (e: Exception) { Log.d(TAG, "Failed to enable / disable profile $iccid") Log.d(TAG, Log.getStackTraceString(e)) diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index 52187850..ac6b9d4e 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -16,6 +16,8 @@ Delete Rename + Timed out waiting for the eSIM chip to switch profiles. You might want to restart the application or even the phone. + Cannot switch to new eSIM profile. Nickname cannot be longer than 64 characters diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt index b81a41ad..246ead21 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt @@ -90,11 +90,7 @@ class LocalProfileAssistantImpl( override fun enableProfile(iccid: String, reconnectTimeout: Long): Boolean { val res = LpacJni.es10cEnableProfile(contextHandle, iccid) == 0 if (reconnectTimeout > 0) { - try { - tryReconnect(reconnectTimeout) - } catch (e: Exception) { - return false - } + tryReconnect(reconnectTimeout) } return res } @@ -102,11 +98,7 @@ class LocalProfileAssistantImpl( override fun disableProfile(iccid: String, reconnectTimeout: Long): Boolean { val res = LpacJni.es10cDisableProfile(contextHandle, iccid) == 0 if (reconnectTimeout > 0) { - try { - tryReconnect(reconnectTimeout) - } catch (e: Exception) { - return false - } + tryReconnect(reconnectTimeout) } return res } From 7834e0348a3dcc7ae94ac3dd6ecc61bceafbf841 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Thu, 21 Mar 2024 21:28:26 -0400 Subject: [PATCH 03/11] refactor: Move notification tracking logic to EuiccChannelFragmentUtils --- .../openeuicc/ui/EuiccManagementFragment.kt | 4 +-- .../openeuicc/ui/ProfileDeleteFragment.kt | 2 +- .../openeuicc/ui/ProfileDownloadFragment.kt | 26 ++++++++++++------ .../util/EuiccChannelFragmentUtils.kt | 27 +++++++++++++++++++ .../lpac_jni/LocalProfileAssistant.kt | 26 ------------------ 5 files changed, 48 insertions(+), 37 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt index 2e162b56..98b3d3ac 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt @@ -163,13 +163,13 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, } private suspend fun doEnableProfile(iccid: String) = - channel.lpa.beginOperation { + beginTrackedOperation { channel.lpa.enableProfile(iccid, reconnectTimeout = 15 * 1000) && preferenceRepository.notificationEnableFlow.first() } private suspend fun doDisableProfile(iccid: String) = - channel.lpa.beginOperation { + beginTrackedOperation { channel.lpa.disableProfile(iccid, reconnectTimeout = 15 * 1000) && preferenceRepository.notificationDisableFlow.first() } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt index ede48ea3..75863541 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt @@ -82,7 +82,7 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker { } } - private suspend fun doDelete() = channel.lpa.beginOperation { + private suspend fun doDelete() = beginTrackedOperation { channel.lpa.deleteProfile(requireArguments().getString("iccid")!!) preferenceRepository.notificationDeleteFlow.first() } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt index 8ab09d91..78a54f21 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt @@ -189,15 +189,25 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(), } } - private suspend fun doDownloadProfile(server: String, code: String?, confirmationCode: String?, imei: String?) = channel.lpa.beginOperation { - downloadProfile(server, code, imei, confirmationCode, object : ProfileDownloadCallback { - override fun onStateUpdate(state: ProfileDownloadCallback.DownloadState) { - lifecycleScope.launch(Dispatchers.Main) { - progress.isIndeterminate = false - progress.progress = state.progress + private suspend fun doDownloadProfile( + server: String, + code: String?, + confirmationCode: String?, + imei: String? + ) = beginTrackedOperation { + channel.lpa.downloadProfile( + server, + code, + imei, + confirmationCode, + object : ProfileDownloadCallback { + override fun onStateUpdate(state: ProfileDownloadCallback.DownloadState) { + lifecycleScope.launch(Dispatchers.Main) { + progress.isIndeterminate = false + progress.progress = state.progress + } } - } - }) + }) // If we get here, we are successful // Only send notifications if the user allowed us to diff --git a/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt index 7c984c7c..feee0cdd 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt @@ -1,8 +1,14 @@ package im.angry.openeuicc.util import android.os.Bundle +import android.util.Log import androidx.fragment.app.Fragment import im.angry.openeuicc.core.EuiccChannel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import net.typeblog.lpac_jni.LocalProfileAssistant + +private const val TAG = "EuiccChannelFragmentUtils" interface EuiccChannelFragmentMarker: OpenEuiccContextMarker @@ -28,6 +34,27 @@ val T.channel: EuiccChannel where T: Fragment, T: EuiccChannelFragmentMarker get() = euiccChannelManager.findEuiccChannelByPortBlocking(slotId, portId)!! +/* + * Begin a "tracked" operation where notifications may be generated by the eSIM + * Automatically handle any newly generated notification during the operation + * if the function "op" returns true. + */ +suspend fun T.beginTrackedOperation(op: suspend () -> Boolean) where T : Fragment, T : EuiccChannelFragmentMarker = + withContext(Dispatchers.IO) { + val latestSeq = channel.lpa.notifications.firstOrNull()?.seqNumber ?: 0 + Log.d(TAG, "Latest notification is $latestSeq before operation") + if (op()) { + Log.d(TAG, "Operation has requested notification handling") + // Note that the exact instance of "channel" might have changed here if reconnected; + // so we MUST use the automatic getter for "channel" + channel.lpa.notifications.filter { it.seqNumber > latestSeq }.forEach { + Log.d(TAG, "Handling notification $it") + channel.lpa.handleNotification(it.seqNumber) + } + } + Log.d(TAG, "Operation complete") + } + interface EuiccProfilesChangedListener { fun onEuiccProfilesChanged() } \ No newline at end of file diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt index b40ba10a..fbc853d4 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt @@ -1,14 +1,6 @@ package net.typeblog.lpac_jni -import android.util.Log -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - interface LocalProfileAssistant { - companion object { - private const val TAG = "LocalProfileAssistant" - } - val valid: Boolean get() = try { // If we can read both eID and profiles properly, we are likely looking at @@ -37,24 +29,6 @@ interface LocalProfileAssistant { fun deleteNotification(seqNumber: Long): Boolean fun handleNotification(seqNumber: Long): Boolean - // Wraps an operation on the eSIM chip (any of the other blocking functions) - // Handles notifications automatically after the operation, unless the lambda executing - // the operation returns false, which inhibits automatic notification processing. - // All code executed within are also wrapped automatically in the IO context. - suspend fun beginOperation(op: suspend LocalProfileAssistant.() -> Boolean) = - withContext(Dispatchers.IO) { - val latestSeq = notifications.firstOrNull()?.seqNumber ?: 0 - Log.d(TAG, "Latest notification is $latestSeq before operation") - if (op(this@LocalProfileAssistant)) { - Log.d(TAG, "Operation has requested notification handling") - notifications.filter { it.seqNumber > latestSeq }.forEach { - Log.d(TAG, "Handling notification $it") - handleNotification(it.seqNumber) - } - } - Log.d(TAG, "Operation complete") - } - fun setNickname( iccid: String, nickname: String ): Boolean From 1ac683f9aba9182f63d379f33b4ae40bf8f0bc9d Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Thu, 21 Mar 2024 22:21:24 -0400 Subject: [PATCH 04/11] refactor: Reconnecting channels is a EuiccChannelManager responsibility Reconnecting did not work properly for OMAPI, because in that case we have to reconnect SEService as well. --- .../core/DefaultEuiccChannelFactory.kt | 2 +- .../core/DefaultEuiccChannelManager.kt | 45 +++++++++-- .../openeuicc/core/EuiccChannelManager.kt | 10 +++ .../openeuicc/ui/EuiccManagementFragment.kt | 74 ++++++++++--------- .../UnprivilegedOpenEuiccApplication.kt | 3 +- .../lpac_jni/LocalProfileAssistant.kt | 4 +- .../impl/LocalProfileAssistantImpl.kt | 67 ++--------------- 7 files changed, 98 insertions(+), 107 deletions(-) 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 index c741f497..1cb36505 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt @@ -10,7 +10,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha private var seService: SEService? = null private suspend fun ensureSEService() { - if (seService == null) { + if (seService == null || !seService!!.isConnected) { seService = connectSEService(context) } } 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 a9786775..ec676d15 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 @@ -6,10 +6,12 @@ import android.util.Log import im.angry.openeuicc.di.AppContainer import im.angry.openeuicc.util.* import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout open class DefaultEuiccChannelManager( protected val appContainer: AppContainer, @@ -87,14 +89,18 @@ open class DefaultEuiccChannelManager( } } + override suspend fun findAllEuiccChannelsByPhysicalSlot(physicalSlotId: Int): List? { + for (card in uiccCards) { + if (card.physicalSlotIndex != physicalSlotId) continue + return card.ports.mapNotNull { tryOpenEuiccChannel(it) } + .ifEmpty { null } + } + return null + } + override fun findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId: Int): List? = runBlocking { - for (card in uiccCards) { - if (card.physicalSlotIndex != physicalSlotId) continue - return@runBlocking card.ports.mapNotNull { tryOpenEuiccChannel(it) } - .ifEmpty { null } - } - return@runBlocking null + findAllEuiccChannelsByPhysicalSlot(physicalSlotId) } override fun findEuiccChannelByPortBlocking(physicalSlotId: Int, portId: Int): EuiccChannel? = @@ -106,6 +112,25 @@ open class DefaultEuiccChannelManager( } } + override suspend fun tryReconnectSlot(physicalSlotId: Int, timeoutMillis: Long) { + invalidateByPhysicalSlot(physicalSlotId) + + withTimeout(timeoutMillis) { + while (true) { + try { + findAllEuiccChannelsByPhysicalSlot(physicalSlotId)!!.forEach { + check(it.valid) { "Invalid channel" } + } + break + } catch (e: Exception) { + Log.d(TAG, "Slot reconnect failure, retrying in 1000 ms") + invalidateByPhysicalSlot(physicalSlotId) + } + delay(1000) + } + } + } + override suspend fun enumerateEuiccChannels() { withContext(Dispatchers.IO) { for (uiccInfo in uiccCards) { @@ -132,4 +157,12 @@ open class DefaultEuiccChannelManager( channels.clear() euiccChannelFactory.cleanup() } + + private fun invalidateByPhysicalSlot(physicalSlotId: Int) { + val toRemove = channels.filter { it.valid && it.slotId == physicalSlotId } + for (channel in toRemove) { + channel.close() + channels.remove(channel) + } + } } \ 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 b0bce1da..5ee40f27 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 @@ -8,6 +8,15 @@ interface EuiccChannelManager { */ suspend fun enumerateEuiccChannels() + /** + * Reconnect ALL EuiccChannels belonging to a physical slot + * Throws TimeoutCancellationException when timed out + * If this operation times out, none of the channels belonging to the slot will be + * guaranteed to be consistent. The caller should either call invalidate() + * and try again later, or the application should simply exit entirely. + */ + suspend fun tryReconnectSlot(physicalSlotId: Int, timeoutMillis: Long = 1000) + /** * Returns the EuiccChannel corresponding to a **logical** slot */ @@ -24,6 +33,7 @@ interface EuiccChannelManager { * Returns all EuiccChannels corresponding to a **physical** slot * Multiple channels are possible in the case of MEP */ + suspend fun findAllEuiccChannelsByPhysicalSlot(physicalSlotId: Int): List? fun findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId: Int): List? /** diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt index 98b3d3ac..c02f7fdb 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt @@ -31,7 +31,6 @@ import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.lang.Exception open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, EuiccChannelFragmentMarker { @@ -132,48 +131,51 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, fab.isEnabled = false lifecycleScope.launch { - try { - if (enable) { - doEnableProfile(iccid) + beginTrackedOperation { + val res = if (enable) { + channel.lpa.enableProfile(iccid) } else { - doDisableProfile(iccid) + channel.lpa.disableProfile(iccid) } - refresh() - fab.isEnabled = true - } catch (e: TimeoutCancellationException) { - // Timed out waiting for SIM to come back online, we can no longer assume that the LPA is still valid - AlertDialog.Builder(requireContext()).apply { - setMessage(R.string.enable_disable_timeout) - setPositiveButton(android.R.string.ok) { dialog, _ -> - dialog.dismiss() - requireActivity().finish() - } - setOnDismissListener { _ -> - requireActivity().finish() - } - show() + + if (!res) { + Log.d(TAG, "Failed to enable / disable profile $iccid") + Toast.makeText(context, R.string.toast_profile_enable_failed, Toast.LENGTH_LONG) + .show() + return@beginTrackedOperation false + } + + try { + euiccChannelManager.tryReconnectSlot(slotId, timeoutMillis = 30 * 1000) + } catch (e: TimeoutCancellationException) { + withContext(Dispatchers.Main) { + // Timed out waiting for SIM to come back online, we can no longer assume that the LPA is still valid + AlertDialog.Builder(requireContext()).apply { + setMessage(R.string.enable_disable_timeout) + setPositiveButton(android.R.string.ok) { dialog, _ -> + dialog.dismiss() + requireActivity().finish() + } + setOnDismissListener { _ -> + requireActivity().finish() + } + show() + } + } + return@beginTrackedOperation false + } + + if (enable) { + preferenceRepository.notificationEnableFlow.first() + } else { + preferenceRepository.notificationDisableFlow.first() } - } catch (e: Exception) { - Log.d(TAG, "Failed to enable / disable profile $iccid") - Log.d(TAG, Log.getStackTraceString(e)) - fab.isEnabled = true - Toast.makeText(context, R.string.toast_profile_enable_failed, Toast.LENGTH_LONG).show() } + refresh() + fab.isEnabled = true } } - private suspend fun doEnableProfile(iccid: String) = - beginTrackedOperation { - channel.lpa.enableProfile(iccid, reconnectTimeout = 15 * 1000) && - preferenceRepository.notificationEnableFlow.first() - } - - private suspend fun doDisableProfile(iccid: String) = - beginTrackedOperation { - channel.lpa.disableProfile(iccid, reconnectTimeout = 15 * 1000) && - preferenceRepository.notificationDisableFlow.first() - } - protected open fun populatePopupWithProfileActions(popup: PopupMenu, profile: LocalProfileInfo) { popup.inflate(R.menu.profile_options) if (profile.isEnabled) { diff --git a/app-unpriv/src/main/java/im/angry/openeuicc/UnprivilegedOpenEuiccApplication.kt b/app-unpriv/src/main/java/im/angry/openeuicc/UnprivilegedOpenEuiccApplication.kt index 55415012..7993b501 100644 --- a/app-unpriv/src/main/java/im/angry/openeuicc/UnprivilegedOpenEuiccApplication.kt +++ b/app-unpriv/src/main/java/im/angry/openeuicc/UnprivilegedOpenEuiccApplication.kt @@ -15,7 +15,8 @@ class UnprivilegedOpenEuiccApplication : OpenEuiccApplication() { override fun onCreate() { super.onCreate() - Thread.setDefaultUncaughtExceptionHandler { _, _ -> + Thread.setDefaultUncaughtExceptionHandler { _, e -> + e.printStackTrace() Intent(this, LogsActivity::class.java).apply { addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt index fbc853d4..118a9633 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt @@ -19,8 +19,8 @@ interface LocalProfileAssistant { // All blocking functions in this class assume that they are executed on non-Main threads // The IO context in Kotlin's coroutine library is recommended. - fun enableProfile(iccid: String, reconnectTimeout: Long = 0): Boolean - fun disableProfile(iccid: String, reconnectTimeout: Long = 0): Boolean + fun enableProfile(iccid: String): Boolean + fun disableProfile(iccid: String): Boolean fun deleteProfile(iccid: String): Boolean fun downloadProfile(smdp: String, matchingId: String?, imei: String?, diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt index 246ead21..98a39690 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt @@ -1,9 +1,6 @@ package net.typeblog.lpac_jni.impl import android.util.Log -import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withTimeout import net.typeblog.lpac_jni.LpacJni import net.typeblog.lpac_jni.ApduInterface import net.typeblog.lpac_jni.EuiccInfo2 @@ -14,8 +11,8 @@ import net.typeblog.lpac_jni.LocalProfileNotification import net.typeblog.lpac_jni.ProfileDownloadCallback class LocalProfileAssistantImpl( - private val apduInterface: ApduInterface, - private val httpInterface: HttpInterface + apduInterface: ApduInterface, + httpInterface: HttpInterface ): LocalProfileAssistant { companion object { private const val TAG = "LocalProfileAssistantImpl" @@ -31,48 +28,6 @@ class LocalProfileAssistantImpl( httpInterface.usePublicKeyIds(pkids) } - private fun tryReconnect(timeoutMillis: Long) = runBlocking { - withTimeout(timeoutMillis) { - try { - LpacJni.euiccFini(contextHandle) - LpacJni.destroyContext(contextHandle) - contextHandle = -1 - } catch (e: Exception) { - // Ignored - } - - while (true) { - try { - apduInterface.disconnect() - } catch (e: Exception) { - // Ignored - } - - try { - apduInterface.connect() - contextHandle = LpacJni.createContext(apduInterface, httpInterface) - check(LpacJni.euiccInit(contextHandle) >= 0) { "Reconnect attempt failed" } - // Validate that we can actually use the APDU channel by trying to read eID and profiles - check(valid) { "Reconnected channel is invalid" } - break - } catch (e: Exception) { - e.printStackTrace() - if (contextHandle != -1L) { - try { - LpacJni.euiccFini(contextHandle) - LpacJni.destroyContext(contextHandle) - contextHandle = -1 - } catch (e: Exception) { - // Ignored - } - } - // continue retrying - delay(1000) - } - } - } - } - override val profiles: List get() = LpacJni.es10cGetProfilesInfo(contextHandle)!!.asList() @@ -87,21 +42,11 @@ class LocalProfileAssistantImpl( override val euiccInfo2: EuiccInfo2? get() = LpacJni.es10cexGetEuiccInfo2(contextHandle) - override fun enableProfile(iccid: String, reconnectTimeout: Long): Boolean { - val res = LpacJni.es10cEnableProfile(contextHandle, iccid) == 0 - if (reconnectTimeout > 0) { - tryReconnect(reconnectTimeout) - } - return res - } + override fun enableProfile(iccid: String): Boolean = + LpacJni.es10cEnableProfile(contextHandle, iccid) == 0 - override fun disableProfile(iccid: String, reconnectTimeout: Long): Boolean { - val res = LpacJni.es10cDisableProfile(contextHandle, iccid) == 0 - if (reconnectTimeout > 0) { - tryReconnect(reconnectTimeout) - } - return res - } + override fun disableProfile(iccid: String): Boolean = + LpacJni.es10cDisableProfile(contextHandle, iccid) == 0 override fun deleteProfile(iccid: String): Boolean { return LpacJni.es10cDeleteProfile(contextHandle, iccid) == 0 From e48f9aa8289ea492fac8b14f63f880f27a3dcac0 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Fri, 22 Mar 2024 21:08:59 -0400 Subject: [PATCH 05/11] refactor: Channel validity, and reconnection * ApduInterfaces also need a concept of validity based on the underlying APDU channel. For example, OMAPI depends on SEService being still connected. * We then rely on this validity to wait for reconnection; we do not need to manually remove all channels under a slot because the rest will be invalid anyway, and the next attempt at connection will lazily recreate the channel. * We had to manage channels manually before during reconnect because `valid` may result in SIGSEGV's when the underlying APDU channel has become invalid. This is avoided by the validity concept added to APDU channels. --- .../core/DefaultEuiccChannelManager.kt | 41 +++++++++---------- .../openeuicc/core/EuiccChannelManager.kt | 11 +++-- .../openeuicc/core/OmapiApduInterface.kt | 3 ++ .../openeuicc/ui/EuiccManagementFragment.kt | 2 +- .../core/TelephonyManagerApduInterface.kt | 5 +++ .../net/typeblog/lpac_jni/ApduInterface.kt | 7 ++++ .../lpac_jni/LocalProfileAssistant.kt | 9 ---- .../impl/LocalProfileAssistantImpl.kt | 22 ++++++++-- 8 files changed, 60 insertions(+), 40 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 ec676d15..811c56ca 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 @@ -103,28 +103,35 @@ open class DefaultEuiccChannelManager( findAllEuiccChannelsByPhysicalSlot(physicalSlotId) } - 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 findEuiccChannelByPort(physicalSlotId: Int, portId: Int): EuiccChannel? = + withContext(Dispatchers.IO) { + uiccCards.find { it.physicalSlotIndex == physicalSlotId }?.let { card -> + card.ports.find { it.portIndex == portId }?.let { tryOpenEuiccChannel(it) } } } - override suspend fun tryReconnectSlot(physicalSlotId: Int, timeoutMillis: Long) { - invalidateByPhysicalSlot(physicalSlotId) + override fun findEuiccChannelByPortBlocking(physicalSlotId: Int, portId: Int): EuiccChannel? = + runBlocking { + findEuiccChannelByPort(physicalSlotId, portId) + } + + override suspend fun waitForReconnect(physicalSlotId: Int, portId: Int, timeoutMillis: Long) { + // If there is already a valid channel, we close it proactively + // Sometimes the current channel can linger on for a bit even after it should have become invalid + channels.find { it.slotId == physicalSlotId && it.portId == portId }?.apply { + if (valid) close() + } withTimeout(timeoutMillis) { while (true) { try { - findAllEuiccChannelsByPhysicalSlot(physicalSlotId)!!.forEach { - check(it.valid) { "Invalid channel" } - } + // tryOpenEuiccChannel() will automatically dispose of invalid channels + // and recreate when needed + val channel = findEuiccChannelByPortBlocking(physicalSlotId, portId)!! + check(channel.valid) { "Invalid channel" } break } catch (e: Exception) { - Log.d(TAG, "Slot reconnect failure, retrying in 1000 ms") - invalidateByPhysicalSlot(physicalSlotId) + Log.d(TAG, "Slot $physicalSlotId port $portId reconnect failure, retrying in 1000 ms") } delay(1000) } @@ -157,12 +164,4 @@ open class DefaultEuiccChannelManager( channels.clear() euiccChannelFactory.cleanup() } - - private fun invalidateByPhysicalSlot(physicalSlotId: Int) { - val toRemove = channels.filter { it.valid && it.slotId == physicalSlotId } - for (channel in toRemove) { - channel.close() - channels.remove(channel) - } - } } \ 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 5ee40f27..9ec30301 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 @@ -9,13 +9,11 @@ interface EuiccChannelManager { suspend fun enumerateEuiccChannels() /** - * Reconnect ALL EuiccChannels belonging to a physical slot - * Throws TimeoutCancellationException when timed out - * If this operation times out, none of the channels belonging to the slot will be - * guaranteed to be consistent. The caller should either call invalidate() - * and try again later, or the application should simply exit entirely. + * Wait for a slot + port to reconnect (i.e. become valid again) + * If the port is currently valid, this function will return immediately. + * On timeout, the caller can decide to either try again later, or alert the user with an error */ - suspend fun tryReconnectSlot(physicalSlotId: Int, timeoutMillis: Long = 1000) + suspend fun waitForReconnect(physicalSlotId: Int, portId: Int, timeoutMillis: Long = 1000) /** * Returns the EuiccChannel corresponding to a **logical** slot @@ -39,6 +37,7 @@ interface EuiccChannelManager { /** * Returns the EuiccChannel corresponding to a **physical** slot and a port ID */ + suspend fun findEuiccChannelByPort(physicalSlotId: Int, portId: Int): EuiccChannel? fun findEuiccChannelByPortBlocking(physicalSlotId: Int, portId: Int): EuiccChannel? /** diff --git a/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt b/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt index 3d3bfe11..227977c9 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt @@ -13,6 +13,9 @@ class OmapiApduInterface( private lateinit var session: Session private lateinit var lastChannel: Channel + override val valid: Boolean + get() = service.isConnected && (this::session.isInitialized && !session.isClosed) + override fun connect() { session = service.getUiccReaderCompat(port.logicalSlotIndex + 1).openSession() } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt index c02f7fdb..c1e5ba6d 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt @@ -146,7 +146,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, } try { - euiccChannelManager.tryReconnectSlot(slotId, timeoutMillis = 30 * 1000) + euiccChannelManager.waitForReconnect(slotId, portId, timeoutMillis = 30 * 1000) } catch (e: TimeoutCancellationException) { withContext(Dispatchers.Main) { // Timed out waiting for SIM to come back online, we can no longer assume that the LPA is still valid diff --git a/app/src/main/java/im/angry/openeuicc/core/TelephonyManagerApduInterface.kt b/app/src/main/java/im/angry/openeuicc/core/TelephonyManagerApduInterface.kt index d6770c3c..509d7ee8 100644 --- a/app/src/main/java/im/angry/openeuicc/core/TelephonyManagerApduInterface.kt +++ b/app/src/main/java/im/angry/openeuicc/core/TelephonyManagerApduInterface.kt @@ -11,6 +11,11 @@ class TelephonyManagerApduInterface( ): ApduInterface { private var lastChannel: Int = -1 + override val valid: Boolean + // TelephonyManager channels will never become truly "invalid", + // just that transactions might return errors or nonsense + get() = lastChannel != -1 + override fun connect() { // Do nothing } diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/ApduInterface.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/ApduInterface.kt index 44d1a3ef..dfa92dfa 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/ApduInterface.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/ApduInterface.kt @@ -9,4 +9,11 @@ interface ApduInterface { fun logicalChannelOpen(aid: ByteArray): Int fun logicalChannelClose(handle: Int) fun transmit(tx: ByteArray): ByteArray + + /** + * Is this APDU connection still valid? + * Note that even if this returns true, the underlying connection might be broken anyway; + * callers should further check with the LPA to fully determine the validity of a channel + */ + val valid: Boolean } \ No newline at end of file diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt index 118a9633..16a91e77 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt @@ -2,15 +2,6 @@ package net.typeblog.lpac_jni interface LocalProfileAssistant { val valid: Boolean - get() = try { - // If we can read both eID and profiles properly, we are likely looking at - // a valid LocalProfileAssistant - eID - profiles - true - } catch (e: Exception) { - false - } val profiles: List val notifications: List val eID: String diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt index 98a39690..a76ba753 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt @@ -11,13 +11,14 @@ import net.typeblog.lpac_jni.LocalProfileNotification import net.typeblog.lpac_jni.ProfileDownloadCallback class LocalProfileAssistantImpl( - apduInterface: ApduInterface, + private val apduInterface: ApduInterface, httpInterface: HttpInterface ): LocalProfileAssistant { companion object { private const val TAG = "LocalProfileAssistantImpl" } + private var finalized = false private var contextHandle: Long = LpacJni.createContext(apduInterface, httpInterface) init { if (LpacJni.euiccInit(contextHandle) < 0) { @@ -28,6 +29,17 @@ class LocalProfileAssistantImpl( httpInterface.usePublicKeyIds(pkids) } + override val valid: Boolean + get() = !finalized && apduInterface.valid && try { + // If we can read both eID and profiles properly, we are likely looking at + // a valid LocalProfileAssistant + eID + profiles + true + } catch (e: Exception) { + false + } + override val profiles: List get() = LpacJni.es10cGetProfilesInfo(contextHandle)!!.asList() @@ -71,8 +83,12 @@ class LocalProfileAssistantImpl( return LpacJni.es10cSetNickname(contextHandle, iccid, nickname) == 0 } + @Synchronized override fun close() { - LpacJni.euiccFini(contextHandle) - LpacJni.destroyContext(contextHandle) + if (!finalized) { + LpacJni.euiccFini(contextHandle) + LpacJni.destroyContext(contextHandle) + finalized = true + } } } \ No newline at end of file From 2517fc817e3128579dfacbd8db508c7d0b893152 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 23 Mar 2024 11:17:10 -0400 Subject: [PATCH 06/11] ComaptibilityCheck: Clarify "Known Broken" --- 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 e6641b3a..7ba42fe7 100644 --- a/app-unpriv/src/main/res/values/strings.xml +++ b/app-unpriv/src/main/res/values/strings.xml @@ -15,7 +15,7 @@ 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. OMAPI access to ISD-R is only possible on the following SIM slots: %s. - Known Broken? + Not on the Known Broken List Making sure your device is not known to have bugs associated with removable eSIMs. Oops, your device is known to have bugs when accessing removable eSIMs. This does not necessarily mean that it will not work at all, but you will have to proceed with caution. \ No newline at end of file From 4b842c4afeaec33c9f9304ceac834a6d39d85a7d Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 23 Mar 2024 20:56:42 -0400 Subject: [PATCH 07/11] feat: Add an "verdict" to compatibility checks --- .../openeuicc/util/CompatibilityCheck.kt | 69 ++++++++++++++++--- app-unpriv/src/main/res/values/strings.xml | 8 +++ 2 files changed, 68 insertions(+), 9 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 83c1c5d9..d2ee8ccd 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 @@ -16,12 +16,16 @@ fun getCompatibilityChecks(context: Context): List = HasSystemFeaturesCheck(context), OmapiConnCheck(context), IsdrChannelAccessCheck(context), - KnownBrokenCheck(context) + KnownBrokenCheck(context), + Verdict(context), ) +inline fun List.findCheck(): T? = + find { it.javaClass == T::class.java }?.let { it as T } + suspend fun List.executeAll(callback: () -> Unit) = withContext(Dispatchers.IO) { forEach { - it.run() + it.run(this@executeAll) withContext(Dispatchers.Main) { callback() } @@ -57,13 +61,13 @@ abstract class CompatibilityCheck(context: Context) { else -> defaultDescription } - protected abstract suspend fun doCheck(): State + protected abstract suspend fun doCheck(allChecks: List): State - suspend fun run() { + suspend fun run(allChecks: List) { state = State.IN_PROGRESS delay(200) state = try { - doCheck() + doCheck(allChecks) } catch (_: Exception) { State.FAILURE } @@ -76,7 +80,7 @@ internal class HasSystemFeaturesCheck(private val context: Context): Compatibili override val defaultDescription: String get() = context.getString(R.string.compatibility_check_system_features_desc) - override suspend fun doCheck(): State { + override suspend fun doCheck(allChecks: List): State { if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) { failureDescription = context.getString(R.string.compatibility_check_system_features_no_telephony) return State.FAILURE @@ -100,7 +104,7 @@ internal class OmapiConnCheck(private val context: Context): CompatibilityCheck( override val defaultDescription: String get() = context.getString(R.string.compatibility_check_omapi_connectivity_desc) - override suspend fun doCheck(): State { + override suspend fun doCheck(allChecks: List): State { val seService = connectSEService(context) if (!seService.isConnected) { failureDescription = context.getString(R.string.compatibility_check_omapi_connectivity_fail) @@ -132,7 +136,7 @@ internal class IsdrChannelAccessCheck(private val context: Context): Compatibili override val defaultDescription: String get() = context.getString(R.string.compatibility_check_isdr_channel_desc) - override suspend fun doCheck(): State { + override suspend fun doCheck(allChecks: List): State { val seService = connectSEService(context) val readers = seService.readers.filter { it.isSIM } if (readers.isEmpty()) { @@ -200,10 +204,57 @@ internal class KnownBrokenCheck(private val context: Context): CompatibilityChec failureDescription = context.getString(R.string.compatibility_check_known_broken_fail) } - override suspend fun doCheck(): State = + override suspend fun doCheck(allChecks: List): State = if (Build.MANUFACTURER.lowercase() in BROKEN_MANUFACTURERS) { State.FAILURE } else { State.SUCCESS } +} + +internal class Verdict(private val context: Context) : CompatibilityCheck(context) { + override val title: String + get() = context.getString(R.string.compatibility_check_verdict) + override val defaultDescription: String + get() = context.getString(R.string.compatibility_check_verdict_desc) + + override suspend fun doCheck(allChecks: List): State { + if (allChecks.findCheck()?.state == State.FAILURE) { + failureDescription = context.getString( + R.string.compatibility_check_verdict_known_broken, + context.getString(R.string.compatibility_check_verdict_fail_shared) + ) + return State.FAILURE + } + + if (allChecks.findCheck()?.state == State.SUCCESS && + allChecks.findCheck()?.state == State.SUCCESS + ) { + successDescription = context.getString(R.string.compatibility_check_verdict_ok) + return State.SUCCESS + } + + if (allChecks.findCheck()?.state == State.FAILURE_UNKNOWN || + allChecks.findCheck()?.state == State.FAILURE_UNKNOWN + ) { + // We are not sure because we can't fully check OMAPI + // however we can guess based on feature flags + // TODO: We probably need a "known-good" list for these devices as well? + failureDescription = context.getString( + if (allChecks.findCheck()?.state == State.SUCCESS) { + R.string.compatibility_check_verdict_unknown_likely_ok + } else { + R.string.compatibility_check_verdict_unknown_likely_fail + }, + context.getString(R.string.compatibility_check_verdict_fail_shared) + ) + return State.FAILURE_UNKNOWN + } + + failureDescription = context.getString( + R.string.compatibility_check_verdict_unknown, + context.getString(R.string.compatibility_check_verdict_fail_shared) + ) + return State.FAILURE_UNKNOWN + } } \ 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 7ba42fe7..625136dc 100644 --- a/app-unpriv/src/main/res/values/strings.xml +++ b/app-unpriv/src/main/res/values/strings.xml @@ -18,4 +18,12 @@ Not on the Known Broken List Making sure your device is not known to have bugs associated with removable eSIMs. Oops, your device is known to have bugs when accessing removable eSIMs. This does not necessarily mean that it will not work at all, but you will have to proceed with caution. + Verdict + Based on all previous checks, how likely is your device to be compatible with removable eSIMs? + You can likely use and manage removable eSIMs on this device. + Your device is known to be buggy when accessing removable eSIMs.\n%s + We cannot determine whether removable eSIMs can be managed on your device. Your device does declare support for OMAPI, though, so it is slightly more likely that it will work.\n%s + We cannot determine whether removable eSIMs can be managed on your device. Since your device does not declare support for OMAPI, it is more likely that managing removable eSIMs on this device is unsupported.\n%s + We cannot determine whether removable eSIMs can be managed on your device.\n%s + However, a removable eSIM that has already been loaded with an eSIM profile will still work; this verdict is only for management features via the app, such as switching and downloading profiles. \ No newline at end of file From 1ed5a4de38d111812003aa4b42363125094e1470 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Fri, 29 Mar 2024 17:31:06 -0400 Subject: [PATCH 08/11] chore: Uprev lpac Fixes #20 --- libs/lpac-jni/src/main/jni/lpac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/lpac-jni/src/main/jni/lpac b/libs/lpac-jni/src/main/jni/lpac index dc09c3e6..0bb19697 160000 --- a/libs/lpac-jni/src/main/jni/lpac +++ b/libs/lpac-jni/src/main/jni/lpac @@ -1 +1 @@ -Subproject commit dc09c3e668fc191694e73fede255a7a0a26f4988 +Subproject commit 0bb196977e7f2e276a1076734897ad8c48d9d5ca From 80adac68c8b18945f35d833cb7b17d3da7542a6e Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 30 Mar 2024 15:30:40 -0400 Subject: [PATCH 09/11] ui: Use KiB instead of KB for free space Fixes #19. --- .../im/angry/openeuicc/ui/ProfileDownloadFragment.kt | 3 +-- .../main/java/im/angry/openeuicc/util/StringUtils.kt | 10 +++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt index 78a54f21..feb0ab06 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt @@ -5,7 +5,6 @@ import android.app.Dialog import android.content.DialogInterface import android.os.Bundle import android.text.Editable -import android.text.format.Formatter import android.util.Log import android.view.* import android.widget.ProgressBar @@ -131,7 +130,7 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(), // Fetch remaining NVRAM val str = channel.lpa.euiccInfo2?.freeNvram?.also { freeNvram = it - }?.let { Formatter.formatShortFileSize(requireContext(), it.toLong()) } + }?.let { formatFreeSpace(it) } withContext(Dispatchers.Main) { profileDownloadFreeSpace.text = getString(R.string.profile_download_free_space, diff --git a/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt index cca08745..ebf87298 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt @@ -19,4 +19,12 @@ fun ByteArray.encodeHex(): String { sb.append(String.format("%02X", this[i])) } return sb.toString() -} \ No newline at end of file +} + +fun formatFreeSpace(size: Int): String = + // SIM cards probably won't have much more space anytime soon. + if (size >= 1024) { + "%.2f KiB".format(size.toDouble() / 1024) + } else { + "$size B" + } \ No newline at end of file From f046d40f2ccf760cdcfe1cf9bafb090b827a8d05 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 30 Mar 2024 15:36:30 -0400 Subject: [PATCH 10/11] privileged: Disable slot mapping support for pre-T Fixes #15. --- .../main/java/im/angry/openeuicc/service/OpenEuiccService.kt | 5 +++++ .../java/im/angry/openeuicc/ui/PrivilegedMainActivity.kt | 5 +++++ 2 files changed, 10 insertions(+) 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 3c9eaf26..65d79180 100644 --- a/app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt +++ b/app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt @@ -1,5 +1,6 @@ package im.angry.openeuicc.service +import android.os.Build import android.service.euicc.* import android.telephony.UiccSlotMapping import android.telephony.euicc.DownloadableSubscription @@ -34,6 +35,10 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker { lpa.profiles.any { it.iccid == iccid } private fun ensurePortIsMapped(slotId: Int, portId: Int) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + return + } + val mappings = telephonyManager.simSlotMapping.toMutableList() mappings.firstOrNull { it.physicalSlotIndex == slotId && it.portIndex == portId }?.let { 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 f3c2b3c8..440e5290 100644 --- a/app/src/main/java/im/angry/openeuicc/ui/PrivilegedMainActivity.kt +++ b/app/src/main/java/im/angry/openeuicc/ui/PrivilegedMainActivity.kt @@ -1,5 +1,6 @@ package im.angry.openeuicc.ui +import android.os.Build import android.view.Menu import android.view.MenuItem import android.widget.Toast @@ -11,6 +12,10 @@ class PrivilegedMainActivity : MainActivity() { super.onCreateOptionsMenu(menu) menuInflater.inflate(R.menu.activity_main_privileged, menu) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + menu.findItem(R.id.slot_mapping).isVisible = false + } + if (tm.supportsDSDS) { val dsds = menu.findItem(R.id.dsds) dsds.isVisible = true From 14687acc7343e4e4f8ddb1b9dcf5a24f42e5491a Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 30 Mar 2024 16:47:30 -0400 Subject: [PATCH 11/11] jmp: Customize the timeout dialog --- app-unpriv/src/jmp/res/values/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app-unpriv/src/jmp/res/values/strings.xml b/app-unpriv/src/jmp/res/values/strings.xml index a4610a76..fa90931e 100644 --- a/app-unpriv/src/jmp/res/values/strings.xml +++ b/app-unpriv/src/jmp/res/values/strings.xml @@ -5,4 +5,6 @@ Buy JMP eSIM Adapter https://jmp.chat/esim-adapter https://gitea.angry.im/jmp-sim/jmp-sim-manager + + Timed out waiting for the eSIM chip to switch profiles. Please manually refresh the eSIM adapter by going to SIM Toolkit, and select Tools -> Reboot. \ No newline at end of file