Compare commits

..

No commits in common. "d5aefcaec740c7fd646912612eefbb49cee2ad12" and "c8ecdee09546191550db0b1f292ed26e9894b39b" have entirely different histories.

16 changed files with 136 additions and 404 deletions

1
.idea/.gitignore generated vendored
View file

@ -9,6 +9,5 @@
/navEditor.xml /navEditor.xml
/runConfigurations.xml /runConfigurations.xml
/workspace.xml /workspace.xml
/AndroidProjectSystem.xml
**/*.iml **/*.iml

View file

@ -8,8 +8,7 @@ import android.se.omapi.SEService
import android.util.Log import android.util.Log
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
import im.angry.openeuicc.core.usb.UsbApduInterface import im.angry.openeuicc.core.usb.UsbApduInterface
import im.angry.openeuicc.core.usb.bulkPair import im.angry.openeuicc.core.usb.getIoEndpoints
import im.angry.openeuicc.core.usb.endpoints
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import java.lang.IllegalArgumentException import java.lang.IllegalArgumentException
@ -62,7 +61,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
} }
override fun tryOpenUsbEuiccChannel(usbDevice: UsbDevice, usbInterface: UsbInterface): EuiccChannel? { override fun tryOpenUsbEuiccChannel(usbDevice: UsbDevice, usbInterface: UsbInterface): EuiccChannel? {
val (bulkIn, bulkOut) = usbInterface.endpoints.bulkPair val (bulkIn, bulkOut) = usbInterface.getIoEndpoints()
if (bulkIn == null || bulkOut == null) return null if (bulkIn == null || bulkOut == null) return null
val conn = usbManager.openDevice(usbDevice) ?: return null val conn = usbManager.openDevice(usbDevice) ?: return null
if (!conn.claimInterface(usbInterface, true)) return null if (!conn.claimInterface(usbInterface, true)) return null

View file

@ -5,8 +5,7 @@ import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager import android.hardware.usb.UsbManager
import android.telephony.SubscriptionManager import android.telephony.SubscriptionManager
import android.util.Log import android.util.Log
import im.angry.openeuicc.core.usb.smartCard import im.angry.openeuicc.core.usb.getSmartCardInterface
import im.angry.openeuicc.core.usb.interfaces
import im.angry.openeuicc.di.AppContainer import im.angry.openeuicc.di.AppContainer
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -245,7 +244,7 @@ open class DefaultEuiccChannelManager(
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
usbManager.deviceList.values.forEach { device -> usbManager.deviceList.values.forEach { device ->
Log.i(TAG, "Scanning USB device ${device.deviceId}:${device.vendorId}") Log.i(TAG, "Scanning USB device ${device.deviceId}:${device.vendorId}")
val iface = device.interfaces.smartCard ?: return@forEach val iface = device.getSmartCardInterface() ?: return@forEach
// If we don't have permission, tell UI code that we found a candidate device, but we // 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 // need permission to be able to do anything with it
if (!usbManager.hasPermission(device)) return@withContext Pair(device, false) if (!usbManager.hasPermission(device)) return@withContext Pair(device, false)

View file

@ -20,12 +20,12 @@ data class UsbCcidDescription(
private const val FEATURE_EXCHANGE_LEVEL_TPDU = 0x10000 private const val FEATURE_EXCHANGE_LEVEL_TPDU = 0x10000
private const val FEATURE_EXCHANGE_LEVEL_SHORT_APDU = 0x20000 private const val FEATURE_EXCHANGE_LEVEL_SHORT_APDU = 0x20000
private const val FEATURE_EXCHANGE_LEVEL_EXTENDED_APDU = 0x40000 private const val FEATURE_EXCHAGE_LEVEL_EXTENDED_APDU = 0x40000
// bVoltageSupport Masks // bVoltageSupport Masks
private const val VOLTAGE_5V0: Byte = 1 private const val VOLTAGE_5V: Byte = 1
private const val VOLTAGE_3V0: Byte = 2 private const val VOLTAGE_3V: Byte = 2
private const val VOLTAGE_1V8: Byte = 4 private const val VOLTAGE_1_8V: Byte = 4
private const val SLOT_OFFSET = 4 private const val SLOT_OFFSET = 4
private const val FEATURES_OFFSET = 40 private const val FEATURES_OFFSET = 40
@ -71,24 +71,31 @@ data class UsbCcidDescription(
} }
enum class Voltage(powerOnValue: Int, mask: Int) { enum class Voltage(powerOnValue: Int, mask: Int) {
// @formatter:off AUTO(0, 0), _5V(1, VOLTAGE_5V.toInt()), _3V(2, VOLTAGE_3V.toInt()), _1_8V(
AUTO(0, 0), 3,
V50(1, VOLTAGE_5V0.toInt()), VOLTAGE_1_8V.toInt()
V30(2, VOLTAGE_3V0.toInt()), );
V18(3, VOLTAGE_1V8.toInt());
// @formatter:on
val mask = powerOnValue.toByte() val mask = powerOnValue.toByte()
val powerOnValue = mask.toByte() val powerOnValue = mask.toByte()
} }
private fun hasFeature(feature: Int) = (dwFeatures and feature) != 0 private fun hasFeature(feature: Int): Boolean =
(dwFeatures and feature) != 0
val voltages: List<Voltage> val voltages: Array<Voltage>
get() { get() =
if (hasFeature(FEATURE_AUTOMATIC_VOLTAGE)) return listOf(Voltage.AUTO) if (hasFeature(FEATURE_AUTOMATIC_VOLTAGE)) {
return Voltage.entries.filter { (it.mask.toInt() and bVoltageSupport.toInt()) != 0 } arrayOf(Voltage.AUTO)
} } else {
Voltage.values().mapNotNull {
if ((it.mask.toInt() and bVoltageSupport.toInt()) != 0) {
it
} else {
null
}
}.toTypedArray()
}
val hasAutomaticPps: Boolean val hasAutomaticPps: Boolean
get() = hasFeature(FEATURE_AUTOMATIC_PPS) get() = hasFeature(FEATURE_AUTOMATIC_PPS)

View file

@ -95,7 +95,6 @@ class UsbCcidTransceiver(
data class UsbCcidErrorException(val msg: String, val errorResponse: CcidDataBlock) : data class UsbCcidErrorException(val msg: String, val errorResponse: CcidDataBlock) :
Exception(msg) Exception(msg)
@Suppress("ArrayInDataClass")
data class CcidDataBlock( data class CcidDataBlock(
val dwLength: Int, val dwLength: Int,
val bSlot: Byte, val bSlot: Byte,
@ -184,26 +183,31 @@ class UsbCcidTransceiver(
usbBulkIn, inputBuffer, inputBuffer.size, DEVICE_COMMUNICATE_TIMEOUT_MILLIS usbBulkIn, inputBuffer, inputBuffer.size, DEVICE_COMMUNICATE_TIMEOUT_MILLIS
) )
if (runBlocking { verboseLoggingFlow.first() }) { 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) } while (readBytes <= 0 && attempts-- > 0)
if (readBytes < CCID_HEADER_LENGTH) { if (readBytes < CCID_HEADER_LENGTH) {
throw UsbTransportException("USB-CCID error - failed to receive CCID header") throw UsbTransportException("USB-CCID error - failed to receive CCID header")
} }
if (inputBuffer[0] != MESSAGE_TYPE_RDR_TO_PC_DATA_BLOCK.toByte()) { if (inputBuffer[0] != MESSAGE_TYPE_RDR_TO_PC_DATA_BLOCK.toByte()) {
throw UsbTransportException(buildString { if (expectedSequenceNumber != inputBuffer[6]) {
append("USB-CCID error - bad CCID header") throw UsbTransportException(
append(", type ") ((("USB-CCID error - bad CCID header, type " + inputBuffer[0]) + " (expected " +
append("%d (expected %d)".format(inputBuffer[0], MESSAGE_TYPE_RDR_TO_PC_DATA_BLOCK)) MESSAGE_TYPE_RDR_TO_PC_DATA_BLOCK) + "), sequence number " + inputBuffer[6]
if (expectedSequenceNumber != inputBuffer[6]) { ) + " (expected " +
append(", sequence number ") expectedSequenceNumber + ")"
append("%d (expected %d)".format(inputBuffer[6], expectedSequenceNumber)) )
} }
}) throw UsbTransportException(
"USB-CCID error - bad CCID header type " + inputBuffer[0]
)
} }
var result = CcidDataBlock.parseHeaderFromBytes(inputBuffer) var result = CcidDataBlock.parseHeaderFromBytes(inputBuffer)
if (expectedSequenceNumber != result.bSeq) { 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) val dataBuffer = ByteArray(result.dwLength)
@ -214,7 +218,9 @@ class UsbCcidTransceiver(
usbBulkIn, inputBuffer, inputBuffer.size, DEVICE_COMMUNICATE_TIMEOUT_MILLIS usbBulkIn, inputBuffer, inputBuffer.size, DEVICE_COMMUNICATE_TIMEOUT_MILLIS
) )
if (readBytes < 0) { 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) System.arraycopy(inputBuffer, 0, dataBuffer, bufferedBytes, readBytes)
bufferedBytes += readBytes bufferedBytes += readBytes
@ -279,7 +285,7 @@ class UsbCcidTransceiver(
} }
val ccidDataBlock = receiveDataBlock(sequenceNumber) val ccidDataBlock = receiveDataBlock(sequenceNumber)
val elapsedTime = SystemClock.elapsedRealtime() - startTime 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 return ccidDataBlock
} }
@ -287,13 +293,13 @@ class UsbCcidTransceiver(
val startTime = SystemClock.elapsedRealtime() val startTime = SystemClock.elapsedRealtime()
skipAvailableInput() skipAvailableInput()
var response: CcidDataBlock? = null var response: CcidDataBlock? = null
for (voltage in usbCcidDescription.voltages) { for (v in usbCcidDescription.voltages) {
Log.v(TAG, "CCID: attempting to power on with voltage $voltage") Log.v(TAG, "CCID: attempting to power on with voltage $v")
response = try { response = try {
iccPowerOnVoltage(voltage.powerOnValue) iccPowerOnVoltage(v.powerOnValue)
} catch (e: UsbCcidErrorException) { } catch (e: UsbCcidErrorException) {
if (e.errorResponse.bError.toInt() == 7) { // Power select error if (e.errorResponse.bError.toInt() == 7) { // Power select error
Log.v(TAG, "CCID: failed to power on with voltage $voltage") Log.v(TAG, "CCID: failed to power on with voltage $v")
iccPowerOff() iccPowerOff()
Log.v(TAG, "CCID: powered off") Log.v(TAG, "CCID: powered off")
continue continue
@ -308,11 +314,8 @@ class UsbCcidTransceiver(
val elapsedTime = SystemClock.elapsedRealtime() - startTime val elapsedTime = SystemClock.elapsedRealtime() - startTime
Log.d( Log.d(
TAG, TAG,
buildString { "Usb transport connected, took " + elapsedTime + "ms, ATR=" +
append("Usb transport connected") response.data?.encodeHex()
append(", took ", elapsedTime, "ms")
append(", ATR=", response.data?.encodeHex())
}
) )
return response return response
} }

View file

@ -6,22 +6,31 @@ import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbEndpoint import android.hardware.usb.UsbEndpoint
import android.hardware.usb.UsbInterface import android.hardware.usb.UsbInterface
class UsbTransportException(message: String) : Exception(message) class UsbTransportException(msg: String) : Exception(msg)
val UsbDevice.interfaces: Iterable<UsbInterface> fun UsbInterface.getIoEndpoints(): Pair<UsbEndpoint?, UsbEndpoint?> {
get() = (0 until interfaceCount).map(::getInterface) var bulkIn: UsbEndpoint? = null
var bulkOut: UsbEndpoint? = null
val Iterable<UsbInterface>.smartCard: UsbInterface? for (i in 0 until endpointCount) {
get() = find { it.interfaceClass == UsbConstants.USB_CLASS_CSCID } val endpoint = getEndpoint(i)
if (endpoint.type != UsbConstants.USB_ENDPOINT_XFER_BULK) {
val UsbInterface.endpoints: Iterable<UsbEndpoint> continue
get() = (0 until endpointCount).map(::getEndpoint) }
if (endpoint.direction == UsbConstants.USB_DIR_IN) {
val Iterable<UsbEndpoint>.bulkPair: Pair<UsbEndpoint?, UsbEndpoint?> bulkIn = endpoint
get() { } else if (endpoint.direction == UsbConstants.USB_DIR_OUT) {
val endpoints = filter { it.type == UsbConstants.USB_ENDPOINT_XFER_BULK } bulkOut = endpoint
return Pair( }
endpoints.find { it.direction == UsbConstants.USB_DIR_IN },
endpoints.find { it.direction == UsbConstants.USB_DIR_OUT },
)
} }
return Pair(bulkIn, bulkOut)
}
fun UsbDevice.getSmartCardInterface(): UsbInterface? {
for (i in 0 until interfaceCount) {
val anInterface = getInterface(i)
if (anInterface.interfaceClass == UsbConstants.USB_CLASS_CSCID) {
return anInterface
}
}
return null
}

View file

@ -23,8 +23,6 @@ import im.angry.openeuicc.common.R
import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import im.angry.openeuicc.vendored.getESTKmeInfo
import im.angry.openeuicc.vendored.getSIMLinkVersion
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.typeblog.lpac_jni.impl.PKID_GSMA_LIVE_CI import net.typeblog.lpac_jni.impl.PKID_GSMA_LIVE_CI
import net.typeblog.lpac_jni.impl.PKID_GSMA_TEST_CI import net.typeblog.lpac_jni.impl.PKID_GSMA_TEST_CI
@ -102,22 +100,24 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
private fun buildEuiccInfoItems(channel: EuiccChannel) = buildList { private fun buildEuiccInfoItems(channel: EuiccChannel) = buildList {
add(Item(R.string.euicc_info_access_mode, channel.type)) 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(
add(Item(R.string.euicc_info_eid, channel.lpa.eID, copiedToastResId = R.string.toast_eid_copied)) Item(
getESTKmeInfo(channel.apduInterface)?.let { R.string.euicc_info_removable,
add(Item(R.string.euicc_info_sku, it.skuName)) formatByBoolean(channel.port.card.isRemovable, YES_NO)
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)) add(
} Item(
getSIMLinkVersion(channel.lpa.eID, channel.lpa.euiccInfo2?.euiccFirmwareVersion)?.let { R.string.euicc_info_eid,
add(Item(R.string.euicc_info_sku, "9eSIM $it")) channel.lpa.eID,
} copiedToastResId = R.string.toast_eid_copied
)
)
channel.lpa.euiccInfo2.let { info -> channel.lpa.euiccInfo2.let { info ->
add(Item(R.string.euicc_info_sgp22_version, info?.sgp22Version.toString())) add(Item(R.string.euicc_info_sgp22_version, info?.sgp22Version))
add(Item(R.string.euicc_info_firmware_version, info?.euiccFirmwareVersion.toString())) add(Item(R.string.euicc_info_firmware_version, info?.euiccFirmwareVersion))
add(Item(R.string.euicc_info_globalplatform_version, info?.globalPlatformVersion.toString())) add(Item(R.string.euicc_info_globalplatform_version, info?.globalPlatformVersion))
add(Item(R.string.euicc_info_pp_version, info?.ppVersion.toString())) add(Item(R.string.euicc_info_pp_version, info?.ppVersion))
add(Item(R.string.euicc_info_sas_accreditation_number, info?.sasAccreditationNumber)) add(Item(R.string.euicc_info_sas_accreditation_number, info?.sasAccreditationNumber))
add(Item(R.string.euicc_info_free_nvram, info?.freeNvram?.let(::formatFreeSpace))) add(Item(R.string.euicc_info_free_nvram, info?.freeNvram?.let(::formatFreeSpace)))
} }
@ -134,8 +134,13 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
} }
add(Item(R.string.euicc_info_ci_type, getString(resId))) add(Item(R.string.euicc_info_ci_type, getString(resId)))
} }
val atr = channel.atr?.encodeHex() ?: getString(R.string.information_unavailable) add(
add(Item(R.string.euicc_info_atr, atr, copiedToastResId = R.string.toast_atr_copied)) Item(
R.string.euicc_info_atr,
channel.atr?.encodeHex() ?: getString(R.string.information_unavailable),
copiedToastResId = R.string.toast_atr_copied,
)
)
} }
private fun formatByBoolean(b: Boolean, res: Pair<Int, Int>): String = private fun formatByBoolean(b: Boolean, res: Pair<Int, Int>): String =

View file

@ -1,49 +0,0 @@
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
}
}

