From 1d67fa5cfa6d22dd1bbed4342916c8493d60dfc1 Mon Sep 17 00:00:00 2001 From: septs Date: Tue, 4 Mar 2025 03:12:40 +0100 Subject: [PATCH 1/3] feat: detect used product (#147) Reviewed-on: https://gitea.angry.im/PeterCxy/OpenEUICC/pulls/147 Co-authored-by: septs Co-committed-by: septs --- .idea/.gitignore | 1 + .../angry/openeuicc/ui/EuiccInfoActivity.kt | 43 +++++++--------- .../im/angry/openeuicc/vendored/estkme.kt | 49 +++++++++++++++++++ .../im/angry/openeuicc/vendored/simlink.kt | 20 ++++++++ app-common/src/main/res/values/strings.xml | 5 ++ .../java/net/typeblog/lpac_jni/EuiccInfo2.kt | 33 ++++++++++--- .../impl/LocalProfileAssistantImpl.kt | 45 +++++++++-------- .../lpac_jni/impl/RootCertificates.kt | 4 +- 8 files changed, 143 insertions(+), 57 deletions(-) create mode 100644 app-common/src/main/java/im/angry/openeuicc/vendored/estkme.kt create mode 100644 app-common/src/main/java/im/angry/openeuicc/vendored/simlink.kt diff --git a/.idea/.gitignore b/.idea/.gitignore index 0d51aca..b7c2402 100644 --- a/.idea/.gitignore +++ b/.idea/.gitignore @@ -9,5 +9,6 @@ /navEditor.xml /runConfigurations.xml /workspace.xml +/AndroidProjectSystem.xml **/*.iml \ No newline at end of file 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 e88ad01..528b232 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 @@ -23,6 +23,8 @@ import im.angry.openeuicc.common.R import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.util.* +import im.angry.openeuicc.vendored.getESTKmeInfo +import im.angry.openeuicc.vendored.getSIMLinkVersion import kotlinx.coroutines.launch import net.typeblog.lpac_jni.impl.PKID_GSMA_LIVE_CI import net.typeblog.lpac_jni.impl.PKID_GSMA_TEST_CI @@ -100,24 +102,22 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { private fun buildEuiccInfoItems(channel: EuiccChannel) = buildList { add(Item(R.string.euicc_info_access_mode, channel.type)) - add( - Item( - R.string.euicc_info_removable, - formatByBoolean(channel.port.card.isRemovable, YES_NO) - ) - ) - add( - Item( - R.string.euicc_info_eid, - channel.lpa.eID, - copiedToastResId = R.string.toast_eid_copied - ) - ) + add(Item(R.string.euicc_info_removable, formatByBoolean(channel.port.card.isRemovable, YES_NO))) + add(Item(R.string.euicc_info_eid, channel.lpa.eID, copiedToastResId = R.string.toast_eid_copied)) + getESTKmeInfo(channel.apduInterface)?.let { + add(Item(R.string.euicc_info_sku, it.skuName)) + add(Item(R.string.euicc_info_sn, it.serialNumber, copiedToastResId = R.string.toast_sn_copied)) + add(Item(R.string.euicc_info_bl_ver, it.bootloaderVersion)) + add(Item(R.string.euicc_info_fw_ver, it.firmwareVersion)) + } + getSIMLinkVersion(channel.lpa.eID, channel.lpa.euiccInfo2?.euiccFirmwareVersion)?.let { + add(Item(R.string.euicc_info_sku, "9eSIM $it")) + } channel.lpa.euiccInfo2.let { info -> - add(Item(R.string.euicc_info_sgp22_version, info?.sgp22Version)) - add(Item(R.string.euicc_info_firmware_version, info?.euiccFirmwareVersion)) - add(Item(R.string.euicc_info_globalplatform_version, info?.globalPlatformVersion)) - add(Item(R.string.euicc_info_pp_version, info?.ppVersion)) + 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_pp_version, info?.ppVersion.toString())) add(Item(R.string.euicc_info_sas_accreditation_number, info?.sasAccreditationNumber)) add(Item(R.string.euicc_info_free_nvram, info?.freeNvram?.let(::formatFreeSpace))) } @@ -134,13 +134,8 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { } add(Item(R.string.euicc_info_ci_type, getString(resId))) } - add( - Item( - R.string.euicc_info_atr, - channel.atr?.encodeHex() ?: getString(R.string.information_unavailable), - copiedToastResId = R.string.toast_atr_copied, - ) - ) + val atr = channel.atr?.encodeHex() ?: getString(R.string.information_unavailable) + add(Item(R.string.euicc_info_atr, atr, copiedToastResId = R.string.toast_atr_copied)) } private fun formatByBoolean(b: Boolean, res: Pair): String = diff --git a/app-common/src/main/java/im/angry/openeuicc/vendored/estkme.kt b/app-common/src/main/java/im/angry/openeuicc/vendored/estkme.kt new file mode 100644 index 0000000..2282921 --- /dev/null +++ b/app-common/src/main/java/im/angry/openeuicc/vendored/estkme.kt @@ -0,0 +1,49 @@ +package im.angry.openeuicc.vendored + +import android.util.Log +import im.angry.openeuicc.core.ApduInterfaceAtrProvider +import im.angry.openeuicc.util.TAG +import im.angry.openeuicc.util.decodeHex +import net.typeblog.lpac_jni.ApduInterface + +data class ESTKmeInfo( + val serialNumber: String?, + val bootloaderVersion: String?, + val firmwareVersion: String?, + val skuName: String?, +) + +fun isESTKmeATR(iface: ApduInterface): Boolean { + if (iface !is ApduInterfaceAtrProvider) return false + val atr = iface.atr ?: return false + val fpr = "estk.me".encodeToByteArray() + for (index in atr.indices) { + if (atr.size - index < fpr.size) break + if (atr.sliceArray(index until index + fpr.size).contentEquals(fpr)) return true + } + return false +} + +fun getESTKmeInfo(iface: ApduInterface): ESTKmeInfo? { + if (!isESTKmeATR(iface)) return null + fun decode(b: ByteArray): String? { + if (b.size < 2) return null + if (b[b.size - 2] != 0x90.toByte() || b[b.size - 1] != 0x00.toByte()) return null + return b.sliceArray(0 until b.size - 2).decodeToString() + } + return try { + iface.withLogicalChannel("A06573746B6D65FFFFFFFFFFFF6D6774".decodeHex()) { transmit -> + fun invoke(p1: Byte) = decode(transmit(byteArrayOf(0x00, 0x00, p1, 0x00, 0x00))) + ESTKmeInfo( + invoke(0x00), // serial number + invoke(0x01), // bootloader version + invoke(0x02), // firmware version + invoke(0x03), // sku name + ) + } + } catch (e: Exception) { + Log.d(TAG, "Failed to get ESTKmeInfo", e) + null + } +} + diff --git a/app-common/src/main/java/im/angry/openeuicc/vendored/simlink.kt b/app-common/src/main/java/im/angry/openeuicc/vendored/simlink.kt new file mode 100644 index 0000000..506f16c --- /dev/null +++ b/app-common/src/main/java/im/angry/openeuicc/vendored/simlink.kt @@ -0,0 +1,20 @@ +package im.angry.openeuicc.vendored + +import net.typeblog.lpac_jni.Version + +private val prefix = Regex("^89044045(84|21)67274948") // SIMLink EID prefix + +fun getSIMLinkVersion(eid: String, version: Version?): String? { + if (version == null || prefix.find(eid, 0) == null) return null + return when { + // @formatter:off + version >= Version(37, 1, 41) -> "v3.1 (beta 1)" + version >= Version(36, 18, 5) -> "v3 (final)" + version >= Version(36, 17, 39) -> "v3 (beta)" + version >= Version(36, 17, 4) -> "v2s" + version >= Version(36, 9, 3) -> "v2.1" + version >= Version(36, 7, 2) -> "v2" + // @formatter:on + else -> null + } +} diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index 71e2418..a45ce1f 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -31,6 +31,7 @@ Cannot switch to new eSIM profile. Confirmation string mismatch ICCID copied to clipboard + Serial Number copied to clipboard EID copied to clipboard ATR copied to clipboard @@ -125,6 +126,10 @@ eUICC Info (%s) Access Mode Removable + Product Name + Product Serial Number + Product Bootloader Version + Product Firmware Version EID SGP.22 Version eUICC OS Version diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/EuiccInfo2.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/EuiccInfo2.kt index 6c73051..0720049 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/EuiccInfo2.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/EuiccInfo2.kt @@ -2,14 +2,31 @@ package net.typeblog.lpac_jni /* Corresponds to EuiccInfo2 in SGP.22 */ data class EuiccInfo2( - val sgp22Version: String, - val profileVersion: String, - val euiccFirmwareVersion: String, - val globalPlatformVersion: String, + val sgp22Version: Version, + val profileVersion: Version, + val euiccFirmwareVersion: Version, + val globalPlatformVersion: Version, val sasAccreditationNumber: String, - val ppVersion: String, + val ppVersion: Version, val freeNvram: Int, val freeRam: Int, - val euiccCiPKIdListForSigning: Array, - val euiccCiPKIdListForVerification: Array, -) \ No newline at end of file + val euiccCiPKIdListForSigning: Set, + val euiccCiPKIdListForVerification: Set, +) + +data class Version( + val major: Int, + val minor: Int, + val patch: Int, +) { + constructor(version: String) : this(version.split('.').map(String::toInt)) + private constructor(parts: List) : this(parts[0], parts[1], parts[2]) + + operator fun compareTo(other: Version): Int { + if (major != other.major) return major - other.major + if (minor != other.minor) return minor - other.minor + return patch - other.patch + } + + override fun toString() = "$major.$minor.$patch" +} diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt index faf5312..3674f4f 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt @@ -10,6 +10,7 @@ import net.typeblog.lpac_jni.LocalProfileAssistant import net.typeblog.lpac_jni.LocalProfileInfo import net.typeblog.lpac_jni.LocalProfileNotification import net.typeblog.lpac_jni.ProfileDownloadCallback +import net.typeblog.lpac_jni.Version class LocalProfileAssistantImpl( isdrAid: ByteArray, @@ -84,8 +85,8 @@ class LocalProfileAssistantImpl( throw IllegalArgumentException("Failed to initialize LPA") } - val pkids = euiccInfo2?.euiccCiPKIdListForVerification ?: arrayOf() - httpInterface.usePublicKeyIds(pkids) + val pkids = euiccInfo2?.euiccCiPKIdListForVerification ?: setOf() + httpInterface.usePublicKeyIds(pkids.toTypedArray()) } override fun setEs10xMss(mss: Byte) { @@ -157,31 +158,29 @@ class LocalProfileAssistantImpl( val cInfo = LpacJni.es10cexGetEuiccInfo2(contextHandle) if (cInfo == 0L) return null - val euiccCiPKIdListForSigning = mutableListOf() - var curr = LpacJni.euiccInfo2GetEuiccCiPKIdListForSigning(cInfo) - while (curr != 0L) { - euiccCiPKIdListForSigning.add(LpacJni.stringDeref(curr)) - curr = LpacJni.stringArrNext(curr) - } - - val euiccCiPKIdListForVerification = mutableListOf() - curr = LpacJni.euiccInfo2GetEuiccCiPKIdListForVerification(cInfo) - while (curr != 0L) { - euiccCiPKIdListForVerification.add(LpacJni.stringDeref(curr)) - curr = LpacJni.stringArrNext(curr) - } - val ret = EuiccInfo2( - LpacJni.euiccInfo2GetSGP22Version(cInfo), - LpacJni.euiccInfo2GetProfileVersion(cInfo), - LpacJni.euiccInfo2GetEuiccFirmwareVersion(cInfo), - LpacJni.euiccInfo2GetGlobalPlatformVersion(cInfo), + Version(LpacJni.euiccInfo2GetSGP22Version(cInfo)), + Version(LpacJni.euiccInfo2GetProfileVersion(cInfo)), + Version(LpacJni.euiccInfo2GetEuiccFirmwareVersion(cInfo)), + Version(LpacJni.euiccInfo2GetGlobalPlatformVersion(cInfo)), LpacJni.euiccInfo2GetSasAcreditationNumber(cInfo), - LpacJni.euiccInfo2GetPpVersion(cInfo), + Version(LpacJni.euiccInfo2GetPpVersion(cInfo)), LpacJni.euiccInfo2GetFreeNonVolatileMemory(cInfo).toInt(), LpacJni.euiccInfo2GetFreeVolatileMemory(cInfo).toInt(), - euiccCiPKIdListForSigning.toTypedArray(), - euiccCiPKIdListForVerification.toTypedArray() + buildSet { + var cursor = LpacJni.euiccInfo2GetEuiccCiPKIdListForSigning(cInfo) + while (cursor != 0L) { + add(LpacJni.stringDeref(cursor)) + cursor = LpacJni.stringArrNext(cursor) + } + }, + buildSet { + var cursor = LpacJni.euiccInfo2GetEuiccCiPKIdListForVerification(cInfo) + while (cursor != 0L) { + add(LpacJni.stringDeref(cursor)) + cursor = LpacJni.stringArrNext(cursor) + } + }, ) LpacJni.euiccInfo2Free(cInfo) diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/RootCertificates.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/RootCertificates.kt index cfd5779..295a911 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/RootCertificates.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/RootCertificates.kt @@ -14,7 +14,7 @@ const val DEFAULT_PKID_GSMA_RSP2_ROOT_CI1 = "81370f5125d0b1d408d4c3b232e6d25e795 // List of GSMA Live CIs // https://www.gsma.com/solutions-and-impact/technologies/esim/gsma-root-ci/ -val PKID_GSMA_LIVE_CI = arrayOf( +val PKID_GSMA_LIVE_CI = setOf( // GSMA RSP2 Root CI1 (SGP.22 v2+v3, CA: DigiCert) // https://euicc-manual.osmocom.org/docs/pki/ci/files/81370f.txt DEFAULT_PKID_GSMA_RSP2_ROOT_CI1, @@ -25,7 +25,7 @@ val PKID_GSMA_LIVE_CI = arrayOf( // SGP.26 v3.0, 2023-12-01 // https://www.gsma.com/solutions-and-impact/technologies/esim/wp-content/uploads/2023/12/SGP.26-v3.0.pdf -val PKID_GSMA_TEST_CI = arrayOf( +val PKID_GSMA_TEST_CI = setOf( // Test CI (SGP.26, NIST P256) // https://euicc-manual.osmocom.org/docs/pki/ci/files/34eecf.txt "34eecf13156518d48d30bdf06853404d115f955d", From ef295c9d128de48cd9e22968dd6f81337bc6029b Mon Sep 17 00:00:00 2001 From: h0353914 Date: Tue, 4 Mar 2025 03:13:39 +0100 Subject: [PATCH 2/3] Add Traditional Chinese (zh-TW) translation (#142) Converted from Simplified Chinese Co-authored-by: h0353914 <45159732+h0353914@users.noreply.github.com> Reviewed-on: https://gitea.angry.im/PeterCxy/OpenEUICC/pulls/142 Co-authored-by: h0353914 Co-committed-by: h0353914 --- .../src/main/res/values-zh-rTW/strings.xml | 147 ++++++++++++++++++ .../src/main/res/values-zh-rTW/strings.xml | 32 ++++ app/src/main/res/values-zh-rTW/strings.xml | 20 +++ 3 files changed, 199 insertions(+) create mode 100644 app-common/src/main/res/values-zh-rTW/strings.xml create mode 100644 app-unpriv/src/main/res/values-zh-rTW/strings.xml create mode 100644 app/src/main/res/values-zh-rTW/strings.xml diff --git a/app-common/src/main/res/values-zh-rTW/strings.xml b/app-common/src/main/res/values-zh-rTW/strings.xml new file mode 100644 index 0000000..28691a0 --- /dev/null +++ b/app-common/src/main/res/values-zh-rTW/strings.xml @@ -0,0 +1,147 @@ + + + 在此裝置上未檢測到此應用程式可訪問的可插拔 eUICC 卡。請插入相容卡或 USB 晶片讀卡機。 + 此 eSIM 上還沒有設定檔 + 未知 + 幫助 + 重新載入卡槽 + 虛擬卡槽 %d + 已啟用 + 已停用 + 電信業者: + 類型: + 啟用 + 停用 + 刪除 + 重新命名 + 等待 eSIM 切換設定檔時逾時。這可能是您手機基頻處理器韌體中的一個錯誤。請嘗試切換飛航模式、重新啟動應用程式或重新啟動手機 + 操作成功, 但是您手機的基頻處理器沒有重新整理。您可能需要切換飛航模式或重新啟動,以便使用新的設定檔。 + 無法切換到新的 eSIM 設定檔。 + 輸入的確認文字不匹配 + 已複製 ICCID 到剪貼簿 + 已複製 EID 到剪貼簿 + 已複製 ATR 到剪貼簿 + 授予 USB 權限 + 需要獲得訪問 USB 晶片讀卡機的權限。 + 無法透過 USB 晶片讀卡機連線到 eSIM。 + 長時間運行的背景作業 + 正在下載 eSIM 設定檔 + 無法下載 eSIM 設定檔 + 正在重新命名 eSIM 設定檔 + 無法重新命名 eSIM 設定檔 + 正在刪除 eSIM 設定檔 + 無法刪除 eSIM 設定檔 + 正在切換 eSIM 設定檔 + 無法切換 eSIM 設定檔 + 新增新 eSIM + 伺服器 (RSP / SM-DP+) + 啟用碼 + 確認碼 (可選) + IMEI (可選) + 本次下載可能會失敗 + 目前晶片的剩餘空間不足,可能導致配置下載失敗。\n是否繼續下載? + 日誌已儲存到指定路徑。需要透過其他 App 分享嗎? + 新名稱 + 無法將名稱編碼為 UTF-8 + 名稱長於 64 字元 + 重新命名設定檔時發生了未知錯誤 + 您確定要刪除 %s 嗎?此動作無法還原。 + 請輸入\'%s\'以確認刪除 + 通知列表 + 通知列表 (%s) + 管理通知 + eSIM 設定檔可以在下載、刪除、啟用或停用時向電信業者傳送通知。此處列出了要傳送的這些通知的佇列。\n\n在\"設定\"中,您可以指定是否自動傳送每種型別的通知。請注意,即使通知已傳送,也不會自動從記錄中刪除,除非佇列空間不足。\n\n在這裡,您可以手動傳送或刪除每個待處理的通知。 + 已下載 + 已刪除 + 已啟用 + 已停用 + 處理 + 刪除 + 儲存日誌 + %s 的日誌 + 設定 + 通知 + 變更 eSIM 設定檔會向電信業者傳送通知。根據需要在此處微調此行為。 + 下載 + 傳送 下載 設定檔的通知 + 刪除 + 傳送 刪除 設定檔的通知 + 切換 + 記錄詳細日誌 + 詳細日誌中包含敏感資訊,開啟此功能後請僅與你信任的人共享你的日誌。 + 日誌 + 檢視應用程式的最新除錯日誌 + 傳送 切換 設定檔的通知\n注意,這種型別的通知是不可靠的。 + 進階 + 允許 停用/刪除 已啟用的設定檔 + 預設情況下,此應用程式會阻止您停用可插拔 eSIM 中已啟用的設定檔。\n因為這樣做 有時 會導致無法存取。\n勾選此框以 移除 此保護措施。 + 資訊 + App 版本 + 原始碼 + 測試 + 準備中 + 可用 + 未在剪貼簿上發現 LPA 碼 + LPA 碼解析錯誤 + 無法將二維碼或剪貼簿內容解析為 LPA 碼 + 下載精靈 + 返回 + 下一步 + 您選擇的 SIM 卡已被移除 + 請選擇或確認下載目標 eSIM 卡槽: + 型別: + 可插拔 + 內建 + 內建, 埠 %d + 目前設定檔: + 剩餘空間: + 您想要如何下載 eSIM 設定檔? + 用相機掃描二維碼 + 從相簿選擇二維碼 + 從剪貼簿讀取 + 手動輸入 + 請輸入或確認下載 eSIM 的詳細資訊: + 正在下載您的 eSIM... + 準備中 + 正在連線到伺服器 + 正在向伺服器驗證您的裝置 + 正在下載 eSIM 設定檔 + 正在寫入 eSIM 設定檔 + 錯誤診斷 + 錯誤代碼: %s + 上次 HTTP 狀態碼 (來自伺服器): %d + 上次 HTTP 應答 (來自伺服器): + 上次 HTTP 錯誤: + 上次 APDU 應答 (來自 SIM): %s + 上次 APDU 應答 (來自 SIM) 是成功的 + 上次 APDU 應答 (來自 SIM) 是失敗的 + 上次 APDU 錯誤: + 儲存 + %s 的錯誤診斷 + eUICC 詳情 + eUICC 詳情 (%s) + 訪問方式 + 可插拔 + SGP.22 版本 + eUICC OS 版本 + GlobalPlatform 版本 + SAS 認證號碼 + Protected Profile 版本 + NVRAM 剩餘空間 (eSIM 儲存容量) + 證書簽發者 (CI) + GSMA 生產環境 CI + GSMA 測試 CI + 未知 eSIM CI + + + 還有 %d 步成為開發者 + 您現在是開發者了! + 語言 + 選擇 App 語言 + 開發人員選項 + 顯示未經過濾的設定檔列表 + 在設定檔列表中包括非生產環境的設定檔 + 忽略 SM-DP+ 的 TLS 證書 + 允許 RSP 伺服器使用任意證書 + 無資訊 + \ No newline at end of file diff --git a/app-unpriv/src/main/res/values-zh-rTW/strings.xml b/app-unpriv/src/main/res/values-zh-rTW/strings.xml new file mode 100644 index 0000000..b8d0eb8 --- /dev/null +++ b/app-unpriv/src/main/res/values-zh-rTW/strings.xml @@ -0,0 +1,32 @@ + + 相容性檢查 + 啟動 SIM 卡應用程式 + 系統功能 + 您的裝置是否具有管理可插拔 eUICC 卡所需的所有功能。例如,基本的電話功能和 OMAPI 支援。 + 您的裝置沒有電話功能。 + 您的裝置/系統未宣告支援 OMAPI。這可能是由於缺少硬體支援,或者可能僅僅是由於缺少標誌。請參閱以下兩項檢查以確定 OMAPI 是否確實受支援。 + OMAPI 連線 + 您的裝置是否允許透過 OMAPI 存取 SIM 卡上的安全元件? + 無法透過 OMAPI 偵測到 SIM 卡的 Secure Element。如果您尚未在此裝置中插入 SIM 卡,請嘗試插入一張 SIM 卡並重試此檢查。 + 已成功檢測到可存取 Secure Element 的卡槽,但僅限於以下 SIM 卡槽:SIM%s + ISD-R 通道存取 + 您的裝置是否支援透過 OMAPI 開啟 eSIM 的 ISD-R (管理) 通道? + 無法確定是否支援透過 OMAPI 進行 ISD-R 的存取。如果尚未插入,您可能需要插入 SIM 卡 (任何 SIM 卡都可以) 重試。 + OMAPI 只能在以下 SIM 插槽上存取 ISD-R:SIM%s + 不在已知錯誤清單中 + 確保您的裝置不存在與可插拔 eSIM 相關的錯誤。 + 很抱歉,您的裝置在存取可插拔 eSIM 時存在錯誤。這並不表示完全無法使用,但我們不保證該應用在您裝置上的行為。 + USB 晶片讀卡機支援 + 您的裝置是否支援透過 USB 晶片讀卡機管理 eSIM? + 您可以透過此裝置上的標準 USB CCID 讀卡機管理 eSIM (即使您在這裡有任何其他檢查項失敗)。請插入讀卡機,然後開啟此應用程式以這種方式管理 eSIM。 + 您的裝置不支援 USB 晶片讀卡機。 + 結論 (USB 晶片讀卡機以外) + 根據之前的所有檢查,您的裝置與可插拔 eSIM 卡相容的可能性有多大? + 您可以使用和管理插入此裝置的可插拔 eSIM 卡。 + 已知您的裝置在存取可插拔 eSIM 卡時存在問題。\n%s + 我們無法確定是否可以在您的裝置上管理可插拔 eSIM 卡。不過,您的裝置確實宣告支援 OMAPI,因此它工作的可能性略高。\n%s + 我們無法確定是否可以在您的裝置上管理可插拔 eSIM 卡。由於您的裝置未宣告支援OMAPI,因此更有可能不支援在此裝置上管理可插拔 eSIM。\n%s + 我們無法確定是否可以在您的裝置上管理可插拔 eSIM 卡。\n%s + 然而,已經載入了eSIM設定檔的可插拔 eSIM 卡仍然可以工作; 即使無法在裝置上直接管理可插拔 eSIM 卡中的設定檔,您仍然可以使用 USB 卡讀卡機來管理設定檔。 + ARA-M SHA-1 已複製到剪貼簿 + \ 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 new file mode 100644 index 0000000..368efbc --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -0,0 +1,20 @@ + + + 在此裝置上找不到 eUICC 晶片。\n在某些裝置上,您可能需要先在此應用的選單中啟用雙卡支援。 + 雙卡 + 雙卡支援狀態已切換。請等待基頻處理器重新啟動。 + 此卡槽支援多個啟用設定檔 (MEP)。要啟用或停用此功能,請使用\"卡槽對映\"工具。 + 卡槽對映 + 虛擬卡槽 %d: + 卡槽 %1$d 端口 %2$d + 您的手機有 %1$d 個虛擬 SIM 卡槽和 %2$d 個實體 SIM 卡槽。%3$s\n\n選擇您希望每個虛擬卡槽對應的實體卡槽 和/或 \"端口\"。請注意,並非所有對映模式都受硬體支援。 + \n\n實體卡槽 %1$d 支援多個啟用的設定檔 (MEP)。要使用此功能,請將其 %2$d 個虛擬\"端口\"分配給上面顯示的不同虛擬卡槽。\n\n啟用 MEP 後,\"端口\"會在 OpenEUICC 中顯示為共享 eSIM 設定檔的獨立的 eSIM 卡槽。 + \n支援雙卡模式,但已停用。如果您的裝置帶有內建 eSIM 晶片,則預設情況下可能不會啟用。更改上面的對映或啟用雙卡以訪問您的 eSIM。 + 您的新卡槽對映已設定完畢。請等待基頻處理器重新整理卡槽。 + 指定的對映可能無效或硬體不支援您指定的對映。 + 透過下載 eSIM 連線到行動網路 + 您的裝置支援 eSIM。要連線到行動網路,請下載電信業者釋出的 eSIM,或插入實體 SIM 卡。 + 跳過 + 下載 eSIM + TelephonyManager (特權) + \ No newline at end of file From d5aefcaec740c7fd646912612eefbb49cee2ad12 Mon Sep 17 00:00:00 2001 From: septs Date: Tue, 4 Mar 2025 03:14:01 +0100 Subject: [PATCH 3/3] refactor: usb ccid driver (#149) Reviewed-on: https://gitea.angry.im/PeterCxy/OpenEUICC/pulls/149 Co-authored-by: septs Co-committed-by: septs --- .../core/DefaultEuiccChannelFactory.kt | 5 +- .../core/DefaultEuiccChannelManager.kt | 5 +- .../openeuicc/core/usb/UsbCcidDescription.kt | 39 ++++++--------- .../openeuicc/core/usb/UsbCcidTransceiver.kt | 49 +++++++++---------- .../angry/openeuicc/core/usb/UsbCcidUtils.kt | 41 ++++++---------- 5 files changed, 61 insertions(+), 78 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 5e87564..ea0bd60 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 @@ -8,7 +8,8 @@ import android.se.omapi.SEService import android.util.Log import im.angry.openeuicc.common.R import im.angry.openeuicc.core.usb.UsbApduInterface -import im.angry.openeuicc.core.usb.getIoEndpoints +import im.angry.openeuicc.core.usb.bulkPair +import im.angry.openeuicc.core.usb.endpoints import im.angry.openeuicc.util.* import java.lang.IllegalArgumentException @@ -61,7 +62,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha } override fun tryOpenUsbEuiccChannel(usbDevice: UsbDevice, usbInterface: UsbInterface): EuiccChannel? { - val (bulkIn, bulkOut) = usbInterface.getIoEndpoints() + val (bulkIn, bulkOut) = usbInterface.endpoints.bulkPair if (bulkIn == null || bulkOut == null) return null val conn = usbManager.openDevice(usbDevice) ?: return null if (!conn.claimInterface(usbInterface, true)) return null diff --git a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt index 293042c..dd57eab 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt @@ -5,7 +5,8 @@ import android.hardware.usb.UsbDevice import android.hardware.usb.UsbManager import android.telephony.SubscriptionManager import android.util.Log -import im.angry.openeuicc.core.usb.getSmartCardInterface +import im.angry.openeuicc.core.usb.smartCard +import im.angry.openeuicc.core.usb.interfaces import im.angry.openeuicc.di.AppContainer import im.angry.openeuicc.util.* import kotlinx.coroutines.Dispatchers @@ -244,7 +245,7 @@ open class DefaultEuiccChannelManager( withContext(Dispatchers.IO) { usbManager.deviceList.values.forEach { device -> Log.i(TAG, "Scanning USB device ${device.deviceId}:${device.vendorId}") - val iface = device.getSmartCardInterface() ?: return@forEach + val iface = device.interfaces.smartCard ?: return@forEach // If we don't have permission, tell UI code that we found a candidate device, but we // need permission to be able to do anything with it if (!usbManager.hasPermission(device)) return@withContext Pair(device, false) diff --git a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidDescription.kt b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidDescription.kt index 5123d53..bc32fb6 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidDescription.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidDescription.kt @@ -20,12 +20,12 @@ data class UsbCcidDescription( private const val FEATURE_EXCHANGE_LEVEL_TPDU = 0x10000 private const val FEATURE_EXCHANGE_LEVEL_SHORT_APDU = 0x20000 - private const val FEATURE_EXCHAGE_LEVEL_EXTENDED_APDU = 0x40000 + private const val FEATURE_EXCHANGE_LEVEL_EXTENDED_APDU = 0x40000 // bVoltageSupport Masks - private const val VOLTAGE_5V: Byte = 1 - private const val VOLTAGE_3V: Byte = 2 - private const val VOLTAGE_1_8V: Byte = 4 + private const val VOLTAGE_5V0: Byte = 1 + private const val VOLTAGE_3V0: Byte = 2 + private const val VOLTAGE_1V8: Byte = 4 private const val SLOT_OFFSET = 4 private const val FEATURES_OFFSET = 40 @@ -71,31 +71,24 @@ data class UsbCcidDescription( } enum class Voltage(powerOnValue: Int, mask: Int) { - AUTO(0, 0), _5V(1, VOLTAGE_5V.toInt()), _3V(2, VOLTAGE_3V.toInt()), _1_8V( - 3, - VOLTAGE_1_8V.toInt() - ); + // @formatter:off + AUTO(0, 0), + V50(1, VOLTAGE_5V0.toInt()), + V30(2, VOLTAGE_3V0.toInt()), + V18(3, VOLTAGE_1V8.toInt()); + // @formatter:on val mask = powerOnValue.toByte() val powerOnValue = mask.toByte() } - private fun hasFeature(feature: Int): Boolean = - (dwFeatures and feature) != 0 + private fun hasFeature(feature: Int) = (dwFeatures and feature) != 0 - val voltages: Array - get() = - if (hasFeature(FEATURE_AUTOMATIC_VOLTAGE)) { - arrayOf(Voltage.AUTO) - } else { - Voltage.values().mapNotNull { - if ((it.mask.toInt() and bVoltageSupport.toInt()) != 0) { - it - } else { - null - } - }.toTypedArray() - } + val voltages: List + get() { + if (hasFeature(FEATURE_AUTOMATIC_VOLTAGE)) return listOf(Voltage.AUTO) + return Voltage.entries.filter { (it.mask.toInt() and bVoltageSupport.toInt()) != 0 } + } val hasAutomaticPps: Boolean get() = hasFeature(FEATURE_AUTOMATIC_PPS) diff --git a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidTransceiver.kt b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidTransceiver.kt index 5ef35af..9155721 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidTransceiver.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidTransceiver.kt @@ -95,6 +95,7 @@ class UsbCcidTransceiver( data class UsbCcidErrorException(val msg: String, val errorResponse: CcidDataBlock) : Exception(msg) + @Suppress("ArrayInDataClass") data class CcidDataBlock( val dwLength: Int, val bSlot: Byte, @@ -183,31 +184,26 @@ class UsbCcidTransceiver( usbBulkIn, inputBuffer, inputBuffer.size, DEVICE_COMMUNICATE_TIMEOUT_MILLIS ) if (runBlocking { verboseLoggingFlow.first() }) { - Log.d(TAG, "Received " + readBytes + " bytes: " + inputBuffer.encodeHex()) + Log.d(TAG, "Received $readBytes bytes: ${inputBuffer.encodeHex()}") } } while (readBytes <= 0 && attempts-- > 0) if (readBytes < CCID_HEADER_LENGTH) { throw UsbTransportException("USB-CCID error - failed to receive CCID header") } if (inputBuffer[0] != MESSAGE_TYPE_RDR_TO_PC_DATA_BLOCK.toByte()) { - if (expectedSequenceNumber != inputBuffer[6]) { - throw UsbTransportException( - ((("USB-CCID error - bad CCID header, type " + inputBuffer[0]) + " (expected " + - MESSAGE_TYPE_RDR_TO_PC_DATA_BLOCK) + "), sequence number " + inputBuffer[6] - ) + " (expected " + - expectedSequenceNumber + ")" - ) - } - throw UsbTransportException( - "USB-CCID error - bad CCID header type " + inputBuffer[0] - ) + throw UsbTransportException(buildString { + append("USB-CCID error - bad CCID header") + append(", type ") + append("%d (expected %d)".format(inputBuffer[0], MESSAGE_TYPE_RDR_TO_PC_DATA_BLOCK)) + if (expectedSequenceNumber != inputBuffer[6]) { + append(", sequence number ") + append("%d (expected %d)".format(inputBuffer[6], expectedSequenceNumber)) + } + }) } var result = CcidDataBlock.parseHeaderFromBytes(inputBuffer) if (expectedSequenceNumber != result.bSeq) { - throw UsbTransportException( - ("USB-CCID error - expected sequence number " + - expectedSequenceNumber + ", got " + result) - ) + throw UsbTransportException("USB-CCID error - expected sequence number $expectedSequenceNumber, got $result") } val dataBuffer = ByteArray(result.dwLength) @@ -218,9 +214,7 @@ class UsbCcidTransceiver( usbBulkIn, inputBuffer, inputBuffer.size, DEVICE_COMMUNICATE_TIMEOUT_MILLIS ) if (readBytes < 0) { - throw UsbTransportException( - "USB error - failed reading response data! Header: $result" - ) + throw UsbTransportException("USB error - failed reading response data! Header: $result") } System.arraycopy(inputBuffer, 0, dataBuffer, bufferedBytes, readBytes) bufferedBytes += readBytes @@ -285,7 +279,7 @@ class UsbCcidTransceiver( } val ccidDataBlock = receiveDataBlock(sequenceNumber) val elapsedTime = SystemClock.elapsedRealtime() - startTime - Log.d(TAG, "USB XferBlock call took " + elapsedTime + "ms") + Log.d(TAG, "USB XferBlock call took ${elapsedTime}ms") return ccidDataBlock } @@ -293,13 +287,13 @@ class UsbCcidTransceiver( val startTime = SystemClock.elapsedRealtime() skipAvailableInput() var response: CcidDataBlock? = null - for (v in usbCcidDescription.voltages) { - Log.v(TAG, "CCID: attempting to power on with voltage $v") + for (voltage in usbCcidDescription.voltages) { + Log.v(TAG, "CCID: attempting to power on with voltage $voltage") response = try { - iccPowerOnVoltage(v.powerOnValue) + iccPowerOnVoltage(voltage.powerOnValue) } catch (e: UsbCcidErrorException) { if (e.errorResponse.bError.toInt() == 7) { // Power select error - Log.v(TAG, "CCID: failed to power on with voltage $v") + Log.v(TAG, "CCID: failed to power on with voltage $voltage") iccPowerOff() Log.v(TAG, "CCID: powered off") continue @@ -314,8 +308,11 @@ class UsbCcidTransceiver( val elapsedTime = SystemClock.elapsedRealtime() - startTime Log.d( TAG, - "Usb transport connected, took " + elapsedTime + "ms, ATR=" + - response.data?.encodeHex() + buildString { + append("Usb transport connected") + append(", took ", elapsedTime, "ms") + append(", ATR=", response.data?.encodeHex()) + } ) return response } diff --git a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidUtils.kt b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidUtils.kt index edca7a0..877c7fd 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidUtils.kt @@ -6,31 +6,22 @@ import android.hardware.usb.UsbDevice import android.hardware.usb.UsbEndpoint import android.hardware.usb.UsbInterface -class UsbTransportException(msg: String) : Exception(msg) +class UsbTransportException(message: String) : Exception(message) -fun UsbInterface.getIoEndpoints(): Pair { - var bulkIn: UsbEndpoint? = null - var bulkOut: UsbEndpoint? = null - for (i in 0 until endpointCount) { - val endpoint = getEndpoint(i) - if (endpoint.type != UsbConstants.USB_ENDPOINT_XFER_BULK) { - continue - } - if (endpoint.direction == UsbConstants.USB_DIR_IN) { - bulkIn = endpoint - } else if (endpoint.direction == UsbConstants.USB_DIR_OUT) { - bulkOut = endpoint - } - } - return Pair(bulkIn, bulkOut) -} +val UsbDevice.interfaces: Iterable + get() = (0 until interfaceCount).map(::getInterface) -fun UsbDevice.getSmartCardInterface(): UsbInterface? { - for (i in 0 until interfaceCount) { - val anInterface = getInterface(i) - if (anInterface.interfaceClass == UsbConstants.USB_CLASS_CSCID) { - return anInterface - } +val Iterable.smartCard: UsbInterface? + get() = find { it.interfaceClass == UsbConstants.USB_CLASS_CSCID } + +val UsbInterface.endpoints: Iterable + get() = (0 until endpointCount).map(::getEndpoint) + +val Iterable.bulkPair: Pair + get() { + val endpoints = filter { it.type == UsbConstants.USB_ENDPOINT_XFER_BULK } + return Pair( + endpoints.find { it.direction == UsbConstants.USB_DIR_IN }, + endpoints.find { it.direction == UsbConstants.USB_DIR_OUT }, + ) } - return null -} \ No newline at end of file