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 1cb3650..c741f49 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 || !seService!!.isConnected) { + if (seService == null) { 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 ec676d1..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 @@ -6,12 +6,10 @@ 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, @@ -89,18 +87,14 @@ 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 { - findAllEuiccChannelsByPhysicalSlot(physicalSlotId) + 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? = @@ -112,25 +106,6 @@ 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) { @@ -157,12 +132,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 5ee40f2..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 @@ -8,15 +8,6 @@ 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 */ @@ -33,7 +24,6 @@ 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 c02f7fd..2e162b5 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,6 +31,7 @@ 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 { @@ -131,51 +132,48 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, fab.isEnabled = false lifecycleScope.launch { - beginTrackedOperation { - val res = if (enable) { - channel.lpa.enableProfile(iccid) - } else { - channel.lpa.disableProfile(iccid) - } - - 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 - } - + try { if (enable) { - preferenceRepository.notificationEnableFlow.first() + doEnableProfile(iccid) } else { - preferenceRepository.notificationDisableFlow.first() + doDisableProfile(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() + } + } 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) = + channel.lpa.beginOperation { + channel.lpa.enableProfile(iccid, reconnectTimeout = 15 * 1000) && + preferenceRepository.notificationEnableFlow.first() + } + + private suspend fun doDisableProfile(iccid: String) = + channel.lpa.beginOperation { + 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-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt index 7586354..ede48ea 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() = beginTrackedOperation { + private suspend fun doDelete() = channel.lpa.beginOperation { 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 78a54f2..8ab09d9 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,25 +189,15 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(), } } - 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 - } + 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 } - }) + } + }) // 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 feee0cd..7c984c7 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,14 +1,8 @@ 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 @@ -34,27 +28,6 @@ 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/app-unpriv/src/main/java/im/angry/openeuicc/UnprivilegedOpenEuiccApplication.kt b/app-unpriv/src/main/java/im/angry/openeuicc/UnprivilegedOpenEuiccApplication.kt index 7993b50..5541501 100644 --- a/app-unpriv/src/main/java/im/angry/openeuicc/UnprivilegedOpenEuiccApplication.kt +++ b/app-unpriv/src/main/java/im/angry/openeuicc/UnprivilegedOpenEuiccApplication.kt @@ -15,8 +15,7 @@ class UnprivilegedOpenEuiccApplication : OpenEuiccApplication() { override fun onCreate() { super.onCreate() - Thread.setDefaultUncaughtExceptionHandler { _, e -> - e.printStackTrace() + Thread.setDefaultUncaughtExceptionHandler { _, _ -> 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 118a963..b40ba10 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,6 +1,14 @@ 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 @@ -19,8 +27,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): Boolean - fun disableProfile(iccid: String): Boolean + fun enableProfile(iccid: String, reconnectTimeout: Long = 0): Boolean + fun disableProfile(iccid: String, reconnectTimeout: Long = 0): Boolean fun deleteProfile(iccid: String): Boolean fun downloadProfile(smdp: String, matchingId: String?, imei: String?, @@ -29,6 +37,24 @@ 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 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 98a3969..246ead2 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,6 +1,9 @@ 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 @@ -11,8 +14,8 @@ import net.typeblog.lpac_jni.LocalProfileNotification import net.typeblog.lpac_jni.ProfileDownloadCallback class LocalProfileAssistantImpl( - apduInterface: ApduInterface, - httpInterface: HttpInterface + private val apduInterface: ApduInterface, + private val httpInterface: HttpInterface ): LocalProfileAssistant { companion object { private const val TAG = "LocalProfileAssistantImpl" @@ -28,6 +31,48 @@ 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() @@ -42,11 +87,21 @@ class LocalProfileAssistantImpl( override val euiccInfo2: EuiccInfo2? get() = LpacJni.es10cexGetEuiccInfo2(contextHandle) - override fun enableProfile(iccid: String): Boolean = - LpacJni.es10cEnableProfile(contextHandle, iccid) == 0 + override fun enableProfile(iccid: String, reconnectTimeout: Long): Boolean { + val res = LpacJni.es10cEnableProfile(contextHandle, iccid) == 0 + if (reconnectTimeout > 0) { + tryReconnect(reconnectTimeout) + } + return res + } - override fun disableProfile(iccid: String): Boolean = - LpacJni.es10cDisableProfile(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 deleteProfile(iccid: String): Boolean { return LpacJni.es10cDeleteProfile(contextHandle, iccid) == 0