From 813eedc5644f29dab0208a650a2a359d34288f3e Mon Sep 17 00:00:00 2001 From: septs Date: Wed, 16 Jul 2025 01:31:06 +0800 Subject: [PATCH 01/12] 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 d526238..f28318f 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -103,6 +103,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 7c2157daa432a2d15542e91ef3d72d19f8007ea3 Mon Sep 17 00:00:00 2001 From: septs Date: Mon, 28 Jul 2025 02:35:56 +0200 Subject: [PATCH 02/12] chore: cleanup short string ids (#207) Reviewed-on: https://gitea.angry.im/PeterCxy/OpenEUICC/pulls/207 Co-authored-by: septs Co-committed-by: septs --- .../core/DefaultEuiccChannelFactory.kt | 4 +-- .../angry/openeuicc/ui/EuiccInfoActivity.kt | 12 ++++----- .../im/angry/openeuicc/ui/MainActivity.kt | 2 +- .../openeuicc/ui/NotificationsActivity.kt | 2 +- .../DownloadWizardSlotSelectFragment.kt | 5 ++-- .../java/im/angry/openeuicc/util/UiUtils.kt | 4 +-- .../main/res/menu/activity_isdr_aid_list.xml | 2 +- .../src/main/res/menu/activity_main.xml | 2 +- .../main/res/menu/activity_notifications.xml | 2 +- app-common/src/main/res/values-ja/strings.xml | 16 ++++++------ .../src/main/res/values-zh-rCN/strings.xml | 16 ++++++------ .../src/main/res/values-zh-rTW/strings.xml | 16 ++++++------ app-common/src/main/res/values/strings.xml | 25 +++++++++++-------- .../core/PrivilegedEuiccChannelFactory.kt | 3 +-- app/src/main/res/values-ja/strings.xml | 2 +- app/src/main/res/values-zh-rCN/strings.xml | 2 +- app/src/main/res/values-zh-rTW/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 18 files changed, 60 insertions(+), 59 deletions(-) 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 0de99b5..87a0eea 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 @@ -37,7 +37,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha ) try { return EuiccChannelImpl( - context.getString(R.string.omapi), + context.getString(R.string.channel_type_omapi), port, intrinsicChannelName = null, OmapiApduInterface( @@ -69,7 +69,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha ): EuiccChannel? { try { return EuiccChannelImpl( - context.getString(R.string.usb), + context.getString(R.string.channel_type_usb), FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)), intrinsicChannelName = ccidCtx.productName, UsbApduInterface( diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt index 4a34edd..bfbcbd8 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt @@ -36,7 +36,7 @@ private val RE_SAS = Regex( class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { companion object { - private val YES_NO = Pair(R.string.yes, R.string.no) + private val YES_NO = Pair(R.string.euicc_info_yes, R.string.euicc_info_no) } private lateinit var swipeRefresh: SwipeRefreshLayout @@ -69,7 +69,7 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { logicalSlotId = intent.getIntExtra("logicalSlotId", 0) val channelTitle = if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { - getString(R.string.usb) + getString(R.string.channel_type_usb) } else { appContainer.customizableTextProvider.formatInternalChannelName(logicalSlotId) } @@ -119,7 +119,7 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { channel.lpa.euiccInfo2?.let { info -> add(Item(R.string.euicc_info_sgp22_version, info.sgp22Version.toString())) add(Item(R.string.euicc_info_firmware_version, info.euiccFirmwareVersion.toString())) - add(Item(R.string.euicc_info_globalplatform_version, info.globalPlatformVersion.toString())) + add(Item(R.string.euicc_info_gp_version, info.globalPlatformVersion.toString())) add(Item(R.string.euicc_info_pp_version, info.ppVersion.toString())) info.sasAccreditationNumber.trim().takeIf(RE_SAS::matches) ?.let { add(Item(R.string.euicc_info_sas_accreditation_number, it.uppercase())) } @@ -131,14 +131,14 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { // FS.27 v2.0, Security Guidelines for UICC Profiles (Page 25 of 27, 2024-01-30) // https://www.gsma.com/solutions-and-impact/technologies/security/wp-content/uploads/2024/01/FS.27-Security-Guidelines-for-UICC-Credentials-v2.0-FINAL-23-July.pdf#page=25 val resId = when { - signers.isEmpty() -> R.string.unknown // the case is not mp, but it's is not common + signers.isEmpty() -> R.string.euicc_info_unknown // the case is not mp, but it's is not common PKID_GSMA_LIVE_CI.any(signers::contains) -> R.string.euicc_info_ci_gsma_live PKID_GSMA_TEST_CI.any(signers::contains) -> R.string.euicc_info_ci_gsma_test else -> R.string.euicc_info_ci_unknown } add(Item(R.string.euicc_info_ci_type, getString(resId))) } - val atr = channel.atr?.encodeHex() ?: getString(R.string.information_unavailable) + val atr = channel.atr?.encodeHex() ?: getString(R.string.euicc_info_unavailable) add(Item(R.string.euicc_info_atr, atr, copiedToastResId = R.string.toast_atr_copied)) } @@ -171,7 +171,7 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { fun bind(item: Item) { copiedToastResId = item.copiedToastResId title.setText(item.titleResId) - content.text = item.content ?: getString(R.string.unknown) + content.text = item.content ?: getString(R.string.euicc_info_unknown) } } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt index 01d0ab2..b42f4cf 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt @@ -174,7 +174,7 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { // If USB readers exist, add them at the very last // We use a wrapper fragment to handle logic specific to USB readers usbDevice?.let { - val productName = it.productName ?: getString(R.string.usb) + val productName = it.productName ?: getString(R.string.channel_type_usb) newPages.add(Page(EuiccChannelManager.USB_CHANNEL_ID, productName) { UsbCcidReaderFragment() }) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt index 21a2d40..07d5f13 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt @@ -60,7 +60,7 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker { // This is slightly different from the MainActivity logic // due to the length (we don't want to display the full USB product name) val channelTitle = if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { - getString(R.string.usb) + getString(R.string.channel_type_usb) } else { appContainer.customizableTextProvider.formatInternalChannelName(logicalSlotId) } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt index 28bc9f0..8097058 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt @@ -19,7 +19,6 @@ import im.angry.openeuicc.util.* import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch -import net.typeblog.lpac_jni.LocalProfileInfo class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardStepFragment() { companion object { @@ -187,12 +186,12 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt } title.text = if (item.logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { - item.intrinsicChannelName ?: root.context.getString(R.string.usb) + item.intrinsicChannelName ?: root.context.getString(R.string.channel_type_usb) } else { appContainer.customizableTextProvider.formatInternalChannelName(item.logicalSlotId) } eID.text = item.eID - activeProfile.text = item.enabledProfileName ?: root.context.getString(R.string.unknown) + activeProfile.text = item.enabledProfileName ?: root.context.getString(R.string.profile_no_enabled_profile) freeSpace.text = formatFreeSpace(item.freeSpace) checkBox.isChecked = adapter.currentSelectedIdx == idx } diff --git a/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt index a73d7fe..c7c859d 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt @@ -102,8 +102,8 @@ fun T.setupLogSaving( AlertDialog.Builder(context).apply { setMessage(R.string.logs_saved_message) - setNegativeButton(R.string.no) { _, _ -> } - setPositiveButton(R.string.yes) { _, _ -> + setNegativeButton(android.R.string.cancel) { _, _ -> } + setPositiveButton(android.R.string.ok) { _, _ -> val intent = Intent(Intent.ACTION_SEND).apply { type = "text/plain" clipData = ClipData.newUri(context.contentResolver, lastFileName, uri) diff --git a/app-common/src/main/res/menu/activity_isdr_aid_list.xml b/app-common/src/main/res/menu/activity_isdr_aid_list.xml index 32f178a..99492d6 100644 --- a/app-common/src/main/res/menu/activity_isdr_aid_list.xml +++ b/app-common/src/main/res/menu/activity_isdr_aid_list.xml @@ -9,7 +9,7 @@ \ No newline at end of file diff --git a/app-common/src/main/res/menu/activity_main.xml b/app-common/src/main/res/menu/activity_main.xml index 0e00292..c15663f 100644 --- a/app-common/src/main/res/menu/activity_main.xml +++ b/app-common/src/main/res/menu/activity_main.xml @@ -3,7 +3,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> diff --git a/app-common/src/main/res/menu/activity_notifications.xml b/app-common/src/main/res/menu/activity_notifications.xml index 87f96a6..b80e06e 100644 --- a/app-common/src/main/res/menu/activity_notifications.xml +++ b/app-common/src/main/res/menu/activity_notifications.xml @@ -4,6 +4,6 @@ \ No newline at end of file diff --git a/app-common/src/main/res/values-ja/strings.xml b/app-common/src/main/res/values-ja/strings.xml index e631d52..946625f 100644 --- a/app-common/src/main/res/values-ja/strings.xml +++ b/app-common/src/main/res/values-ja/strings.xml @@ -2,10 +2,10 @@ このアプリでアクセスできるリムーバブル eUICC カードがデバイス上で検出されていません。互換性のあるカード挿入または USB リーダーを接続してください。 この eSIM にはプロファイルがありません。 - 不明 - 情報がありません - ヘルプ - スロットを再読み込み + 不明 + 情報がありません + ヘルプ + スロットを再読み込み 論理スロット %d 有効済み 無効済み @@ -110,7 +110,7 @@ 製品ファームウェアバージョン SGP.22 バージョン eUICC OS バージョン - グローバルプラットフォームのバージョン + グローバルプラットフォームのバージョン SAS 認定番号 保護されたプロファイルのバージョン NVRAM の空き容量 (eSIM プロファイルストレージ) @@ -118,8 +118,8 @@ GSMA ライブ CI GSMA テスト CI 不明な eSIM CI - はい - いいえ + はい + いいえ 保存 %s のログ 開発者になるまであと %d ステップです。 @@ -167,6 +167,6 @@ この操作は、デフォルトでは非表示になっている危険な操作です。代わりに、すべての構成ファイルを手動で削除することもできます。 モデムに更新コマンドを送信 ISD-R AID リストのカスタマイズ - リセット + リセット ISD-R AID リスト diff --git a/app-common/src/main/res/values-zh-rCN/strings.xml b/app-common/src/main/res/values-zh-rCN/strings.xml index b0938d3..e947c6a 100644 --- a/app-common/src/main/res/values-zh-rCN/strings.xml +++ b/app-common/src/main/res/values-zh-rCN/strings.xml @@ -2,9 +2,9 @@ 在此设备上未检测到此应用程序可访问的可插拔 eUICC 卡。请插入兼容卡或 USB 读卡器。 此 eSIM 上还没有配置文件 - 未知 - 帮助 - 重新加载卡槽 + 未知 + 帮助 + 重新加载卡槽 逻辑卡槽 %d 已启用 已禁用 @@ -132,7 +132,7 @@ 可插拔 SGP.22 版本 eUICC OS 版本 - GlobalPlatform 版本 + GlobalPlatform 版本 SAS 认证号码 Protected Profile 版本 NVRAM 剩余空间 (eSIM 存储容量) @@ -140,8 +140,8 @@ GSMA 生产环境 CI GSMA 测试 CI 未知 eSIM CI - - + + 还有 %d 步成为开发者 你现在是开发者了! 语言 @@ -152,7 +152,7 @@ 在配置文件列表中包括非生产环境的配置文件 无视 SM-DP+ 的 TLS 证书 允许 RSP 服务器使用任意证书 - 无信息 + 无信息 输入的确认文本不匹配 此芯片已被擦除 正在擦除 eSIM 芯片 @@ -167,6 +167,6 @@ 此操作是默认隐藏的危险操作。作为替代方案,您可以手动删除所有配置文件。 向基带发送刷新命令 自定义 ISD-R AID 列表 - 重置 + 重置 ISD-R AID 列表 \ No newline at end of file diff --git a/app-common/src/main/res/values-zh-rTW/strings.xml b/app-common/src/main/res/values-zh-rTW/strings.xml index e0334c6..88cc011 100644 --- a/app-common/src/main/res/values-zh-rTW/strings.xml +++ b/app-common/src/main/res/values-zh-rTW/strings.xml @@ -2,9 +2,9 @@ 在此裝置上未檢測到此應用程式可訪問的可插拔 eUICC 卡。請插入相容卡或 USB 晶片讀卡機。 此 eSIM 上還沒有設定檔 - 未知 - 幫助 - 重新載入卡槽 + 未知 + 幫助 + 重新載入卡槽 虛擬卡槽 %d 已啟用 已停用 @@ -132,7 +132,7 @@ 可插拔 SGP.22 版本 eUICC OS 版本 - GlobalPlatform 版本 + GlobalPlatform 版本 SAS 認證號碼 Protected Profile 版本 NVRAM 剩餘空間 (eSIM 儲存容量) @@ -140,8 +140,8 @@ GSMA 生產環境 CI GSMA 測試 CI 未知 eSIM CI - - + + 還有 %d 步成為開發者 您現在是開發者了! 語言 @@ -152,7 +152,7 @@ 在設定檔列表中包括非生產環境的設定檔 忽略 SM-DP+ 的 TLS 證書 允許 RSP 伺服器使用任意證書 - 無資訊 + 無資訊 輸入的確認文字不匹配 此晶片已被擦除 正在擦除 eSIM 晶片 @@ -167,6 +167,6 @@ 此操作是預設隱藏的危險操作。作為替代方案,您可以手動刪除所有設定檔。 向基帶發送刷新命令 自訂 ISD-R AID 列表 - 重置 + 重置 ISD-R AID 列表 \ 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 d526238..e09da9f 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -2,14 +2,15 @@ No removable eUICC card accessible by this app is detected on this device. Insert a compatible card or a USB reader. No profiles (yet) on this eSIM. - Unknown - Information Unavailable - Help - Reload Slots + + Help + + Reload Slots + Unknown Logical Slot %d - USB - OpenMobile API (OMAPI) + USB + OpenMobile API (OMAPI) Enabled @@ -139,7 +140,7 @@ ISD-R AID SGP.22 Version eUICC OS Version - GlobalPlatform Version + GlobalPlatform Version SAS Accreditation Number Protected Profile Version Free NVRAM (eSIM profile storage) @@ -156,8 +157,11 @@ I CONFIRM TO ERASE THE CHIP WHOSE EID ENDS WITH %s AND UNDERSTAND THAT THIS IS IRREVERSIBLE Erase - Yes - No + + Yes + No + Unknown + Information Unavailable Save Logs at %s @@ -165,10 +169,9 @@ You are %d steps away from being a developer. You are now a developer! - Reset - ISD-R AID List Saved custom ISD-R AID list. + Reset Settings Notifications diff --git a/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt b/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt index 68eddef..978e886 100644 --- a/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt +++ b/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt @@ -2,7 +2,6 @@ package im.angry.openeuicc.core import android.content.Context import android.util.Log -import im.angry.openeuicc.OpenEuiccApplication import im.angry.openeuicc.R import im.angry.openeuicc.util.* import kotlinx.coroutines.flow.first @@ -32,7 +31,7 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto ) try { return EuiccChannelImpl( - context.getString(R.string.telephony_manager), + context.getString(R.string.channel_type_telephony_manager), port, intrinsicChannelName = null, TelephonyManagerApduInterface( diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index acc1728..fbf5c53 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -1,7 +1,7 @@ このデバイスで eUICC が見つかりません。\nデバイスによってはアプリのメニューからデュアル SIM を有効化する必要があります。 - TelephonyManager (特権) + TelephonyManager (特権) デュアル SIM DSDS の状態が切り替わりました。モデムが再起動するまでお待ちください。 このスロットは MEP (Multiple Enabled Profiles) をサポートしています。この機能を有効化または無効化するには「スロットマッピングツール」を使用してください。 diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index d6befc2..acd9a61 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -16,7 +16,7 @@ 您的设备支持 eSIM。要连接到移动网络,请下载运营商发布的 eSIM,或插入物理 SIM 卡。 跳过 下载 eSIM - TelephonyManager (特权) + TelephonyManager (特权) 全局使用 TelephonyManager 在默认情况下,可移除 eUICC 将仅使用 OMAPI。这与非特权模式 (EasyEUICC) 一致。在某些设备上 OMAPI 可能存在问题 -- 选择此选项以强制使用 TelephonyManager。 \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 10285d3..52b5aa8 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -16,7 +16,7 @@ 您的裝置支援 eSIM。要連線到行動網路,請下載電信業者釋出的 eSIM,或插入實體 SIM 卡。 跳過 下載 eSIM - TelephonyManager (特權) + TelephonyManager (特權) 全域使用 TelephonyManager 在預設情況下,可移除 eUICC 將僅使用 OMAPI。這與非特權模式 (EasyEUICC) 一致。在某些裝置上 OMAPI 可能有問題 -- 選擇此選項以強制使用 TelephonyManager。 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ddf17e4..2c26bc3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,7 +1,7 @@ OpenEUICC No eUICC found on this device.\nOn some devices, you may need to enable dual SIM first in the menu of this app. - TelephonyManager (Privileged) + TelephonyManager (Privileged) Dual SIM From 72ec20f82431b34f4c1ba438b32379be4edd6838 Mon Sep 17 00:00:00 2001 From: septs Date: Sun, 10 Aug 2025 22:03:25 +0200 Subject: [PATCH 03/12] feat: 16k page sizes (#211) see https://developer.android.com/guide/practices/page-sizes Reviewed-on: https://gitea.angry.im/PeterCxy/OpenEUICC/pulls/211 Co-authored-by: septs Co-committed-by: septs --- libs/lpac-jni/src/main/jni/Application.mk | 3 ++- libs/lpac-jni/src/main/jni/lpac-jni.mk | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/libs/lpac-jni/src/main/jni/Application.mk b/libs/lpac-jni/src/main/jni/Application.mk index 2112196..c1d3766 100644 --- a/libs/lpac-jni/src/main/jni/Application.mk +++ b/libs/lpac-jni/src/main/jni/Application.mk @@ -1,4 +1,5 @@ APP_ABI := all APP_SHORT_COMMANDS := true APP_CFLAGS := -Wno-compound-token-split-by-macro -APP_LDFLAGS := -Wl,--build-id=none -z muldefs \ No newline at end of file +APP_LDFLAGS := -Wl,--build-id=none -z muldefs +APP_SUPPORT_FLEXIBLE_PAGE_SIZES := true diff --git a/libs/lpac-jni/src/main/jni/lpac-jni.mk b/libs/lpac-jni/src/main/jni/lpac-jni.mk index c0bcee7..dad173c 100644 --- a/libs/lpac-jni/src/main/jni/lpac-jni.mk +++ b/libs/lpac-jni/src/main/jni/lpac-jni.mk @@ -1,4 +1,5 @@ LOCAL_PATH := $(call my-dir) +LOCAL_LDFLAGS += "-Wl,-z,max-page-size=16384" # function to find all *.c files under a directory define all-c-files-under From deb0a372b14caacdf5d3e509135efb9b19c95f10 Mon Sep 17 00:00:00 2001 From: septs Date: Sun, 10 Aug 2025 22:07:10 +0200 Subject: [PATCH 04/12] feat: allow copy app version and source code url (#216) Reviewed-on: https://gitea.angry.im/PeterCxy/OpenEUICC/pulls/216 Co-authored-by: septs Co-committed-by: septs --- app-common/src/main/res/xml/pref_settings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app-common/src/main/res/xml/pref_settings.xml b/app-common/src/main/res/xml/pref_settings.xml index 690a120..17505e1 100644 --- a/app-common/src/main/res/xml/pref_settings.xml +++ b/app-common/src/main/res/xml/pref_settings.xml @@ -96,12 +96,14 @@ Date: Sun, 10 Aug 2025 22:07:25 +0200 Subject: [PATCH 05/12] fix: aid list (#217) Reviewed-on: https://gitea.angry.im/PeterCxy/OpenEUICC/pulls/217 Co-authored-by: septs Co-committed-by: septs --- .../src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt index 5f4aec4..464aeee 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt @@ -50,9 +50,12 @@ internal object PreferenceConstants { # eUICC standard $EUICC_DEFAULT_ISDR_AID - # eSTK.me + # ESTKme AUX (deprecated, use SE0 instead) A06573746B6D65FFFFFFFF4953442D52 + # ESTKme SE0 + A06573746B6D65FFFF4953442D522030 + # eSIM.me A0000005591010000000008900000300 From a8b7482afb98236c591912dc24524da7543bbf59 Mon Sep 17 00:00:00 2001 From: septs Date: Sun, 10 Aug 2025 22:08:31 +0200 Subject: [PATCH 06/12] feat: version name suffix (#215) see https://developer.android.com/build/build-variants Reviewed-on: https://gitea.angry.im/PeterCxy/OpenEUICC/pulls/215 Co-authored-by: septs Co-committed-by: septs --- app-unpriv/build.gradle.kts | 3 +++ app/build.gradle.kts | 3 +++ .../src/main/kotlin/im/angry/openeuicc/build/Versioning.kt | 6 +++--- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app-unpriv/build.gradle.kts b/app-unpriv/build.gradle.kts index 66a60b4..d59fe71 100644 --- a/app-unpriv/build.gradle.kts +++ b/app-unpriv/build.gradle.kts @@ -27,6 +27,9 @@ android { } buildTypes { + defaultConfig { + versionNameSuffix = "-unpriv" + } release { isMinifyEnabled = false proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b9c2100..69e0db5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -23,6 +23,9 @@ android { } buildTypes { + defaultConfig { + versionNameSuffix = "-priv" + } release { isMinifyEnabled = false proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") diff --git a/buildSrc/src/main/kotlin/im/angry/openeuicc/build/Versioning.kt b/buildSrc/src/main/kotlin/im/angry/openeuicc/build/Versioning.kt index 24f0235..ef07c43 100644 --- a/buildSrc/src/main/kotlin/im/angry/openeuicc/build/Versioning.kt +++ b/buildSrc/src/main/kotlin/im/angry/openeuicc/build/Versioning.kt @@ -16,7 +16,7 @@ val Project.gitVersionCode: Int standardOutput = stdout } stdout.toString("utf-8").trim('\n').toInt() - } catch (e: Exception) { + } catch (_: Exception) { 0 } @@ -29,7 +29,7 @@ val Project.gitVersionName: String standardOutput = stdout } stdout.toString("utf-8").trim('\n') - } catch (e: Exception) { + } catch (_: Exception) { "Unknown" } @@ -38,7 +38,7 @@ class MyVersioningPlugin: Plugin { target.configure { defaultConfig { versionCode = target.gitVersionCode - versionName = target.gitVersionName + versionName = target.gitVersionName.removePrefix("unpriv-") } applicationVariants.all { From 4d8b8e8fb5489dcab8b4c0419e75578aed8f1b71 Mon Sep 17 00:00:00 2001 From: septs Date: Fri, 15 Aug 2025 00:18:41 +0200 Subject: [PATCH 07/12] fix: update .gitignore to ignore all deployment target files (#222) Reviewed-on: https://gitea.angry.im/PeterCxy/OpenEUICC/pulls/222 Co-authored-by: septs Co-committed-by: septs --- .idea/.gitignore | 2 +- .idea/deploymentTargetSelector.xml | 37 ------------------------------ 2 files changed, 1 insertion(+), 38 deletions(-) delete mode 100644 .idea/deploymentTargetSelector.xml diff --git a/.idea/.gitignore b/.idea/.gitignore index b7c2402..2e12995 100644 --- a/.idea/.gitignore +++ b/.idea/.gitignore @@ -2,7 +2,7 @@ /caches /libraries /assetWizardSettings.xml -/deploymentTargetDropDown.xml +/deploymentTarget*.xml /gradle.xml /misc.xml /modules.xml diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml deleted file mode 100644 index e40be60..0000000 --- a/.idea/deploymentTargetSelector.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From 7e7f5c2b055e3c51aa95556cbfbda6e25aec3259 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Fri, 15 Aug 2025 21:06:23 -0400 Subject: [PATCH 08/12] feat: Magisk module builds on CI Much of this is taken from AndroPlus's Magisk module repo, thanks! There's still no release builds for privileged OpenEUICC and the Magisk module; this is still intentional (for now). However, it is possible to produce a release Magisk zip locally via the `:app:assembleReleaseMagiskModule` task. --- .forgejo/workflows/build-debug.yml | 15 +++- app/build.gradle.kts | 59 +++++++++++++++ app/magisk/customize.sh | 9 +++ app/magisk/module_installer.sh | 33 +++++++++ app/magisk/uninstall.sh | 1 + .../kotlin/im/angry/openeuicc/build/Magisk.kt | 74 +++++++++++++++++++ 6 files changed, 188 insertions(+), 3 deletions(-) create mode 100644 app/magisk/customize.sh create mode 100644 app/magisk/module_installer.sh create mode 100644 app/magisk/uninstall.sh create mode 100644 buildSrc/src/main/kotlin/im/angry/openeuicc/build/Magisk.kt diff --git a/.forgejo/workflows/build-debug.yml b/.forgejo/workflows/build-debug.yml index 660dabc..0818b8b 100644 --- a/.forgejo/workflows/build-debug.yml +++ b/.forgejo/workflows/build-debug.yml @@ -33,14 +33,23 @@ jobs: uses: https://gitea.angry.im/actions/setup-android@v3 - name: Build Debug APKs - run: ./gradlew --no-daemon assembleDebug + run: ./gradlew --no-daemon assembleDebug :app:assembleDebugMagiskModuleDir - name: Copy Artifacts - run: find . -name 'app*-debug.apk' -exec cp {} . \; + run: | + find . -name 'app*-debug.apk' -exec cp {} . \; + cp -r app/build/magisk/debug ./magisk-debug - - name: Upload Artifacts + - name: Upload APK Artifacts uses: https://gitea.angry.im/actions/upload-artifact@v3 with: name: Debug APKs compression-level: 0 path: app*-debug.apk + + - name: Upload Magisk Artifacts + uses: https://gitea.angry.im/actions/upload-artifact@v3 + with: + name: magisk-debug + compression-level: 0 + path: magisk-debug diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 69e0db5..ddf92af 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,4 @@ +import com.android.build.gradle.internal.api.ApkVariantOutputImpl import im.angry.openeuicc.build.* plugins { @@ -48,4 +49,62 @@ dependencies { testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") +} + +val modulePropsTemplate = mutableMapOf( + "id" to android.defaultConfig.applicationId!!, + "name" to "OpenEUICC", + "version" to android.defaultConfig.versionName!!, + "versionCode" to "${android.defaultConfig.versionCode}", + "author" to "OpenEUICC authors", + "description" to "OpenEUICC is an open-source app that provides system-level eSIM integration." +) + +val moduleCustomizeScript = project.file("magisk/customize.sh").readText() + .replace("{APK_NAME}", "OpenEUICC") + .replace("{PKG_NAME}", android.defaultConfig.applicationId!!) + +val moduleUninstallScript = project.file("magisk/uninstall.sh").readText() + .replace("{PKG_NAME}", android.defaultConfig.applicationId!!) + +tasks.register("assembleDebugMagiskModuleDir") { + variant = "debug" + appName = "OpenEUICC" + permsFile = project.rootProject.file("privapp_whitelist_im.angry.openeuicc.xml") + moduleInstaller = project.file("magisk/module_installer.sh") + moduleCustomizeScriptText = moduleCustomizeScript + moduleUninstallScriptText = moduleUninstallScript + moduleProp = modulePropsTemplate.let { + it["description"] = "(debug build) ${it["description"]}" + it["versionCode"] = "${(android.applicationVariants.find { it.name == "debug" }!!.outputs.first() as ApkVariantOutputImpl).versionCodeOverride}" + it + } + dependsOn("assembleDebug") +} + +tasks.register("assembleDebugMagiskModule") { + dependsOn("assembleDebugMagiskModuleDir") + from((tasks.getByName("assembleDebugMagiskModuleDir") as MagiskModuleDirTask).outputDir) + archiveFileName = "magisk-debug.zip" + destinationDirectory = project.layout.buildDirectory.dir("magisk") + entryCompression = ZipEntryCompression.STORED +} + +tasks.register("assembleReleaseMagiskModuleDir") { + variant = "release" + appName = "OpenEUICC" + permsFile = project.rootProject.file("privapp_whitelist_im.angry.openeuicc.xml") + moduleInstaller = project.file("magisk/module_installer.sh") + moduleCustomizeScriptText = moduleCustomizeScript + moduleUninstallScriptText = moduleUninstallScript + moduleProp = modulePropsTemplate + dependsOn("assembleRelease") +} + +tasks.register("assembleReleaseMagiskModule") { + dependsOn("assembleReleaseMagiskModuleDir") + from((tasks.getByName("assembleReleaseMagiskModuleDir") as MagiskModuleDirTask).outputDir) + archiveFileName = "magisk-release.zip" + destinationDirectory = project.layout.buildDirectory.dir("magisk") + entryCompression = ZipEntryCompression.STORED } \ No newline at end of file diff --git a/app/magisk/customize.sh b/app/magisk/customize.sh new file mode 100644 index 0000000..707b401 --- /dev/null +++ b/app/magisk/customize.sh @@ -0,0 +1,9 @@ +TMP_FILE="$TMPDIR/{APK_NAME}" + +chmod u+x "$MODPATH/uninstall.sh" +cp "$MODPATH/system/system_ext/{APK_NAME}/{APK_NAME}.apk" "$TMP_FILE" + +pm install -r "$TMP_FILE" +rm -f "$TMP_FILE" + +pm grant "{PKG_NAME}" android.permission.READ_PHONE_STATE \ No newline at end of file diff --git a/app/magisk/module_installer.sh b/app/magisk/module_installer.sh new file mode 100644 index 0000000..28b48e5 --- /dev/null +++ b/app/magisk/module_installer.sh @@ -0,0 +1,33 @@ +#!/sbin/sh + +################# +# Initialization +################# + +umask 022 + +# echo before loading util_functions +ui_print() { echo "$1"; } + +require_new_magisk() { + ui_print "*******************************" + ui_print " Please install Magisk v20.4+! " + ui_print "*******************************" + exit 1 +} + +######################### +# Load util_functions.sh +######################### + +OUTFD=$2 +ZIPFILE=$3 + +mount /data 2>/dev/null + +[ -f /data/adb/magisk/util_functions.sh ] || require_new_magisk +. /data/adb/magisk/util_functions.sh +[ $MAGISK_VER_CODE -lt 20400 ] && require_new_magisk + +install_module +exit 0 diff --git a/app/magisk/uninstall.sh b/app/magisk/uninstall.sh new file mode 100644 index 0000000..1eb0200 --- /dev/null +++ b/app/magisk/uninstall.sh @@ -0,0 +1 @@ +pm uninstall "{PKG_NAME}" \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/im/angry/openeuicc/build/Magisk.kt b/buildSrc/src/main/kotlin/im/angry/openeuicc/build/Magisk.kt new file mode 100644 index 0000000..6245d8c --- /dev/null +++ b/buildSrc/src/main/kotlin/im/angry/openeuicc/build/Magisk.kt @@ -0,0 +1,74 @@ +package im.angry.openeuicc.build + +import org.gradle.api.DefaultTask +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction +import java.io.File + +abstract class MagiskModuleDirTask : DefaultTask() { + @get:Input + abstract val variant : Property + + @get:Input + abstract val appName : Property + + @get:InputFile + abstract val permsFile : Property + + @get:InputFile + abstract val moduleInstaller : Property + + @get:Input + abstract val moduleCustomizeScriptText : Property + + @get:Input + abstract val moduleUninstallScriptText : Property + + @get:Input + abstract val moduleProp : MapProperty + + @InputDirectory + val inputDir = variant.map { project.layout.buildDirectory.dir("outputs/apk/${it}") } + + @OutputDirectory + val outputDir = variant.map { project.layout.buildDirectory.dir("magisk/${it}") } + + @TaskAction + fun build() { + val dir = outputDir.get().get() + project.mkdir(dir) + val systemExtDir = dir.dir("system/system_ext") + val permDir = dir.dir("system/system_ext/etc/permissions") + val appDir = systemExtDir.dir("priv-app/${appName.get()}") + val metaInfDir = dir.dir("META-INF/com/google/android") + project.mkdir(systemExtDir) + project.mkdir(metaInfDir) + project.mkdir(appDir) + project.mkdir(permDir) + project.copy { + into(appDir) + from(inputDir) { + include("app-${variant.get()}.apk") + rename("app-${variant.get()}.apk", "${appName.get()}.apk") + } + } + project.copy { + from(permsFile) + into(permDir) + } + project.copy { + from(moduleInstaller) + into(metaInfDir) + rename(".*", "update-binary") + } + dir.file("customize.sh").asFile.writeText(moduleCustomizeScriptText.get()) + dir.file("uninstall.sh").asFile.writeText(moduleUninstallScriptText.get()) + metaInfDir.file("updater-script").asFile.writeText("# MAGISK") + dir.file("module.prop").asFile.writeText(moduleProp.get().map { (k, v) -> "$k=$v" }.joinToString("\n")) + } +} \ No newline at end of file From 27b7e50b9759e0295aaf85cffbd2bb1aa284be2b Mon Sep 17 00:00:00 2001 From: septs Date: Sun, 17 Aug 2025 02:37:16 +0200 Subject: [PATCH 09/12] refactor: simplify developer options click handling and toast messages (#221) Reviewed-on: https://gitea.angry.im/PeterCxy/OpenEUICC/pulls/221 Co-authored-by: septs Co-committed-by: septs --- .../im/angry/openeuicc/ui/SettingsFragment.kt | 41 +++++++------------ 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt index 6554142..509cf74 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt @@ -34,7 +34,7 @@ open class SettingsFragment: PreferenceFragmentCompat() { // Show / hide developer preference based on whether it is enabled lifecycleScope.launch { preferenceRepository.developerOptionsEnabledFlow - .onEach { developerPref.isVisible = it } + .onEach(developerPref::setVisible) .collect() } @@ -100,41 +100,30 @@ open class SettingsFragment: PreferenceFragmentCompat() { @Suppress("UNUSED_PARAMETER") private fun onAppVersionClicked(pref: Preference): Boolean { if (developerPref.isVisible) return false + val now = System.currentTimeMillis() - if (now - lastClickTimestamp >= 1000) { - numClicks = 1 - } else { - numClicks++ - } + numClicks = if (now - lastClickTimestamp >= 1000) 1 else numClicks + 1 lastClickTimestamp = now - if (numClicks == 7) { - lifecycleScope.launch { - preferenceRepository.developerOptionsEnabledFlow.updatePreference(true) - - lastToast?.cancel() - Toast.makeText( - requireContext(), - R.string.developer_options_enabled, - Toast.LENGTH_SHORT - ).show() - } - } else if (numClicks > 1) { - lastToast?.cancel() - lastToast = Toast.makeText( - requireContext(), - getString(R.string.developer_options_steps, 7 - numClicks), - Toast.LENGTH_SHORT - ) - lastToast!!.show() + lifecycleScope.launch { + preferenceRepository.developerOptionsEnabledFlow.updatePreference(numClicks >= 7) } + val toastText = when { + numClicks == 7 -> getString(R.string.developer_options_enabled) + numClicks > 1 -> getString(R.string.developer_options_steps, 7 - numClicks) + else -> return true + } + + lastToast?.cancel() + lastToast = Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT) + lastToast!!.show() return true } protected fun CheckBoxPreference.bindBooleanFlow(flow: PreferenceFlowWrapper) { lifecycleScope.launch { - flow.collect { isChecked = it } + flow.collect(::setChecked) } setOnPreferenceChangeListener { _, newValue -> From 9e40232ed03dce58e35ca1758246d382ffec78a5 Mon Sep 17 00:00:00 2001 From: septs Date: Sun, 17 Aug 2025 02:45:48 +0200 Subject: [PATCH 10/12] feat: es10x mss as preference (#213) Reviewed-on: https://gitea.angry.im/PeterCxy/OpenEUICC/pulls/213 Co-authored-by: septs Co-committed-by: septs --- .../core/DefaultEuiccChannelFactory.kt | 88 +++++++++---------- .../angry/openeuicc/core/EuiccChannelImpl.kt | 12 ++- .../im/angry/openeuicc/ui/SettingsFragment.kt | 20 ++++- .../angry/openeuicc/util/PreferenceUtils.kt | 3 + app-common/src/main/res/values/strings.xml | 10 +++ app-common/src/main/res/xml/pref_settings.xml | 8 ++ .../core/PrivilegedEuiccChannelFactory.kt | 1 + 7 files changed, 89 insertions(+), 53 deletions(-) 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 87a0eea..78a8c3f 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 @@ -21,7 +21,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha override suspend fun tryOpenEuiccChannel( port: UiccPortInfoCompat, isdrAid: ByteArray - ): EuiccChannel? { + ): EuiccChannel? = try { if (port.portIndex != 0) { Log.w( DefaultEuiccChannelManager.TAG, @@ -35,58 +35,52 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha DefaultEuiccChannelManager.TAG, "Trying OMAPI for physical slot ${port.card.physicalSlotIndex}" ) - try { - return EuiccChannelImpl( - context.getString(R.string.channel_type_omapi), + EuiccChannelImpl( + context.getString(R.string.channel_type_omapi), + port, + intrinsicChannelName = null, + OmapiApduInterface( + seService!!, port, - intrinsicChannelName = null, - OmapiApduInterface( - seService!!, - port, - context.preferenceRepository.verboseLoggingFlow - ), - isdrAid, - context.preferenceRepository.verboseLoggingFlow, - context.preferenceRepository.ignoreTLSCertificateFlow, - ).also { - Log.i(DefaultEuiccChannelManager.TAG, "Is OMAPI channel, setting MSS to 60") - it.lpa.setEs10xMss(60) - } - } catch (_: IllegalArgumentException) { - // Failed - Log.w( - DefaultEuiccChannelManager.TAG, - "OMAPI APDU interface unavailable for physical slot ${port.card.physicalSlotIndex} with ISD-R AID: ${isdrAid.encodeHex()}." - ) - } - - return null + context.preferenceRepository.verboseLoggingFlow + ), + isdrAid, + context.preferenceRepository.verboseLoggingFlow, + context.preferenceRepository.ignoreTLSCertificateFlow, + context.preferenceRepository.es10xMssFlow, + ) + } catch (_: IllegalArgumentException) { + // Failed + Log.w( + DefaultEuiccChannelManager.TAG, + "OMAPI APDU interface unavailable for physical slot ${port.card.physicalSlotIndex} with ISD-R AID: ${isdrAid.encodeHex()}." + ) + null } override fun tryOpenUsbEuiccChannel( ccidCtx: UsbCcidContext, isdrAid: ByteArray - ): EuiccChannel? { - try { - return EuiccChannelImpl( - context.getString(R.string.channel_type_usb), - FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)), - intrinsicChannelName = ccidCtx.productName, - UsbApduInterface( - ccidCtx - ), - isdrAid, - context.preferenceRepository.verboseLoggingFlow, - context.preferenceRepository.ignoreTLSCertificateFlow, - ) - } catch (_: IllegalArgumentException) { - // Failed - Log.w( - DefaultEuiccChannelManager.TAG, - "USB APDU interface unavailable for ISD-R AID: ${isdrAid.encodeHex()}." - ) - } - return null + ): EuiccChannel? = try { + EuiccChannelImpl( + context.getString(R.string.channel_type_usb), + FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)), + intrinsicChannelName = ccidCtx.productName, + UsbApduInterface( + ccidCtx + ), + isdrAid, + context.preferenceRepository.verboseLoggingFlow, + context.preferenceRepository.ignoreTLSCertificateFlow, + context.preferenceRepository.es10xMssFlow, + ) + } catch (_: IllegalArgumentException) { + // Failed + Log.w( + DefaultEuiccChannelManager.TAG, + "USB APDU interface unavailable for ISD-R AID: ${isdrAid.encodeHex()}." + ) + null } override fun cleanup() { diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt index 2a33c20..eaec522 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt @@ -1,8 +1,9 @@ package im.angry.openeuicc.core import im.angry.openeuicc.util.UiccPortInfoCompat -import im.angry.openeuicc.util.decodeHex import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking import net.typeblog.lpac_jni.ApduInterface import net.typeblog.lpac_jni.LocalProfileAssistant import net.typeblog.lpac_jni.impl.HttpInterfaceImpl @@ -15,7 +16,8 @@ class EuiccChannelImpl( override val apduInterface: ApduInterface, override val isdrAid: ByteArray, verboseLoggingFlow: Flow, - ignoreTLSCertificateFlow: Flow + ignoreTLSCertificateFlow: Flow, + es10xMssFlow: Flow, ) : EuiccChannel { override val slotId = port.card.physicalSlotIndex override val logicalSlotId = port.logicalSlotIndex @@ -25,8 +27,10 @@ class EuiccChannelImpl( LocalProfileAssistantImpl( isdrAid, apduInterface, - HttpInterfaceImpl(verboseLoggingFlow, ignoreTLSCertificateFlow) - ) + HttpInterfaceImpl(verboseLoggingFlow, ignoreTLSCertificateFlow), + ).also { + it.setEs10xMss(runBlocking { es10xMssFlow.first().toByte() }) + } override val atr: ByteArray? get() = (apduInterface as? ApduInterfaceAtrProvider)?.atr diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt index 509cf74..7a717ac 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt @@ -8,6 +8,7 @@ import android.provider.Settings import android.widget.Toast import androidx.lifecycle.lifecycleScope import androidx.preference.CheckBoxPreference +import androidx.preference.ListPreference import androidx.preference.Preference import androidx.preference.PreferenceCategory import androidx.preference.PreferenceFragmentCompat @@ -16,7 +17,6 @@ import im.angry.openeuicc.util.* import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking open class SettingsFragment: PreferenceFragmentCompat() { private lateinit var developerPref: PreferenceCategory @@ -84,6 +84,9 @@ open class SettingsFragment: PreferenceFragmentCompat() { requirePreference("pref_developer_euicc_memory_reset") .bindBooleanFlow(preferenceRepository.euiccMemoryResetFlow) + requirePreference("pref_developer_es10x_mss") + .bindIntFlow(preferenceRepository.es10xMssFlow, 63) + requirePreference("pref_developer_isdr_aid_list").apply { intent = Intent(requireContext(), IsdrAidListActivity::class.java) } @@ -127,13 +130,26 @@ open class SettingsFragment: PreferenceFragmentCompat() { } setOnPreferenceChangeListener { _, newValue -> - runBlocking { + lifecycleScope.launch { flow.updatePreference(newValue as Boolean) } true } } + private fun ListPreference.bindIntFlow(flow: PreferenceFlowWrapper, defaultValue: Int) { + lifecycleScope.launch { + flow.collect { value = it.toString() } + } + + setOnPreferenceChangeListener { _, newValue -> + lifecycleScope.launch { + flow.updatePreference((newValue as String).toIntOrNull() ?: defaultValue) + } + true + } + } + protected fun mergePreferenceOverlay(overlayKey: String, targetKey: String) { val overlayCat = requirePreference(overlayKey) val targetCat = requirePreference(targetKey) diff --git a/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt index 464aeee..2fef3db 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt @@ -5,6 +5,7 @@ import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import androidx.fragment.app.Fragment @@ -38,6 +39,7 @@ internal object PreferenceKeys { val IGNORE_TLS_CERTIFICATE = booleanPreferencesKey("ignore_tls_certificate") val EUICC_MEMORY_RESET = booleanPreferencesKey("euicc_memory_reset") val ISDR_AID_LIST = stringPreferencesKey("isdr_aid_list") + val ES10X_MSS = intPreferencesKey("es10x_mss") } const val EUICC_DEFAULT_ISDR_AID = "A0000005591010FFFFFFFF8900000100" @@ -89,6 +91,7 @@ open class PreferenceRepository(private val context: Context) { PreferenceConstants.DEFAULT_AID_LIST, { Base64.getEncoder().encodeToString(it.encodeToByteArray()) }, { Base64.getDecoder().decode(it).decodeToString() }) + val es10xMssFlow = bindFlow(PreferenceKeys.ES10X_MSS, 63) protected fun bindFlow( key: Preferences.Key, diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index e09da9f..ae0700b 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -200,6 +200,16 @@ Accept any TLS certificate used by the RSP server Allow erasing eUICC This is a dangerous operation and hidden by default. As an alternative, you can delete all profiles manually. + ES10x MSS + Global ES10x MSS + + High Speed + Compatibility Mode + + + 255 + 63 + Customize ISD-R AID list Some brands of removable eUICCs may use their own non-standard ISD-R AID, rendering them inaccessible to third-party apps. We can attempt to use non-standard AIDs added in this list, but there is no guarantee that they will work. Info diff --git a/app-common/src/main/res/xml/pref_settings.xml b/app-common/src/main/res/xml/pref_settings.xml index 17505e1..831b04d 100644 --- a/app-common/src/main/res/xml/pref_settings.xml +++ b/app-common/src/main/res/xml/pref_settings.xml @@ -81,6 +81,14 @@ app:summary="@string/pref_developer_euicc_memory_reset_desc" app:title="@string/pref_developer_euicc_memory_reset" /> + + Date: Sun, 17 Aug 2025 09:41:15 -0400 Subject: [PATCH 11/12] Add updateJson to Magisk module props --- app/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ddf92af..4012227 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -77,6 +77,7 @@ tasks.register("assembleDebugMagiskModuleDir") { moduleProp = modulePropsTemplate.let { it["description"] = "(debug build) ${it["description"]}" it["versionCode"] = "${(android.applicationVariants.find { it.name == "debug" }!!.outputs.first() as ApkVariantOutputImpl).versionCodeOverride}" + it["updateJson"] = "https://openeuicc.com/magisk/magisk-debug.json" it } dependsOn("assembleDebug") From f167d059dccb8bb1de48543fd89a1fa0820321f9 Mon Sep 17 00:00:00 2001 From: septs Date: Mon, 18 Aug 2025 09:57:15 +0800 Subject: [PATCH 12/12] 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?