View file

@ -1,20 +0,0 @@
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
}
}

View file

@ -1,147 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="no_euicc">在此裝置上未檢測到此應用程式可訪問的可插拔 eUICC 卡。請插入相容卡或 USB 晶片讀卡機。</string>
<string name="no_profile">此 eSIM 上還沒有設定檔</string>
<string name="unknown">未知</string>
<string name="help">幫助</string>
<string name="reload">重新載入卡槽</string>
<string name="channel_name_format">虛擬卡槽 %d</string>
<string name="enabled">已啟用</string>
<string name="disabled">已停用</string>
<string name="provider">電信業者:</string>
<string name="profile_class">類型:</string>
<string name="enable">啟用</string>
<string name="disable">停用</string>
<string name="delete">刪除</string>
<string name="rename">重新命名</string>
<string name="enable_disable_timeout">等待 eSIM 切換設定檔時逾時。這可能是您手機基頻處理器韌體中的一個錯誤。請嘗試切換飛航模式、重新啟動應用程式或重新啟動手機</string>
<string name="switch_did_not_refresh">操作成功, 但是您手機的基頻處理器沒有重新整理。您可能需要切換飛航模式或重新啟動,以便使用新的設定檔。</string>
<string name="toast_profile_enable_failed">無法切換到新的 eSIM 設定檔。</string>
<string name="toast_profile_delete_confirm_text_mismatched">輸入的確認文字不匹配</string>
<string name="toast_iccid_copied">已複製 ICCID 到剪貼簿</string>
<string name="toast_eid_copied">已複製 EID 到剪貼簿</string>
<string name="toast_atr_copied">已複製 ATR 到剪貼簿</string>
<string name="usb_permission">授予 USB 權限</string>
<string name="usb_permission_needed">需要獲得訪問 USB 晶片讀卡機的權限。</string>
<string name="usb_failed">無法透過 USB 晶片讀卡機連線到 eSIM。</string>
<string name="task_notification">長時間運行的背景作業</string>
<string name="task_profile_download">正在下載 eSIM 設定檔</string>
<string name="task_profile_download_failure">無法下載 eSIM 設定檔</string>
<string name="task_profile_rename">正在重新命名 eSIM 設定檔</string>
<string name="task_profile_rename_failure">無法重新命名 eSIM 設定檔</string>
<string name="task_profile_delete">正在刪除 eSIM 設定檔</string>
<string name="task_profile_delete_failure">無法刪除 eSIM 設定檔</string>
<string name="task_profile_switch">正在切換 eSIM 設定檔</string>
<string name="task_profile_switch_failure">無法切換 eSIM 設定檔</string>
<string name="profile_download">新增新 eSIM</string>
<string name="profile_download_server">伺服器 (RSP / SM-DP+)</string>
<string name="profile_download_code">啟用碼</string>
<string name="profile_download_confirmation_code">確認碼 (可選)</string>
<string name="profile_download_imei">IMEI (可選)</string>
<string name="profile_download_low_nvram_title">本次下載可能會失敗</string>
<string name="profile_download_low_nvram_message">目前晶片的剩餘空間不足,可能導致配置下載失敗。\n是否繼續下載</string>
<string name="logs_saved_message">日誌已儲存到指定路徑。需要透過其他 App 分享嗎?</string>
<string name="profile_rename_new_name">新名稱</string>
<string name="profile_rename_encoding_error">無法將名稱編碼為 UTF-8</string>
<string name="profile_rename_too_long">名稱長於 64 字元</string>
<string name="profile_rename_failure">重新命名設定檔時發生了未知錯誤</string>
<string name="profile_delete_confirm">您確定要刪除 %s 嗎?此動作無法還原。</string>
<string name="profile_delete_confirm_input">請輸入\'%s\'以確認刪除</string>
<string name="profile_notifications">通知列表</string>
<string name="profile_notifications_detailed_format">通知列表 (%s)</string>
<string name="profile_notifications_show">管理通知</string>
<string name="profile_notifications_help">eSIM 設定檔可以在下載、刪除、啟用或停用時向電信業者傳送通知。此處列出了要傳送的這些通知的佇列。\n\n在\"設定\"中,您可以指定是否自動傳送每種型別的通知。請注意,即使通知已傳送,也不會自動從記錄中刪除,除非佇列空間不足。\n\n在這裡您可以手動傳送或刪除每個待處理的通知。</string>
<string name="profile_notification_operation_download">已下載</string>
<string name="profile_notification_operation_delete">已刪除</string>
<string name="profile_notification_operation_enable">已啟用</string>
<string name="profile_notification_operation_disable">已停用</string>
<string name="profile_notification_process">處理</string>
<string name="profile_notification_delete">刪除</string>
<string name="logs_save">儲存日誌</string>
<string name="logs_filename_template">%s 的日誌</string>
<string name="pref_settings">設定</string>
<string name="pref_notifications">通知</string>
<string name="pref_notifications_desc">變更 eSIM 設定檔會向電信業者傳送通知。根據需要在此處微調此行為。</string>
<string name="pref_notifications_download">下載</string>
<string name="pref_notifications_download_desc">傳送 <i>下載</i> 設定檔的通知</string>
<string name="pref_notifications_delete">刪除</string>
<string name="pref_notifications_delete_desc">傳送 <i>刪除</i> 設定檔的通知</string>
<string name="pref_notifications_switch">切換</string>
<string name="pref_advanced_verbose_logging">記錄詳細日誌</string>
<string name="pref_advanced_verbose_logging_desc">詳細日誌中包含敏感資訊,開啟此功能後請僅與你信任的人共享你的日誌。</string>
<string name="pref_advanced_logs">日誌</string>
<string name="pref_advanced_logs_desc">檢視應用程式的最新除錯日誌</string>
<string name="pref_notifications_switch_desc">傳送 <i>切換</i> 設定檔的通知\n注意這種型別的通知是不可靠的。</string>
<string name="pref_advanced">進階</string>
<string name="pref_advanced_disable_safeguard_removable_esim">允許 停用/刪除 已啟用的設定檔</string>
<string name="pref_advanced_disable_safeguard_removable_esim_desc">預設情況下,此應用程式會阻止您停用可插拔 eSIM 中已啟用的設定檔。\n因為這樣做 <i>有時</i> 會導致無法存取。\n勾選此框以 <i>移除</i> 此保護措施。</string>
<string name="pref_info">資訊</string>
<string name="pref_info_app_version">App 版本</string>
<string name="pref_info_source_code">原始碼</string>
<string name="profile_class_testing">測試</string>
<string name="profile_class_provisioning">準備中</string>
<string name="profile_class_operational">可用</string>
<string name="profile_download_no_lpa_string">未在剪貼簿上發現 LPA 碼</string>
<string name="profile_download_incorrect_lpa_string">LPA 碼解析錯誤</string>
<string name="profile_download_incorrect_lpa_string_message">無法將二維碼或剪貼簿內容解析為 LPA 碼</string>
<string name="download_wizard">下載精靈</string>
<string name="download_wizard_back">返回</string>
<string name="download_wizard_next">下一步</string>
<string name="download_wizard_slot_removed">您選擇的 SIM 卡已被移除</string>
<string name="download_wizard_slot_select">請選擇或確認下載目標 eSIM 卡槽:</string>
<string name="download_wizard_slot_type">型別:</string>
<string name="download_wizard_slot_type_removable">可插拔</string>
<string name="download_wizard_slot_type_internal">內建</string>
<string name="download_wizard_slot_type_internal_port">內建, 埠 %d</string>
<string name="download_wizard_slot_active_profile">目前設定檔:</string>
<string name="download_wizard_slot_free_space">剩餘空間:</string>
<string name="download_wizard_method_select">您想要如何下載 eSIM 設定檔?</string>
<string name="download_wizard_method_qr_code">用相機掃描二維碼</string>
<string name="download_wizard_method_gallery">從相簿選擇二維碼</string>
<string name="download_wizard_method_clipboard">從剪貼簿讀取</string>
<string name="download_wizard_method_manual">手動輸入</string>
<string name="download_wizard_details">請輸入或確認下載 eSIM 的詳細資訊:</string>
<string name="download_wizard_progress">正在下載您的 eSIM...</string>
<string name="download_wizard_progress_step_preparing">準備中</string>
<string name="download_wizard_progress_step_connecting">正在連線到伺服器</string>
<string name="download_wizard_progress_step_authenticating">正在向伺服器驗證您的裝置</string>
<string name="download_wizard_progress_step_downloading">正在下載 eSIM 設定檔</string>
<string name="download_wizard_progress_step_finalizing">正在寫入 eSIM 設定檔</string>
<string name="download_wizard_diagnostics">錯誤診斷</string>
<string name="download_wizard_diagnostics_error_code">錯誤代碼: %s</string>
<string name="download_wizard_diagnostics_last_http_status">上次 HTTP 狀態碼 (來自伺服器): %d</string>
<string name="download_wizard_diagnostics_last_http_response">上次 HTTP 應答 (來自伺服器):</string>
<string name="download_wizard_diagnostics_last_http_exception">上次 HTTP 錯誤:</string>
<string name="download_wizard_diagnostics_last_apdu_response">上次 APDU 應答 (來自 SIM): %s</string>
<string name="download_wizard_diagnostics_last_apdu_response_success">上次 APDU 應答 (來自 SIM) 是成功的</string>
<string name="download_wizard_diagnostics_last_apdu_response_fail">上次 APDU 應答 (來自 SIM) 是失敗的</string>
<string name="download_wizard_diagnostics_last_apdu_exception">上次 APDU 錯誤:</string>
<string name="download_wizard_diagnostics_save">儲存</string>
<string name="download_wizard_diagnostics_file_template">%s 的錯誤診斷</string>
<string name="euicc_info">eUICC 詳情</string>
<string name="euicc_info_activity_title">eUICC 詳情 (%s)</string>
<string name="euicc_info_access_mode">訪問方式</string>
<string name="euicc_info_removable">可插拔</string>
<string name="euicc_info_sgp22_version">SGP.22 版本</string>
<string name="euicc_info_firmware_version">eUICC OS 版本</string>
<string name="euicc_info_globalplatform_version">GlobalPlatform 版本</string>
<string name="euicc_info_sas_accreditation_number">SAS 認證號碼</string>
<string name="euicc_info_pp_version">Protected Profile 版本</string>
<string name="euicc_info_free_nvram">NVRAM 剩餘空間 (eSIM 儲存容量)</string>
<string name="euicc_info_ci_type">證書簽發者 (CI)</string>
<string name="euicc_info_ci_gsma_live">GSMA 生產環境 CI</string>
<string name="euicc_info_ci_gsma_test">GSMA 測試 CI</string>
<string name="euicc_info_ci_unknown">未知 eSIM CI</string>
<string name="yes"></string>
<string name="no"></string>
<string name="developer_options_steps">還有 %d 步成為開發者</string>
<string name="developer_options_enabled">您現在是開發者了!</string>
<string name="pref_advanced_language">語言</string>
<string name="pref_advanced_language_desc">選擇 App 語言</string>
<string name="pref_developer">開發人員選項</string>
<string name="pref_developer_unfiltered_profile_list">顯示未經過濾的設定檔列表</string>
<string name="pref_developer_unfiltered_profile_list_desc">在設定檔列表中包括非生產環境的設定檔</string>
<string name="pref_developer_ignore_tls_certificate">忽略 SM-DP+ 的 TLS 證書</string>
<string name="pref_developer_ignore_tls_certificate_desc">允許 RSP 伺服器使用任意證書</string>
<string name="information_unavailable">無資訊</string>
</resources>

