From 4b737a6988d90b6251981a1a90fa177a1978f28e Mon Sep 17 00:00:00 2001 From: septs Date: Wed, 16 Jul 2025 01:31:06 +0800 Subject: [PATCH] feat: simplified error handling --- .../ui/wizard/SimplifiedErrorHandling.kt | 145 ++++++++++++++++++ app-common/src/main/res/values/strings.xml | 20 +++ 2 files changed, 165 insertions(+) create mode 100644 app-common/src/main/java/im/angry/openeuicc/ui/wizard/SimplifiedErrorHandling.kt diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/SimplifiedErrorHandling.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/SimplifiedErrorHandling.kt new file mode 100644 index 0000000..14a4f57 --- /dev/null +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/SimplifiedErrorHandling.kt @@ -0,0 +1,145 @@ +package im.angry.openeuicc.ui.wizard + +import androidx.annotation.StringRes +import im.angry.openeuicc.common.R +import net.typeblog.lpac_jni.LocalProfileAssistant +import org.json.JSONObject +import java.net.NoRouteToHostException +import java.net.PortUnreachableException +import java.net.SocketException +import java.net.SocketTimeoutException +import java.net.UnknownHostException +import javax.net.ssl.SSLException + +object SimplifiedErrorHandling { + enum class ErrorCode(@StringRes val titleResId: Int, @StringRes val suggestResId: Int?) { + ICCIDAlready( + R.string.download_wizard_error_iccid_already, + R.string.download_wizard_error_suggest_profile_installed + ), + InsufficientMemory( + R.string.download_wizard_error_insufficient_memory, + R.string.download_wizard_error_suggest_insufficient_memory + ), + UnsupportedProfile( + R.string.download_wizard_error_unsupported_profile, + null + ), + CardInternalError( + R.string.download_wizard_error_card_internal_error, + null + ), + EIDMismatch( + R.string.download_wizard_error_eid_mismatch, + R.string.download_wizard_error_suggest_contact_reissue + ), + UnreleasedProfile( + R.string.download_wizard_error_profile_unreleased, + R.string.download_wizard_error_suggest_contact_reissue + ), + MatchingIDRefused( + R.string.download_wizard_error_matching_id_refused, + R.string.download_wizard_error_suggest_contact_carrier + ), + ProfileRetriesExceeded( + R.string.download_wizard_error_profile_retries_exceeded, + R.string.download_wizard_error_suggest_contact_carrier + ), + ConfirmationCodeMissing( + R.string.download_wizard_error_confirmation_code_missing, + R.string.download_wizard_error_suggest_contact_carrier + ), + ConfirmationCodeRefused( + R.string.download_wizard_error_confirmation_code_refused, + R.string.download_wizard_error_suggest_contact_carrier + ), + ConfirmationCodeRetriesExceeded( + R.string.download_wizard_error_confirmation_code_retries_exceeded, + R.string.download_wizard_error_suggest_contact_carrier + ), + ProfileExpired( + R.string.download_wizard_error_profile_expired, + R.string.download_wizard_error_suggest_contact_carrier + ), + UnknownHost( + R.string.download_wizard_error_unknown_hostname, + null + ), + NetworkUnreachable( + R.string.download_wizard_error_network_unreachable, + R.string.download_wizard_error_suggest_network_unreachable + ), + TLSError( + R.string.download_wizard_error_tls_certificate, + null + ) + } + + private val httpErrors = buildMap { + // Stage: AuthenticateClient + put("8.1" to "4.8", ErrorCode.InsufficientMemory) + put("8.1.1" to "3.8", ErrorCode.EIDMismatch) + put("8.2" to "1.2", ErrorCode.UnreleasedProfile) + put("8.2.6" to "3.8", ErrorCode.MatchingIDRefused) + put("8.8.5" to "6.4", ErrorCode.ProfileRetriesExceeded) + + // Stage: GetBoundProfilePackage + put("8.2.7" to "2.2", ErrorCode.ConfirmationCodeMissing) + put("8.2.7" to "3.8", ErrorCode.ConfirmationCodeRefused) + put("8.2.7" to "6.4", ErrorCode.ConfirmationCodeRetriesExceeded) + + // Stage: AuthenticateClient, GetBoundProfilePackage + put("8.8.5" to "4.10", ErrorCode.ProfileExpired) + } + + fun toSimplifiedDownloadError(exc: LocalProfileAssistant.ProfileDownloadException) = when { + exc.lpaErrorReason != "ES10B_ERROR_REASON_UNDEFINED" -> toSimplifiedLPAErrorReason(exc.lpaErrorReason) + exc.lastHttpResponse?.rcode == 200 -> toSimplifiedHTTPResponse(exc.lastHttpResponse!!) + exc.lastHttpException != null -> toSimplifiedHTTPException(exc.lastHttpException!!) + exc.lastApduResponse != null -> toSimplifiedAPDUResponse(exc.lastApduResponse!!) + else -> null + } + + private fun toSimplifiedLPAErrorReason(reason: String) = when (reason) { + "ES10B_ERROR_REASON_UNSUPPORTED_CRT_VALUES" -> ErrorCode.UnsupportedProfile + "ES10B_ERROR_REASON_UNSUPPORTED_REMOTE_OPERATION_TYPE" -> ErrorCode.UnsupportedProfile + "ES10B_ERROR_REASON_UNSUPPORTED_PROFILE_CLASS" -> ErrorCode.UnsupportedProfile + "ES10B_ERROR_REASON_INSTALL_FAILED_DUE_TO_ICCID_ALREADY_EXISTS_ON_EUICC" -> ErrorCode.ICCIDAlready + "ES10B_ERROR_REASON_INSTALL_FAILED_DUE_TO_INSUFFICIENT_MEMORY_FOR_PROFILE" -> ErrorCode.InsufficientMemory + "ES10B_ERROR_REASON_INSTALL_FAILED_DUE_TO_INTERRUPTION" -> ErrorCode.CardInternalError + "ES10B_ERROR_REASON_INSTALL_FAILED_DUE_TO_PE_PROCESSING_ERROR" -> ErrorCode.CardInternalError + else -> null + } + + private fun toSimplifiedHTTPResponse(response: net.typeblog.lpac_jni.HttpInterface.HttpResponse): ErrorCode? { + if (response.data.first().toInt() != '{'.code) return null + val response = JSONObject(response.data.decodeToString()) + val statusCodeData = response.optJSONObject("header") + ?.optJSONObject("functionExecutionStatus") + ?.optJSONObject("statusCodeData") + ?: return null + val subjectCode = statusCodeData.optString("subjectCode") + val reasonCode = statusCodeData.optString("reasonCode") + return httpErrors[subjectCode to reasonCode] + } + + private fun toSimplifiedHTTPException(exc: Exception) = when (exc) { + is SSLException -> ErrorCode.TLSError + is UnknownHostException -> ErrorCode.UnknownHost + is NoRouteToHostException -> ErrorCode.NetworkUnreachable + is PortUnreachableException -> ErrorCode.NetworkUnreachable + is SocketTimeoutException -> ErrorCode.NetworkUnreachable + is SocketException -> exc.message + ?.contains("Connection reset", ignoreCase = true) + ?.let { if (it) ErrorCode.NetworkUnreachable else null } + else -> null + } + + private fun toSimplifiedAPDUResponse(resp: ByteArray): ErrorCode? { + val isSuccess = resp.size >= 2 && + resp[resp.size - 2] == 0x90.toByte() && + resp[resp.size - 1] == 0x00.toByte() + if (isSuccess) return null + return ErrorCode.CardInternalError + } +} diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index 38bb976..45ec3a7 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -102,6 +102,26 @@ Last APDU exception: Save Diagnostics at %s + This eSIM profile is installed, Cannot be reinstalled. + Sorry, The remaining capacity of this eSIM chip cannot be used to install this eSIM profile. + Sorry, This eSIM profile is unsupported. + An error occurred inside the card. + This eSIM profile has been installed on another device. + This eSIM profile has been unreleased. + This eSIM activation code is invalid. + The maximum number of retries for the eSIM profile has been exceeded. + Please enter the confirmation code to continue. + The confirmation code you entered is invalid. + This eSIM profile has been expired. + The maximum number of retries for the Confirmation Code has been exceeded. + Unknown SM-DP+ address + The current network is unreachable + TLS certificate error, this eSIM profile is not supported + You are trying to reinstall an already installed eSIM profile + Please delete an eSIM profile and try again + Please contact your carrier for assistance. + Please contact your carrier to reissue this eSIM profile. + The current network is unavailable. Please try again after changing the network. Logs have been saved to the selected path. Would you like to share the log through another app? -- 2.45.3