From f167d059dccb8bb1de48543fd89a1fa0820321f9 Mon Sep 17 00:00:00 2001 From: septs Date: Mon, 18 Aug 2025 09:57:15 +0800 Subject: [PATCH 1/6] 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 ae0700b..b90338c 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -104,6 +104,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? From 1ed53a2f32a198ea0d92f14b9fce7ae03407d8a9 Mon Sep 17 00:00:00 2001 From: septs Date: Thu, 28 Aug 2025 21:31:44 +0800 Subject: [PATCH 2/6] feat: add EIDNotSupported error handling and corresponding message --- .../im/angry/openeuicc/ui/wizard/SimplifiedErrorHandling.kt | 5 +++++ app-common/src/main/res/values/strings.xml | 1 + 2 files changed, 6 insertions(+) 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 index 14a4f57..1968b4f 100644 --- 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 @@ -29,6 +29,10 @@ object SimplifiedErrorHandling { R.string.download_wizard_error_card_internal_error, null ), + EIDNotSupported( + R.string.download_wizard_error_eid_not_supported, + R.string.download_wizard_error_suggest_contact_carrier + ), EIDMismatch( R.string.download_wizard_error_eid_mismatch, R.string.download_wizard_error_suggest_contact_reissue @@ -78,6 +82,7 @@ object SimplifiedErrorHandling { private val httpErrors = buildMap { // Stage: AuthenticateClient put("8.1" to "4.8", ErrorCode.InsufficientMemory) + put("8.1.1" to "2.1", ErrorCode.EIDNotSupported) 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) diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index b90338c..a41da62 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -108,6 +108,7 @@ 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 device does not support the EID required for this eSIM profile. This eSIM profile has been installed on another device. This eSIM profile has been unreleased. This eSIM activation code is invalid. From 19f07de151b4c4df58afb30461c5de70ac2c4206 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 7 Sep 2025 10:51:33 -0400 Subject: [PATCH 3/6] Redo SimplifiedErrorHandling as SimplifiedErrorMessages ...and it shouldn't be an object, it should just be an enum class with a companion object. --- .../ui/wizard/SimplifiedErrorHandling.kt | 150 ----------------- .../ui/wizard/SimplifiedErrorMessages.kt | 154 ++++++++++++++++++ 2 files changed, 154 insertions(+), 150 deletions(-) delete mode 100644 app-common/src/main/java/im/angry/openeuicc/ui/wizard/SimplifiedErrorHandling.kt create mode 100644 app-common/src/main/java/im/angry/openeuicc/ui/wizard/SimplifiedErrorMessages.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 deleted file mode 100644 index 1968b4f..0000000 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/SimplifiedErrorHandling.kt +++ /dev/null @@ -1,150 +0,0 @@ -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 - ), - EIDNotSupported( - R.string.download_wizard_error_eid_not_supported, - R.string.download_wizard_error_suggest_contact_carrier - ), - 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 "2.1", ErrorCode.EIDNotSupported) - 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/java/im/angry/openeuicc/ui/wizard/SimplifiedErrorMessages.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/SimplifiedErrorMessages.kt new file mode 100644 index 0000000..8ce5740 --- /dev/null +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/SimplifiedErrorMessages.kt @@ -0,0 +1,154 @@ +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 + +enum class SimplifiedErrorMessages( + @StringRes val titleResId: Int, + @StringRes val suggestResId: Int? +) { + ICCIDAlreadyInUse( + 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 + ), + EIDNotSupported( + R.string.download_wizard_error_eid_not_supported, + R.string.download_wizard_error_suggest_contact_carrier + ), + 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 + ); + + companion object { + private val httpErrors = buildMap { + // Stage: AuthenticateClient + put("8.1" to "4.8", InsufficientMemory) + put("8.1.1" to "2.1", EIDNotSupported) + put("8.1.1" to "3.8", EIDMismatch) + put("8.2" to "1.2", UnreleasedProfile) + put("8.2.6" to "3.8", MatchingIDRefused) + put("8.8.5" to "6.4", ProfileRetriesExceeded) + + // Stage: GetBoundProfilePackage + put("8.2.7" to "2.2", ConfirmationCodeMissing) + put("8.2.7" to "3.8", ConfirmationCodeRefused) + put("8.2.7" to "6.4", ConfirmationCodeRetriesExceeded) + + // Stage: AuthenticateClient, GetBoundProfilePackage + put("8.8.5" to "4.10", ProfileExpired) + } + + fun fromDownloadError(exc: LocalProfileAssistant.ProfileDownloadException) = when { + exc.lpaErrorReason != "ES10B_ERROR_REASON_UNDEFINED" -> fromLPAErrorReason(exc.lpaErrorReason) + exc.lastHttpResponse?.rcode == 200 -> fromHTTPResponse(exc.lastHttpResponse!!) + exc.lastHttpException != null -> fromHTTPException(exc.lastHttpException!!) + exc.lastApduResponse != null -> fromAPDUResponse(exc.lastApduResponse!!) + else -> null + } + + private fun fromLPAErrorReason(reason: String) = when (reason) { + "ES10B_ERROR_REASON_UNSUPPORTED_CRT_VALUES" -> UnsupportedProfile + "ES10B_ERROR_REASON_UNSUPPORTED_REMOTE_OPERATION_TYPE" -> UnsupportedProfile + "ES10B_ERROR_REASON_UNSUPPORTED_PROFILE_CLASS" -> UnsupportedProfile + "ES10B_ERROR_REASON_INSTALL_FAILED_DUE_TO_ICCID_ALREADY_EXISTS_ON_EUICC" -> ICCIDAlreadyInUse + "ES10B_ERROR_REASON_INSTALL_FAILED_DUE_TO_INSUFFICIENT_MEMORY_FOR_PROFILE" -> InsufficientMemory + "ES10B_ERROR_REASON_INSTALL_FAILED_DUE_TO_INTERRUPTION" -> CardInternalError + "ES10B_ERROR_REASON_INSTALL_FAILED_DUE_TO_PE_PROCESSING_ERROR" -> CardInternalError + else -> null + } + + private fun fromHTTPResponse(httpResponse: net.typeblog.lpac_jni.HttpInterface.HttpResponse): SimplifiedErrorMessages? { + if (httpResponse.data.first().toInt() != '{'.code) return null + val response = JSONObject(httpResponse.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 fromHTTPException(exc: Exception) = when (exc) { + is SSLException -> TLSError + is UnknownHostException -> UnknownHost + is NoRouteToHostException -> NetworkUnreachable + is PortUnreachableException -> NetworkUnreachable + is SocketTimeoutException -> NetworkUnreachable + is SocketException -> exc.message + ?.contains("Connection reset", ignoreCase = true) + ?.let { if (it) NetworkUnreachable else null } + + else -> null + } + + private fun fromAPDUResponse(resp: ByteArray): SimplifiedErrorMessages? { + val isSuccess = resp.size >= 2 && + resp[resp.size - 2] == 0x90.toByte() && + resp[resp.size - 1] == 0x00.toByte() + if (isSuccess) return null + return CardInternalError + } + } +} From 8a41eabfb35c3a6d7fbd4989ea41a8bff7ba9b33 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 7 Sep 2025 10:59:20 -0400 Subject: [PATCH 4/6] Rework simplified error message strings --- app-common/src/main/res/values/strings.xml | 32 +++++++++++----------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index a41da62..3a9dda3 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -104,27 +104,27 @@ 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 device does not support the EID required for this eSIM profile. - 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. + This eSIM profile is already present on your eSIM chip. + Your eSIM chip does not have sufficient memory left to download the profile. + This eSIM profile is unsupported by your eSIM chip. + An error occurred in your eSIM chip. + The EID of your device or eSIM chip is unsupported by your carrier. + This eSIM profile has been downloaded on another device. + This eSIM profile has been revoked. + The activation code is invalid. + The maximum number of download attempts for the eSIM profile has been exceeded. + Confirmation code is required to download this profile. 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. + This eSIM profile has expired. + The maximum number of download attempts for the confirmation code has been exceeded. Unknown SM-DP+ address - The current network is unreachable + 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 + You are trying to reinstall an already downloaded eSIM profile + Please delete some unused eSIM profiles 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. + Please try again after connecting to a different network (e.g. switching between Wi-Fi and data). Logs have been saved to the selected path. Would you like to share the log through another app? From 943f2bd14ed6527a3b0d1b5034093a8f94190756 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 7 Sep 2025 14:39:52 -0400 Subject: [PATCH 5/6] Incorporate simplified error messages to the download progress UI --- .../wizard/DownloadWizardProgressFragment.kt | 55 ++++++++++++++++--- .../res/layout/download_progress_item.xml | 37 ++++++++++++- 2 files changed, 82 insertions(+), 10 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt index 342a687..29e87b0 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt @@ -43,18 +43,36 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep private data class ProgressItem( val titleRes: Int, - var state: ProgressState + var state: ProgressState, + var errorMessage: SimplifiedErrorMessages?, ) private val progressItems = arrayOf( - ProgressItem(R.string.download_wizard_progress_step_preparing, ProgressState.NotStarted), - ProgressItem(R.string.download_wizard_progress_step_connecting, ProgressState.NotStarted), + ProgressItem( + R.string.download_wizard_progress_step_preparing, + ProgressState.NotStarted, + null + ), + ProgressItem( + R.string.download_wizard_progress_step_connecting, + ProgressState.NotStarted, + null + ), ProgressItem( R.string.download_wizard_progress_step_authenticating, - ProgressState.NotStarted + ProgressState.NotStarted, + null ), - ProgressItem(R.string.download_wizard_progress_step_downloading, ProgressState.NotStarted), - ProgressItem(R.string.download_wizard_progress_step_finalizing, ProgressState.NotStarted) + ProgressItem( + R.string.download_wizard_progress_step_downloading, + ProgressState.NotStarted, + null + ), + ProgressItem( + R.string.download_wizard_progress_step_finalizing, + ProgressState.NotStarted, + null + ) ) private val adapter = ProgressItemAdapter() @@ -122,8 +140,13 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep // Change the state of the last InProgress item to success (or error) progressItems.forEachIndexed { index, progressItem -> if (progressItem.state == ProgressState.InProgress) { - progressItem.state = - if (state.downloadError == null) ProgressState.Done else ProgressState.Error + if (state.downloadError == null) { + progressItem.state = ProgressState.Done + } else { + progressItem.state = ProgressState.Error + progressItem.errorMessage = + SimplifiedErrorMessages.fromDownloadError(state.downloadError!!) + } } adapter.notifyItemChanged(index) @@ -197,9 +220,15 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep private val progressBar = root.requireViewById(R.id.download_progress_icon_progress) private val icon = root.requireViewById(R.id.download_progress_icon) + private val errorTitle = + root.requireViewById(R.id.download_progress_item_error_title) + private val errorSuggestion = + root.requireViewById(R.id.download_progress_item_error_suggestion) fun bind(item: ProgressItem) { title.text = getString(item.titleRes) + errorTitle.visibility = View.GONE + errorSuggestion.visibility = View.GONE when (item.state) { ProgressState.NotStarted -> { @@ -222,6 +251,16 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep progressBar.visibility = View.GONE icon.setImageResource(R.drawable.ic_error_outline) icon.visibility = View.VISIBLE + + if (item.errorMessage != null) { + errorTitle.visibility = View.VISIBLE + errorTitle.text = getString(item.errorMessage!!.titleResId) + + if (item.errorMessage!!.suggestResId != null) { + errorSuggestion.visibility = View.VISIBLE + errorSuggestion.text = getString(item.errorMessage!!.suggestResId!!) + } + } } } } diff --git a/app-common/src/main/res/layout/download_progress_item.xml b/app-common/src/main/res/layout/download_progress_item.xml index f1d0852..3012f44 100644 --- a/app-common/src/main/res/layout/download_progress_item.xml +++ b/app-common/src/main/res/layout/download_progress_item.xml @@ -12,7 +12,7 @@ android:textSize="14sp" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toTopOf="@id/download_progress_item_error_title" app:layout_constraintEnd_toStartOf="@id/download_progress_icon_container" app:layout_constrainedWidth="true" app:layout_constraintHorizontal_bias="0.0" /> @@ -23,7 +23,6 @@ android:layout_width="30dp" android:layout_height="30dp" app:layout_constraintTop_toTopOf="parent" - app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent"> + + + + \ No newline at end of file From 18d0521576df66be349f3f9ee4834022d951e261 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 7 Sep 2025 14:55:36 -0400 Subject: [PATCH 6/6] Fixup layout centering for download progress --- .../res/layout/download_progress_item.xml | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/app-common/src/main/res/layout/download_progress_item.xml b/app-common/src/main/res/layout/download_progress_item.xml index 3012f44..c59673b 100644 --- a/app-common/src/main/res/layout/download_progress_item.xml +++ b/app-common/src/main/res/layout/download_progress_item.xml @@ -1,29 +1,32 @@ + android:layout_height="wrap_content"> + app:layout_constraintBottom_toBottomOf="@id/download_progress_icon_container" + app:layout_constraintEnd_toStartOf="@id/download_progress_icon_container" + app:layout_constraintHorizontal_bias="0.0" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@id/download_progress_icon_container" + app:layout_constraintVertical_bias="0.5" /> + app:layout_constraintVertical_bias="0.0"> + app:layout_constraintHorizontal_bias="0.0" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/download_progress_item_title" /> + app:layout_constraintHorizontal_bias="0.0" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/download_progress_item_title" /> \ No newline at end of file