View file

@ -31,7 +31,6 @@
<string name="toast_profile_enable_failed">Cannot switch to new eSIM profile.</string> <string name="toast_profile_enable_failed">Cannot switch to new eSIM profile.</string>
<string name="toast_profile_delete_confirm_text_mismatched">Confirmation string mismatch</string> <string name="toast_profile_delete_confirm_text_mismatched">Confirmation string mismatch</string>
<string name="toast_iccid_copied">ICCID copied to clipboard</string> <string name="toast_iccid_copied">ICCID copied to clipboard</string>
<string name="toast_sn_copied">Serial Number copied to clipboard</string>
<string name="toast_eid_copied">EID copied to clipboard</string> <string name="toast_eid_copied">EID copied to clipboard</string>
<string name="toast_atr_copied">ATR copied to clipboard</string> <string name="toast_atr_copied">ATR copied to clipboard</string>
@ -126,10 +125,6 @@
<string name="euicc_info_activity_title">eUICC Info (%s)</string> <string name="euicc_info_activity_title">eUICC Info (%s)</string>
<string name="euicc_info_access_mode">Access Mode</string> <string name="euicc_info_access_mode">Access Mode</string>
<string name="euicc_info_removable">Removable</string> <string name="euicc_info_removable">Removable</string>
<string name="euicc_info_sku">Product Name</string>
<string name="euicc_info_sn">Product Serial Number</string>
<string name="euicc_info_bl_ver">Product Bootloader Version</string>
<string name="euicc_info_fw_ver">Product Firmware Version</string>
<string name="euicc_info_eid" translatable="false">EID</string> <string name="euicc_info_eid" translatable="false">EID</string>
<string name="euicc_info_sgp22_version">SGP.22 Version</string> <string name="euicc_info_sgp22_version">SGP.22 Version</string>
<string name="euicc_info_firmware_version">eUICC OS Version</string> <string name="euicc_info_firmware_version">eUICC OS Version</string>

