From 4b737a6988d90b6251981a1a90fa177a1978f28e Mon Sep 17 00:00:00 2001 From: septs Date: Wed, 16 Jul 2025 01:31:06 +0800 Subject: [PATCH 01/19] 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? From 677b69cedfcae37557ca5626bb1741987ffb0a10 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 20 Jul 2025 10:29:27 -0400 Subject: [PATCH 02/19] feat: quick compatibility check Co-authored-by: septs --- app-unpriv/src/main/AndroidManifest.xml | 5 + .../openeuicc/di/UnprivilegedAppContainer.kt | 5 + .../di/UnprivilegedUiComponentFactory.kt | 7 +- .../ui/QuickCompatibilityActivity.kt | 25 ++++ .../ui/QuickCompatibilityFragment.kt | 140 ++++++++++++++++++ .../openeuicc/ui/UnprivilegedMainActivity.kt | 13 +- .../openeuicc/util/CompatibilityCheck.kt | 6 +- .../util/UnprivilegedPreferenceRepository.kt | 14 ++ .../angry/openeuicc/util/UnprivilegedUtils.kt | 6 + .../layout/activity_quick_compatibility.xml | 16 ++ .../layout/fragment_quick_compatibility.xml | 53 +++++++ app-unpriv/src/main/res/values/strings.xml | 10 ++ 12 files changed, 294 insertions(+), 6 deletions(-) create mode 100644 app-unpriv/src/main/java/im/angry/openeuicc/ui/QuickCompatibilityActivity.kt create mode 100644 app-unpriv/src/main/java/im/angry/openeuicc/ui/QuickCompatibilityFragment.kt create mode 100644 app-unpriv/src/main/java/im/angry/openeuicc/util/UnprivilegedPreferenceRepository.kt create mode 100644 app-unpriv/src/main/java/im/angry/openeuicc/util/UnprivilegedUtils.kt create mode 100644 app-unpriv/src/main/res/layout/activity_quick_compatibility.xml create mode 100644 app-unpriv/src/main/res/layout/fragment_quick_compatibility.xml diff --git a/app-unpriv/src/main/AndroidManifest.xml b/app-unpriv/src/main/AndroidManifest.xml index ce985cd..ba37079 100644 --- a/app-unpriv/src/main/AndroidManifest.xml +++ b/app-unpriv/src/main/AndroidManifest.xml @@ -23,6 +23,11 @@ + + = emptyList() + ) + } + + private val conclusion: TextView by lazy { + requireView().requireViewById(R.id.quick_availability_conclusion) + } + + private val resultSlots: TextView by lazy { + requireView().requireViewById(R.id.quick_availability_result_slots) + } + + private val resultNotes: TextView by lazy { + requireView().requireViewById(R.id.quick_availability_result_notes) + } + + private val hidden: CheckBox by lazy { + requireView().requireViewById(R.id.quick_availability_hidden) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = inflater.inflate(R.layout.fragment_quick_compatibility, container, false).apply { + requireViewById(R.id.quick_availability_device_information) + .text = formatDeviceInformation() + requireViewById