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 1cb36505..c741f497 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 811c56ca..a9786775 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,54 +87,24 @@ 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) - } - - 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) } + 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 { - 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 { - // 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 $physicalSlotId port $portId reconnect failure, retrying in 1000 ms") + withContext(Dispatchers.IO) { + uiccCards.find { it.physicalSlotIndex == physicalSlotId }?.let { card -> + card.ports.find { it.portIndex == portId }?.let { tryOpenEuiccChannel(it) } } - delay(1000) } } - } override suspend fun enumerateEuiccChannels() { withContext(Dispatchers.IO) { 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 9ec30301..b0bce1da 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,13 +8,6 @@ interface EuiccChannelManager { */ suspend fun enumerateEuiccChannels() - /** - * 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 waitForReconnect(physicalSlotId: Int, portId: Int, timeoutMillis: Long = 1000) - /** * Returns the EuiccChannel corresponding to a **logical** slot */ @@ -31,13 +24,11 @@ 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? /** * 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 227977c9..3d3bfe11 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,9 +13,6 @@ 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 c1e5ba6d..7564cc88 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,7 +16,6 @@ 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 @@ -27,10 +26,10 @@ 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 +import java.lang.Exception open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, EuiccChannelFragmentMarker { @@ -131,51 +130,35 @@ 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.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 - 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: 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 75863541..ede48ea3 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 feb0ab06..8ab09d91 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,6 +5,7 @@ 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 @@ -130,7 +131,7 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(), // Fetch remaining NVRAM val str = channel.lpa.euiccInfo2?.freeNvram?.also { freeNvram = it - }?.let { formatFreeSpace(it) } + }?.let { Formatter.formatShortFileSize(requireContext(), it.toLong()) } withContext(Dispatchers.Main) { profileDownloadFreeSpace.text = getString(R.string.profile_download_free_space, @@ -188,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 feee0cdd..7c984c7c 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-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt index ebf87298..cca08745 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,12 +19,4 @@ fun ByteArray.encodeHex(): String { sb.append(String.format("%02X", this[i])) } return sb.toString() -} - -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 +} \ No newline at end of file diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index ac6b9d4e..52187850 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -16,8 +16,6 @@ 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/app-unpriv/src/jmp/res/values/strings.xml b/app-unpriv/src/jmp/res/values/strings.xml index fa90931e..a4610a76 100644 --- a/app-unpriv/src/jmp/res/values/strings.xml +++ b/app-unpriv/src/jmp/res/values/strings.xml @@ -5,6 +5,4 @@ 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 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 f6702bde..f139840e 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 @@ open 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/app-unpriv/src/main/java/im/angry/openeuicc/util/CompatibilityCheck.kt b/app-unpriv/src/main/java/im/angry/openeuicc/util/CompatibilityCheck.kt index d2ee8ccd..ae5a4da8 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,16 +16,12 @@ fun getCompatibilityChecks(context: Context): List = HasSystemFeaturesCheck(context), OmapiConnCheck(context), IsdrChannelAccessCheck(context), - KnownBrokenCheck(context), - Verdict(context), + KnownBrokenCheck(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(this@executeAll) + it.run() withContext(Dispatchers.Main) { callback() } @@ -61,13 +57,13 @@ abstract class CompatibilityCheck(context: Context) { else -> defaultDescription } - protected abstract suspend fun doCheck(allChecks: List): State + protected abstract suspend fun doCheck(): State - suspend fun run(allChecks: List) { + suspend fun run() { state = State.IN_PROGRESS delay(200) state = try { - doCheck(allChecks) + doCheck() } catch (_: Exception) { State.FAILURE } @@ -80,7 +76,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(allChecks: List): State { + override suspend fun doCheck(): State { if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) { failureDescription = context.getString(R.string.compatibility_check_system_features_no_telephony) return State.FAILURE @@ -91,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_UNKNOWN + return State.FAILURE } return State.SUCCESS @@ -104,7 +100,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(allChecks: List): State { + override suspend fun doCheck(): State { val seService = connectSEService(context) if (!seService.isConnected) { failureDescription = context.getString(R.string.compatibility_check_omapi_connectivity_fail) @@ -136,7 +132,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(allChecks: List): State { + override suspend fun doCheck(): State { val seService = connectSEService(context) val readers = seService.readers.filter { it.isSIM } if (readers.isEmpty()) { @@ -204,57 +200,10 @@ internal class KnownBrokenCheck(private val context: Context): CompatibilityChec failureDescription = context.getString(R.string.compatibility_check_known_broken_fail) } - override suspend fun doCheck(allChecks: List): State = + override suspend fun doCheck(): 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 625136dc..588c0735 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 / 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. + 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. @@ -15,15 +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. - Not on the Known Broken List + Known Broken? 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 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 509d7ee8..d6770c3c 100644 --- a/app/src/main/java/im/angry/openeuicc/core/TelephonyManagerApduInterface.kt +++ b/app/src/main/java/im/angry/openeuicc/core/TelephonyManagerApduInterface.kt @@ -11,11 +11,6 @@ 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/app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt b/app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt index 65d79180..3c9eaf26 100644 --- a/app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt +++ b/app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt @@ -1,6 +1,5 @@ package im.angry.openeuicc.service -import android.os.Build import android.service.euicc.* import android.telephony.UiccSlotMapping import android.telephony.euicc.DownloadableSubscription @@ -35,10 +34,6 @@ 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 440e5290..f3c2b3c8 100644 --- a/app/src/main/java/im/angry/openeuicc/ui/PrivilegedMainActivity.kt +++ b/app/src/main/java/im/angry/openeuicc/ui/PrivilegedMainActivity.kt @@ -1,6 +1,5 @@ package im.angry.openeuicc.ui -import android.os.Build import android.view.Menu import android.view.MenuItem import android.widget.Toast @@ -12,10 +11,6 @@ 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 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 dfa92dfa..44d1a3ef 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,11 +9,4 @@ 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 16a91e77..b40ba10a 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,7 +1,24 @@ 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 + // a valid LocalProfileAssistant + eID + profiles + true + } catch (e: Exception) { + false + } val profiles: List val notifications: List val eID: String @@ -10,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?, @@ -20,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 a76ba753..b81a41ad 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 @@ -12,13 +15,12 @@ import net.typeblog.lpac_jni.ProfileDownloadCallback class LocalProfileAssistantImpl( private val apduInterface: ApduInterface, - httpInterface: HttpInterface + private val 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) { @@ -29,16 +31,47 @@ 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 + 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() @@ -54,11 +87,29 @@ 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) { + try { + tryReconnect(reconnectTimeout) + } catch (e: Exception) { + return false + } + } + 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) { + try { + tryReconnect(reconnectTimeout) + } catch (e: Exception) { + return false + } + } + return res + } override fun deleteProfile(iccid: String): Boolean { return LpacJni.es10cDeleteProfile(contextHandle, iccid) == 0 @@ -83,12 +134,8 @@ class LocalProfileAssistantImpl( return LpacJni.es10cSetNickname(contextHandle, iccid, nickname) == 0 } - @Synchronized override fun close() { - if (!finalized) { - LpacJni.euiccFini(contextHandle) - LpacJni.destroyContext(contextHandle) - finalized = true - } + LpacJni.euiccFini(contextHandle) + LpacJni.destroyContext(contextHandle) } } \ No newline at end of file diff --git a/libs/lpac-jni/src/main/jni/lpac b/libs/lpac-jni/src/main/jni/lpac index 0bb19697..dc09c3e6 160000 --- a/libs/lpac-jni/src/main/jni/lpac +++ b/libs/lpac-jni/src/main/jni/lpac @@ -1 +1 @@ -Subproject commit 0bb196977e7f2e276a1076734897ad8c48d9d5ca +Subproject commit dc09c3e668fc191694e73fede255a7a0a26f4988