View file

@ -1,32 +0,0 @@
<resources>
<string name="compatibility_check">相容性檢查</string>
<string name="open_sim_toolkit">啟動 SIM 卡應用程式</string>
<string name="compatibility_check_system_features">系統功能</string>
<string name="compatibility_check_system_features_desc">您的裝置是否具有管理可插拔 eUICC 卡所需的所有功能。例如,基本的電話功能和 OMAPI 支援。</string>
<string name="compatibility_check_system_features_no_telephony">您的裝置沒有電話功能。</string>
<string name="compatibility_check_system_features_no_omapi">您的裝置/系統未宣告支援 OMAPI。這可能是由於缺少硬體支援或者可能僅僅是由於缺少標誌。請參閱以下兩項檢查以確定 OMAPI 是否確實受支援。</string>
<string name="compatibility_check_omapi_connectivity">OMAPI 連線</string>
<string name="compatibility_check_omapi_connectivity_desc">您的裝置是否允許透過 OMAPI 存取 SIM 卡上的安全元件?</string>
<string name="compatibility_check_omapi_connectivity_fail">無法透過 OMAPI 偵測到 SIM 卡的 Secure Element。如果您尚未在此裝置中插入 SIM 卡,請嘗試插入一張 SIM 卡並重試此檢查。</string>
<string name="compatibility_check_omapi_connectivity_partial_success_sim_number">已成功檢測到可存取 Secure Element 的卡槽,但僅限於以下 SIM 卡槽:<b>SIM%s</b></string>
<string name="compatibility_check_isdr_channel">ISD-R 通道存取</string>
<string name="compatibility_check_isdr_channel_desc">您的裝置是否支援透過 OMAPI 開啟 eSIM 的 ISD-R (管理) 通道?</string>
<string name="compatibility_check_isdr_channel_desc_unknown">無法確定是否支援透過 OMAPI 進行 ISD-R 的存取。如果尚未插入,您可能需要插入 SIM 卡 (任何 SIM 卡都可以) 重試。</string>
<string name="compatibility_check_isdr_channel_desc_partial_fail">OMAPI 只能在以下 SIM 插槽上存取 ISD-R<b>SIM%s</b></string>
<string name="compatibility_check_known_broken">不在已知錯誤清單中</string>
<string name="compatibility_check_known_broken_desc">確保您的裝置不存在與可插拔 eSIM 相關的錯誤。</string>
<string name="compatibility_check_known_broken_fail">很抱歉,您的裝置在存取可插拔 eSIM 時存在錯誤。這並不表示完全無法使用,但我們不保證該應用在您裝置上的行為。</string>
<string name="compatibility_check_usb">USB 晶片讀卡機支援</string>
<string name="compatibility_check_usb_desc">您的裝置是否支援透過 USB 晶片讀卡機管理 eSIM</string>
<string name="compatibility_check_usb_ok">您可以透過此裝置上的標準 USB CCID 讀卡機管理 eSIM (即使您在這裡有任何其他檢查項失敗)。請插入讀卡機,然後開啟此應用程式以這種方式管理 eSIM。</string>
<string name="compatibility_check_usb_fail">您的裝置不支援 USB 晶片讀卡機。</string>
<string name="compatibility_check_verdict">結論 (USB 晶片讀卡機以外)</string>
<string name="compatibility_check_verdict_desc">根據之前的所有檢查,您的裝置與可插拔 eSIM 卡相容的可能性有多大?</string>
<string name="compatibility_check_verdict_ok">您可以使用和管理插入此裝置的可插拔 eSIM 卡。</string>
<string name="compatibility_check_verdict_known_broken">已知您的裝置在存取可插拔 eSIM 卡時存在問題。\n%s</string>
<string name="compatibility_check_verdict_unknown_likely_ok">我們無法確定是否可以在您的裝置上管理可插拔 eSIM 卡。不過,您的裝置確實宣告支援 OMAPI因此它工作的可能性略高。\n%s</string>
<string name="compatibility_check_verdict_unknown_likely_fail">我們無法確定是否可以在您的裝置上管理可插拔 eSIM 卡。由於您的裝置未宣告支援OMAPI因此更有可能不支援在此裝置上管理可插拔 eSIM。\n%s</string>
<string name="compatibility_check_verdict_unknown">我們無法確定是否可以在您的裝置上管理可插拔 eSIM 卡。\n%s</string>
<string name="compatibility_check_verdict_fail_shared">然而已經載入了eSIM設定檔的可插拔 eSIM 卡仍然可以工作; 即使無法在裝置上直接管理可插拔 eSIM 卡中的設定檔,您仍然可以使用 USB 卡讀卡機來管理設定檔。</string>
<string name="toast_ara_m_copied">ARA-M SHA-1 已複製到剪貼簿</string>
</resources>

View file

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="no_euicc_priv">在此裝置上找不到 eUICC 晶片。\n在某些裝置上您可能需要先在此應用的選單中啟用雙卡支援。</string>
<string name="dsds">雙卡</string>
<string name="toast_dsds_switched">雙卡支援狀態已切換。請等待基頻處理器重新啟動。</string>
<string name="footer_mep">此卡槽支援多個啟用設定檔 (MEP)。要啟用或停用此功能,請使用\"卡槽對映\"工具。</string>
<string name="slot_mapping">卡槽對映</string>
<string name="slot_mapping_logical_slot">虛擬卡槽 %d:</string>
<string name="slot_mapping_port">卡槽 %1$d 端口 %2$d</string>
<string name="slot_mapping_help">您的手機有 %1$d 個虛擬 SIM 卡槽和 %2$d 個實體 SIM 卡槽。%3$s\n\n選擇您希望每個虛擬卡槽對應的實體卡槽 和/或 \"端口\"。請注意,並非所有對映模式都受硬體支援。</string>
<string name="slot_mapping_help_mep">\n\n實體卡槽 %1$d 支援多個啟用的設定檔 (MEP)。要使用此功能,請將其 %2$d 個虛擬\"端口\"分配給上面顯示的不同虛擬卡槽。\n\n啟用 MEP 後,\"端口\"會在 OpenEUICC 中顯示為共享 eSIM 設定檔的獨立的 eSIM 卡槽。</string>
<string name="slot_mapping_help_dsds">\n支援雙卡模式但已停用。如果您的裝置帶有內建 eSIM 晶片,則預設情況下可能不會啟用。更改上面的對映或啟用雙卡以訪問您的 eSIM。</string>
<string name="slot_mapping_completed">您的新卡槽對映已設定完畢。請等待基頻處理器重新整理卡槽。</string>
<string name="slot_mapping_failure">指定的對映可能無效或硬體不支援您指定的對映。</string>
<string name="lui_title">透過下載 eSIM 連線到行動網路</string>
<string name="lui_desc">您的裝置支援 eSIM。要連線到行動網路請下載電信業者釋出的 eSIM或插入實體 SIM 卡。</string>
<string name="lui_skip">跳過</string>
<string name="lui_download">下載 eSIM</string>
<string name="telephony_manager">TelephonyManager (特權)</string>
</resources>

View file

@ -2,31 +2,14 @@ package net.typeblog.lpac_jni
/* Corresponds to EuiccInfo2 in SGP.22 */ /* Corresponds to EuiccInfo2 in SGP.22 */
data class EuiccInfo2( data class EuiccInfo2(
val sgp22Version: Version, val sgp22Version: String,
val profileVersion: Version, val profileVersion: String,
val euiccFirmwareVersion: Version, val euiccFirmwareVersion: String,
val globalPlatformVersion: Version, val globalPlatformVersion: String,
val sasAccreditationNumber: String, val sasAccreditationNumber: String,
val ppVersion: Version, val ppVersion: String,
val freeNvram: Int, val freeNvram: Int,
val freeRam: Int, val freeRam: Int,
val euiccCiPKIdListForSigning: Set<String>, val euiccCiPKIdListForSigning: Array<String>,
val euiccCiPKIdListForVerification: Set<String>, val euiccCiPKIdListForVerification: Array<String>,
) )
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<Int>) : 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"
}

View file

@ -10,7 +10,6 @@ import net.typeblog.lpac_jni.LocalProfileAssistant
import net.typeblog.lpac_jni.LocalProfileInfo import net.typeblog.lpac_jni.LocalProfileInfo
import net.typeblog.lpac_jni.LocalProfileNotification import net.typeblog.lpac_jni.LocalProfileNotification
import net.typeblog.lpac_jni.ProfileDownloadCallback import net.typeblog.lpac_jni.ProfileDownloadCallback
import net.typeblog.lpac_jni.Version
class LocalProfileAssistantImpl( class LocalProfileAssistantImpl(
isdrAid: ByteArray, isdrAid: ByteArray,
@ -85,8 +84,8 @@ class LocalProfileAssistantImpl(
throw IllegalArgumentException("Failed to initialize LPA") throw IllegalArgumentException("Failed to initialize LPA")
} }
val pkids = euiccInfo2?.euiccCiPKIdListForVerification ?: setOf() val pkids = euiccInfo2?.euiccCiPKIdListForVerification ?: arrayOf()
httpInterface.usePublicKeyIds(pkids.toTypedArray()) httpInterface.usePublicKeyIds(pkids)
} }
override fun setEs10xMss(mss: Byte) { override fun setEs10xMss(mss: Byte) {
@ -158,29 +157,31 @@ class LocalProfileAssistantImpl(
val cInfo = LpacJni.es10cexGetEuiccInfo2(contextHandle) val cInfo = LpacJni.es10cexGetEuiccInfo2(contextHandle)
if (cInfo == 0L) return null if (cInfo == 0L) return null
val euiccCiPKIdListForSigning = mutableListOf<String>()
var curr = LpacJni.euiccInfo2GetEuiccCiPKIdListForSigning(cInfo)
while (curr != 0L) {
euiccCiPKIdListForSigning.add(LpacJni.stringDeref(curr))
curr = LpacJni.stringArrNext(curr)
}
val euiccCiPKIdListForVerification = mutableListOf<String>()
curr = LpacJni.euiccInfo2GetEuiccCiPKIdListForVerification(cInfo)
while (curr != 0L) {
euiccCiPKIdListForVerification.add(LpacJni.stringDeref(curr))
curr = LpacJni.stringArrNext(curr)
}
val ret = EuiccInfo2( val ret = EuiccInfo2(
Version(LpacJni.euiccInfo2GetSGP22Version(cInfo)), LpacJni.euiccInfo2GetSGP22Version(cInfo),
Version(LpacJni.euiccInfo2GetProfileVersion(cInfo)), LpacJni.euiccInfo2GetProfileVersion(cInfo),
Version(LpacJni.euiccInfo2GetEuiccFirmwareVersion(cInfo)), LpacJni.euiccInfo2GetEuiccFirmwareVersion(cInfo),
Version(LpacJni.euiccInfo2GetGlobalPlatformVersion(cInfo)), LpacJni.euiccInfo2GetGlobalPlatformVersion(cInfo),
LpacJni.euiccInfo2GetSasAcreditationNumber(cInfo), LpacJni.euiccInfo2GetSasAcreditationNumber(cInfo),
Version(LpacJni.euiccInfo2GetPpVersion(cInfo)), LpacJni.euiccInfo2GetPpVersion(cInfo),
LpacJni.euiccInfo2GetFreeNonVolatileMemory(cInfo).toInt(), LpacJni.euiccInfo2GetFreeNonVolatileMemory(cInfo).toInt(),
LpacJni.euiccInfo2GetFreeVolatileMemory(cInfo).toInt(), LpacJni.euiccInfo2GetFreeVolatileMemory(cInfo).toInt(),
buildSet { euiccCiPKIdListForSigning.toTypedArray(),
var cursor = LpacJni.euiccInfo2GetEuiccCiPKIdListForSigning(cInfo) euiccCiPKIdListForVerification.toTypedArray()
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) LpacJni.euiccInfo2Free(cInfo)

View file

@ -14,7 +14,7 @@ const val DEFAULT_PKID_GSMA_RSP2_ROOT_CI1 = "81370f5125d0b1d408d4c3b232e6d25e795
// List of GSMA Live CIs // List of GSMA Live CIs
// https://www.gsma.com/solutions-and-impact/technologies/esim/gsma-root-ci/ // https://www.gsma.com/solutions-and-impact/technologies/esim/gsma-root-ci/
val PKID_GSMA_LIVE_CI = setOf( val PKID_GSMA_LIVE_CI = arrayOf(
// GSMA RSP2 Root CI1 (SGP.22 v2+v3, CA: DigiCert) // GSMA RSP2 Root CI1 (SGP.22 v2+v3, CA: DigiCert)
// https://euicc-manual.osmocom.org/docs/pki/ci/files/81370f.txt // https://euicc-manual.osmocom.org/docs/pki/ci/files/81370f.txt
DEFAULT_PKID_GSMA_RSP2_ROOT_CI1, DEFAULT_PKID_GSMA_RSP2_ROOT_CI1,
@ -25,7 +25,7 @@ val PKID_GSMA_LIVE_CI = setOf(
// SGP.26 v3.0, 2023-12-01 // 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 // https://www.gsma.com/solutions-and-impact/technologies/esim/wp-content/uploads/2023/12/SGP.26-v3.0.pdf
val PKID_GSMA_TEST_CI = setOf( val PKID_GSMA_TEST_CI = arrayOf(
// Test CI (SGP.26, NIST P256) // Test CI (SGP.26, NIST P256)
// https://euicc-manual.osmocom.org/docs/pki/ci/files/34eecf.txt // https://euicc-manual.osmocom.org/docs/pki/ci/files/34eecf.txt
"34eecf13156518d48d30bdf06853404d115f955d", "34eecf13156518d48d30bdf06853404d115f955d",