Compare commits
12 commits
3acc46f3ab
...
703bcb9cd6
Author | SHA1 | Date | |
---|---|---|---|
703bcb9cd6 | |||
f3fa192531 | |||
d67d9a2696 | |||
d053be28e5 | |||
6324c80601 | |||
d5aefcaec7 | |||
ef295c9d12 | |||
1d67fa5cfa | |||
c8ecdee095 | |||
03bfdf373c | |||
9517f53712 | |||
f5074acae2 |
40 changed files with 630 additions and 238 deletions
1
.idea/.gitignore
generated
vendored
1
.idea/.gitignore
generated
vendored
|
@ -9,5 +9,6 @@
|
||||||
/navEditor.xml
|
/navEditor.xml
|
||||||
/runConfigurations.xml
|
/runConfigurations.xml
|
||||||
/workspace.xml
|
/workspace.xml
|
||||||
|
/AndroidProjectSystem.xml
|
||||||
|
|
||||||
**/*.iml
|
**/*.iml
|
|
@ -8,7 +8,8 @@ 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.getIoEndpoints
|
import im.angry.openeuicc.core.usb.bulkPair
|
||||||
|
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
|
||||||
|
|
||||||
|
@ -61,7 +62,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.getIoEndpoints()
|
val (bulkIn, bulkOut) = usbInterface.endpoints.bulkPair
|
||||||
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
|
||||||
|
|
|
@ -5,7 +5,8 @@ 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.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.di.AppContainer
|
||||||
import im.angry.openeuicc.util.*
|
import im.angry.openeuicc.util.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -244,7 +245,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.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
|
// 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)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package im.angry.openeuicc.core
|
package im.angry.openeuicc.core
|
||||||
|
|
||||||
import im.angry.openeuicc.util.*
|
import im.angry.openeuicc.util.*
|
||||||
|
import net.typeblog.lpac_jni.ApduInterface
|
||||||
import net.typeblog.lpac_jni.LocalProfileAssistant
|
import net.typeblog.lpac_jni.LocalProfileAssistant
|
||||||
|
|
||||||
interface EuiccChannel {
|
interface EuiccChannel {
|
||||||
|
@ -28,5 +29,10 @@ interface EuiccChannel {
|
||||||
*/
|
*/
|
||||||
val intrinsicChannelName: String?
|
val intrinsicChannelName: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The underlying APDU interface for this channel
|
||||||
|
*/
|
||||||
|
val apduInterface: ApduInterface
|
||||||
|
|
||||||
fun close()
|
fun close()
|
||||||
}
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package im.angry.openeuicc.core
|
package im.angry.openeuicc.core
|
||||||
|
|
||||||
import im.angry.openeuicc.util.*
|
import im.angry.openeuicc.util.UiccPortInfoCompat
|
||||||
|
import im.angry.openeuicc.util.decodeHex
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import net.typeblog.lpac_jni.ApduInterface
|
import net.typeblog.lpac_jni.ApduInterface
|
||||||
import net.typeblog.lpac_jni.LocalProfileAssistant
|
import net.typeblog.lpac_jni.LocalProfileAssistant
|
||||||
|
@ -11,16 +12,25 @@ class EuiccChannelImpl(
|
||||||
override val type: String,
|
override val type: String,
|
||||||
override val port: UiccPortInfoCompat,
|
override val port: UiccPortInfoCompat,
|
||||||
override val intrinsicChannelName: String?,
|
override val intrinsicChannelName: String?,
|
||||||
private val apduInterface: ApduInterface,
|
override val apduInterface: ApduInterface,
|
||||||
verboseLoggingFlow: Flow<Boolean>,
|
verboseLoggingFlow: Flow<Boolean>,
|
||||||
ignoreTLSCertificateFlow: Flow<Boolean>
|
ignoreTLSCertificateFlow: Flow<Boolean>
|
||||||
) : EuiccChannel {
|
) : EuiccChannel {
|
||||||
|
companion object {
|
||||||
|
// TODO: This needs to go somewhere else.
|
||||||
|
val ISDR_AID = "A0000005591010FFFFFFFF8900000100".decodeHex()
|
||||||
|
}
|
||||||
|
|
||||||
override val slotId = port.card.physicalSlotIndex
|
override val slotId = port.card.physicalSlotIndex
|
||||||
override val logicalSlotId = port.logicalSlotIndex
|
override val logicalSlotId = port.logicalSlotIndex
|
||||||
override val portId = port.portIndex
|
override val portId = port.portIndex
|
||||||
|
|
||||||
override val lpa: LocalProfileAssistant =
|
override val lpa: LocalProfileAssistant =
|
||||||
LocalProfileAssistantImpl(apduInterface, HttpInterfaceImpl(verboseLoggingFlow, ignoreTLSCertificateFlow))
|
LocalProfileAssistantImpl(
|
||||||
|
ISDR_AID,
|
||||||
|
apduInterface,
|
||||||
|
HttpInterfaceImpl(verboseLoggingFlow, ignoreTLSCertificateFlow)
|
||||||
|
)
|
||||||
|
|
||||||
override val atr: ByteArray?
|
override val atr: ByteArray?
|
||||||
get() = (apduInterface as? ApduInterfaceAtrProvider)?.atr
|
get() = (apduInterface as? ApduInterfaceAtrProvider)?.atr
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package im.angry.openeuicc.core
|
package im.angry.openeuicc.core
|
||||||
|
|
||||||
import im.angry.openeuicc.util.*
|
import im.angry.openeuicc.util.*
|
||||||
|
import net.typeblog.lpac_jni.ApduInterface
|
||||||
import net.typeblog.lpac_jni.LocalProfileAssistant
|
import net.typeblog.lpac_jni.LocalProfileAssistant
|
||||||
|
|
||||||
class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel {
|
class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel {
|
||||||
|
@ -33,6 +34,8 @@ class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel {
|
||||||
get() = channel.valid
|
get() = channel.valid
|
||||||
override val intrinsicChannelName: String?
|
override val intrinsicChannelName: String?
|
||||||
get() = channel.intrinsicChannelName
|
get() = channel.intrinsicChannelName
|
||||||
|
override val apduInterface: ApduInterface
|
||||||
|
get() = channel.apduInterface
|
||||||
override val atr: ByteArray?
|
override val atr: ByteArray?
|
||||||
get() = channel.atr
|
get() = channel.atr
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,6 @@ import android.util.Log
|
||||||
import im.angry.openeuicc.util.*
|
import im.angry.openeuicc.util.*
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.single
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import net.typeblog.lpac_jni.ApduInterface
|
import net.typeblog.lpac_jni.ApduInterface
|
||||||
|
|
||||||
|
@ -21,7 +20,12 @@ class OmapiApduInterface(
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var session: Session
|
private lateinit var session: Session
|
||||||
private lateinit var lastChannel: Channel
|
private val channels = arrayOf<Channel?>(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
|
||||||
override val valid: Boolean
|
override val valid: Boolean
|
||||||
get() = service.isConnected && (this::session.isInitialized && !session.isClosed)
|
get() = service.isConnected && (this::session.isInitialized && !session.isClosed)
|
||||||
|
@ -38,24 +42,24 @@ class OmapiApduInterface(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun logicalChannelOpen(aid: ByteArray): Int {
|
override fun logicalChannelOpen(aid: ByteArray): Int {
|
||||||
check(!this::lastChannel.isInitialized) {
|
val channel = session.openLogicalChannel(aid)
|
||||||
"Can only open one channel"
|
check(channel != null) { "Failed to open logical channel (${aid.encodeHex()})" }
|
||||||
}
|
val index = channels.indexOf(null)
|
||||||
lastChannel = session.openLogicalChannel(aid)!!
|
check(index != -1) { "No free logical channel slots" }
|
||||||
return 1
|
synchronized(channels) { channels[index] = channel }
|
||||||
|
return index
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun logicalChannelClose(handle: Int) {
|
override fun logicalChannelClose(handle: Int) {
|
||||||
check(handle == 1 && !this::lastChannel.isInitialized) {
|
val channel = channels.getOrNull(handle)
|
||||||
"Unknown channel"
|
check(channel != null) { "Invalid logical channel handle $handle" }
|
||||||
}
|
if (channel.isOpen) channel.close()
|
||||||
lastChannel.close()
|
synchronized(channels) { channels[handle] = null }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun transmit(tx: ByteArray): ByteArray {
|
override fun transmit(handle: Int, tx: ByteArray): ByteArray {
|
||||||
check(this::lastChannel.isInitialized) {
|
val channel = channels.getOrNull(handle)
|
||||||
"Unknown channel"
|
check(channel != null) { "Invalid logical channel handle $handle" }
|
||||||
}
|
|
||||||
|
|
||||||
if (runBlocking { verboseLoggingFlow.first() }) {
|
if (runBlocking { verboseLoggingFlow.first() }) {
|
||||||
Log.d(TAG, "OMAPI APDU: ${tx.encodeHex()}")
|
Log.d(TAG, "OMAPI APDU: ${tx.encodeHex()}")
|
||||||
|
@ -63,7 +67,7 @@ class OmapiApduInterface(
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (i in 0..10) {
|
for (i in 0..10) {
|
||||||
val res = lastChannel.transmit(tx)
|
val res = channel.transmit(tx)
|
||||||
if (runBlocking { verboseLoggingFlow.first() }) {
|
if (runBlocking { verboseLoggingFlow.first() }) {
|
||||||
Log.d(TAG, "OMAPI APDU response: ${res.encodeHex()}")
|
Log.d(TAG, "OMAPI APDU response: ${res.encodeHex()}")
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,10 +21,13 @@ class UsbApduInterface(
|
||||||
private lateinit var ccidDescription: UsbCcidDescription
|
private lateinit var ccidDescription: UsbCcidDescription
|
||||||
private lateinit var transceiver: UsbCcidTransceiver
|
private lateinit var transceiver: UsbCcidTransceiver
|
||||||
|
|
||||||
private var channelId = -1
|
|
||||||
|
|
||||||
override var atr: ByteArray? = null
|
override var atr: ByteArray? = null
|
||||||
|
|
||||||
|
override val valid: Boolean
|
||||||
|
get() = channels.isNotEmpty()
|
||||||
|
|
||||||
|
private var channels = mutableSetOf<Int>()
|
||||||
|
|
||||||
override fun connect() {
|
override fun connect() {
|
||||||
ccidDescription = UsbCcidDescription.fromRawDescriptors(conn.rawDescriptors)!!
|
ccidDescription = UsbCcidDescription.fromRawDescriptors(conn.rawDescriptors)!!
|
||||||
|
|
||||||
|
@ -46,11 +49,11 @@ class UsbApduInterface(
|
||||||
|
|
||||||
override fun disconnect() {
|
override fun disconnect() {
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
atr = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun logicalChannelOpen(aid: ByteArray): Int {
|
override fun logicalChannelOpen(aid: ByteArray): Int {
|
||||||
check(channelId == -1) { "Logical channel already opened" }
|
|
||||||
|
|
||||||
// OPEN LOGICAL CHANNEL
|
// OPEN LOGICAL CHANNEL
|
||||||
val req = manageChannelCmd(true, 0)
|
val req = manageChannelCmd(true, 0)
|
||||||
|
|
||||||
|
@ -66,7 +69,7 @@ class UsbApduInterface(
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
channelId = resp[0].toInt()
|
val channelId = resp[0].toInt()
|
||||||
Log.d(TAG, "channelId = $channelId")
|
Log.d(TAG, "channelId = $channelId")
|
||||||
|
|
||||||
// Then, select AID
|
// Then, select AID
|
||||||
|
@ -78,31 +81,31 @@ class UsbApduInterface(
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
channels.add(channelId)
|
||||||
|
|
||||||
return channelId
|
return channelId
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun logicalChannelClose(handle: Int) {
|
override fun logicalChannelClose(handle: Int) {
|
||||||
check(handle == channelId) { "Logical channel ID mismatch" }
|
check(channels.contains(handle)) {
|
||||||
check(channelId != -1) { "Logical channel is not opened" }
|
"Invalid logical channel handle $handle"
|
||||||
|
}
|
||||||
// CLOSE LOGICAL CHANNEL
|
// CLOSE LOGICAL CHANNEL
|
||||||
val req = manageChannelCmd(false, channelId.toByte())
|
val req = manageChannelCmd(false, handle.toByte())
|
||||||
val resp = transmitApduByChannel(req, channelId.toByte())
|
val resp = transmitApduByChannel(req, handle.toByte())
|
||||||
|
|
||||||
if (!isSuccessResponse(resp)) {
|
if (!isSuccessResponse(resp)) {
|
||||||
Log.d(TAG, "CLOSE LOGICAL CHANNEL failed: ${resp.encodeHex()}")
|
Log.d(TAG, "CLOSE LOGICAL CHANNEL failed: ${resp.encodeHex()}")
|
||||||
}
|
}
|
||||||
|
channels.remove(handle)
|
||||||
channelId = -1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun transmit(tx: ByteArray): ByteArray {
|
override fun transmit(handle: Int, tx: ByteArray): ByteArray {
|
||||||
check(channelId != -1) { "Logical channel is not opened" }
|
check(channels.contains(handle)) {
|
||||||
return transmitApduByChannel(tx, channelId.toByte())
|
"Invalid logical channel handle $handle"
|
||||||
|
}
|
||||||
|
return transmitApduByChannel(tx, handle.toByte())
|
||||||
}
|
}
|
||||||
|
|
||||||
override val valid: Boolean
|
|
||||||
get() = channelId != -1
|
|
||||||
|
|
||||||
private fun isSuccessResponse(resp: ByteArray): Boolean =
|
private fun isSuccessResponse(resp: ByteArray): Boolean =
|
||||||
resp.size >= 2 && resp[resp.size - 2] == 0x90.toByte() && resp[resp.size - 1] == 0x00.toByte()
|
resp.size >= 2 && resp[resp.size - 2] == 0x90.toByte() && resp[resp.size - 1] == 0x00.toByte()
|
||||||
|
|
|
@ -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_EXCHAGE_LEVEL_EXTENDED_APDU = 0x40000
|
private const val FEATURE_EXCHANGE_LEVEL_EXTENDED_APDU = 0x40000
|
||||||
|
|
||||||
// bVoltageSupport Masks
|
// bVoltageSupport Masks
|
||||||
private const val VOLTAGE_5V: Byte = 1
|
private const val VOLTAGE_5V0: Byte = 1
|
||||||
private const val VOLTAGE_3V: Byte = 2
|
private const val VOLTAGE_3V0: Byte = 2
|
||||||
private const val VOLTAGE_1_8V: Byte = 4
|
private const val VOLTAGE_1V8: 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,30 +71,23 @@ data class UsbCcidDescription(
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class Voltage(powerOnValue: Int, mask: Int) {
|
enum class Voltage(powerOnValue: Int, mask: Int) {
|
||||||
AUTO(0, 0), _5V(1, VOLTAGE_5V.toInt()), _3V(2, VOLTAGE_3V.toInt()), _1_8V(
|
// @formatter:off
|
||||||
3,
|
AUTO(0, 0),
|
||||||
VOLTAGE_1_8V.toInt()
|
V50(1, VOLTAGE_5V0.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): Boolean =
|
private fun hasFeature(feature: Int) = (dwFeatures and feature) != 0
|
||||||
(dwFeatures and feature) != 0
|
|
||||||
|
|
||||||
val voltages: Array<Voltage>
|
val voltages: List<Voltage>
|
||||||
get() =
|
get() {
|
||||||
if (hasFeature(FEATURE_AUTOMATIC_VOLTAGE)) {
|
if (hasFeature(FEATURE_AUTOMATIC_VOLTAGE)) return listOf(Voltage.AUTO)
|
||||||
arrayOf(Voltage.AUTO)
|
return Voltage.entries.filter { (it.mask.toInt() and bVoltageSupport.toInt()) != 0 }
|
||||||
} else {
|
|
||||||
Voltage.values().mapNotNull {
|
|
||||||
if ((it.mask.toInt() and bVoltageSupport.toInt()) != 0) {
|
|
||||||
it
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}.toTypedArray()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val hasAutomaticPps: Boolean
|
val hasAutomaticPps: Boolean
|
||||||
|
|
|
@ -95,6 +95,7 @@ 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,
|
||||||
|
@ -183,31 +184,26 @@ 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 {
|
||||||
|
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]) {
|
if (expectedSequenceNumber != inputBuffer[6]) {
|
||||||
throw UsbTransportException(
|
append(", sequence number ")
|
||||||
((("USB-CCID error - bad CCID header, type " + inputBuffer[0]) + " (expected " +
|
append("%d (expected %d)".format(inputBuffer[6], expectedSequenceNumber))
|
||||||
MESSAGE_TYPE_RDR_TO_PC_DATA_BLOCK) + "), sequence number " + inputBuffer[6]
|
|
||||||
) + " (expected " +
|
|
||||||
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(
|
throw UsbTransportException("USB-CCID error - expected sequence number $expectedSequenceNumber, got $result")
|
||||||
("USB-CCID error - expected sequence number " +
|
|
||||||
expectedSequenceNumber + ", got " + result)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val dataBuffer = ByteArray(result.dwLength)
|
val dataBuffer = ByteArray(result.dwLength)
|
||||||
|
@ -218,9 +214,7 @@ 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(
|
throw UsbTransportException("USB error - failed reading response data! Header: $result")
|
||||||
"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
|
||||||
|
@ -285,7 +279,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -293,13 +287,13 @@ class UsbCcidTransceiver(
|
||||||
val startTime = SystemClock.elapsedRealtime()
|
val startTime = SystemClock.elapsedRealtime()
|
||||||
skipAvailableInput()
|
skipAvailableInput()
|
||||||
var response: CcidDataBlock? = null
|
var response: CcidDataBlock? = null
|
||||||
for (v in usbCcidDescription.voltages) {
|
for (voltage in usbCcidDescription.voltages) {
|
||||||
Log.v(TAG, "CCID: attempting to power on with voltage $v")
|
Log.v(TAG, "CCID: attempting to power on with voltage $voltage")
|
||||||
response = try {
|
response = try {
|
||||||
iccPowerOnVoltage(v.powerOnValue)
|
iccPowerOnVoltage(voltage.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 $v")
|
Log.v(TAG, "CCID: failed to power on with voltage $voltage")
|
||||||
iccPowerOff()
|
iccPowerOff()
|
||||||
Log.v(TAG, "CCID: powered off")
|
Log.v(TAG, "CCID: powered off")
|
||||||
continue
|
continue
|
||||||
|
@ -314,8 +308,11 @@ class UsbCcidTransceiver(
|
||||||
val elapsedTime = SystemClock.elapsedRealtime() - startTime
|
val elapsedTime = SystemClock.elapsedRealtime() - startTime
|
||||||
Log.d(
|
Log.d(
|
||||||
TAG,
|
TAG,
|
||||||
"Usb transport connected, took " + elapsedTime + "ms, ATR=" +
|
buildString {
|
||||||
response.data?.encodeHex()
|
append("Usb transport connected")
|
||||||
|
append(", took ", elapsedTime, "ms")
|
||||||
|
append(", ATR=", response.data?.encodeHex())
|
||||||
|
}
|
||||||
)
|
)
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,31 +6,22 @@ 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(msg: String) : Exception(msg)
|
class UsbTransportException(message: String) : Exception(message)
|
||||||
|
|
||||||
fun UsbInterface.getIoEndpoints(): Pair<UsbEndpoint?, UsbEndpoint?> {
|
val UsbDevice.interfaces: Iterable<UsbInterface>
|
||||||
var bulkIn: UsbEndpoint? = null
|
get() = (0 until interfaceCount).map(::getInterface)
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun UsbDevice.getSmartCardInterface(): UsbInterface? {
|
val Iterable<UsbInterface>.smartCard: UsbInterface?
|
||||||
for (i in 0 until interfaceCount) {
|
get() = find { it.interfaceClass == UsbConstants.USB_CLASS_CSCID }
|
||||||
val anInterface = getInterface(i)
|
|
||||||
if (anInterface.interfaceClass == UsbConstants.USB_CLASS_CSCID) {
|
val UsbInterface.endpoints: Iterable<UsbEndpoint>
|
||||||
return anInterface
|
get() = (0 until endpointCount).map(::getEndpoint)
|
||||||
|
|
||||||
|
val Iterable<UsbEndpoint>.bulkPair: Pair<UsbEndpoint?, UsbEndpoint?>
|
||||||
|
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
|
|
||||||
}
|
|
|
@ -23,6 +23,8 @@ 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
|
||||||
|
@ -100,24 +102,22 @@ 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(
|
add(Item(R.string.euicc_info_removable, formatByBoolean(channel.port.card.isRemovable, YES_NO)))
|
||||||
Item(
|
add(Item(R.string.euicc_info_eid, channel.lpa.eID, copiedToastResId = R.string.toast_eid_copied))
|
||||||
R.string.euicc_info_removable,
|
getESTKmeInfo(channel.apduInterface)?.let {
|
||||||
formatByBoolean(channel.port.card.isRemovable, YES_NO)
|
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(
|
add(Item(R.string.euicc_info_fw_ver, it.firmwareVersion))
|
||||||
Item(
|
}
|
||||||
R.string.euicc_info_eid,
|
getSIMLinkVersion(channel.lpa.eID, channel.lpa.euiccInfo2?.euiccFirmwareVersion)?.let {
|
||||||
channel.lpa.eID,
|
add(Item(R.string.euicc_info_sku, "9eSIM $it"))
|
||||||
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))
|
add(Item(R.string.euicc_info_sgp22_version, info?.sgp22Version.toString()))
|
||||||
add(Item(R.string.euicc_info_firmware_version, info?.euiccFirmwareVersion))
|
add(Item(R.string.euicc_info_firmware_version, info?.euiccFirmwareVersion.toString()))
|
||||||
add(Item(R.string.euicc_info_globalplatform_version, info?.globalPlatformVersion))
|
add(Item(R.string.euicc_info_globalplatform_version, info?.globalPlatformVersion.toString()))
|
||||||
add(Item(R.string.euicc_info_pp_version, info?.ppVersion))
|
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_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,13 +134,8 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
||||||
}
|
}
|
||||||
add(Item(R.string.euicc_info_ci_type, getString(resId)))
|
add(Item(R.string.euicc_info_ci_type, getString(resId)))
|
||||||
}
|
}
|
||||||
add(
|
val atr = channel.atr?.encodeHex() ?: getString(R.string.information_unavailable)
|
||||||
Item(
|
add(Item(R.string.euicc_info_atr, atr, copiedToastResId = R.string.toast_atr_copied))
|
||||||
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 =
|
||||||
|
|
|
@ -122,7 +122,7 @@ open class SettingsFragment: PreferenceFragmentCompat() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CheckBoxPreference.bindBooleanFlow(flow: PreferenceFlowWrapper<Boolean>) {
|
protected fun CheckBoxPreference.bindBooleanFlow(flow: PreferenceFlowWrapper<Boolean>) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
flow.collect { isChecked = it }
|
flow.collect { isChecked = it }
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,9 +124,22 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
|
||||||
processLpaString(text.toString())
|
processLpaString(text.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun processLpaString(s: String) {
|
private fun processLpaString(input: String) {
|
||||||
val components = s.split("$")
|
try {
|
||||||
if (components.size < 3 || components[0] != "LPA:1") {
|
val parsed = ActivationCode.fromString(input)
|
||||||
|
state.smdp = parsed.address
|
||||||
|
state.matchingId = parsed.matchingId
|
||||||
|
if (parsed.confirmationCodeRequired) {
|
||||||
|
AlertDialog.Builder(requireContext()).apply {
|
||||||
|
setTitle(R.string.profile_download_required_confirmation_code)
|
||||||
|
setMessage(R.string.profile_download_required_confirmation_code_message)
|
||||||
|
setCancelable(true)
|
||||||
|
setPositiveButton(android.R.string.ok, null)
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gotoNextFragment(DownloadWizardDetailsFragment())
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
AlertDialog.Builder(requireContext()).apply {
|
AlertDialog.Builder(requireContext()).apply {
|
||||||
setTitle(R.string.profile_download_incorrect_lpa_string)
|
setTitle(R.string.profile_download_incorrect_lpa_string)
|
||||||
setMessage(R.string.profile_download_incorrect_lpa_string_message)
|
setMessage(R.string.profile_download_incorrect_lpa_string_message)
|
||||||
|
@ -134,11 +147,7 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
|
||||||
setNegativeButton(android.R.string.cancel, null)
|
setNegativeButton(android.R.string.cancel, null)
|
||||||
show()
|
show()
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
state.smdp = components[1]
|
|
||||||
state.matchingId = components[2]
|
|
||||||
gotoNextFragment(DownloadWizardDetailsFragment())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class DownloadMethodViewHolder(private val root: View) : ViewHolder(root) {
|
private class DownloadMethodViewHolder(private val root: View) : ViewHolder(root) {
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
package im.angry.openeuicc.util
|
||||||
|
|
||||||
|
data class ActivationCode(
|
||||||
|
val address: String,
|
||||||
|
val matchingId: String? = null,
|
||||||
|
val oid: String? = null,
|
||||||
|
val confirmationCodeRequired: Boolean = false,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun fromString(input: String): ActivationCode {
|
||||||
|
val components = input.removePrefix("LPA:").split('$')
|
||||||
|
if (components.size < 2 || components[0] != "1") {
|
||||||
|
throw IllegalArgumentException("Invalid activation code format")
|
||||||
|
}
|
||||||
|
return ActivationCode(
|
||||||
|
address = components[1].trim(),
|
||||||
|
matchingId = components.getOrNull(2)?.trim()?.ifBlank { null },
|
||||||
|
oid = components.getOrNull(3)?.trim()?.ifBlank { null },
|
||||||
|
confirmationCodeRequired = components.getOrNull(4)?.trim() == "1"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,6 +31,7 @@ internal object PreferenceKeys {
|
||||||
|
|
||||||
// ---- Developer Options ----
|
// ---- Developer Options ----
|
||||||
val DEVELOPER_OPTIONS_ENABLED = booleanPreferencesKey("developer_options_enabled")
|
val DEVELOPER_OPTIONS_ENABLED = booleanPreferencesKey("developer_options_enabled")
|
||||||
|
val REMOVABLE_TMAPI = booleanPreferencesKey("removable_tmapi")
|
||||||
val UNFILTERED_PROFILE_LIST = booleanPreferencesKey("unfiltered_profile_list")
|
val UNFILTERED_PROFILE_LIST = booleanPreferencesKey("unfiltered_profile_list")
|
||||||
val IGNORE_TLS_CERTIFICATE = booleanPreferencesKey("ignore_tls_certificate")
|
val IGNORE_TLS_CERTIFICATE = booleanPreferencesKey("ignore_tls_certificate")
|
||||||
}
|
}
|
||||||
|
@ -48,6 +49,7 @@ class PreferenceRepository(private val context: Context) {
|
||||||
|
|
||||||
// ---- Developer Options ----
|
// ---- Developer Options ----
|
||||||
val developerOptionsEnabledFlow = bindFlow(PreferenceKeys.DEVELOPER_OPTIONS_ENABLED, false)
|
val developerOptionsEnabledFlow = bindFlow(PreferenceKeys.DEVELOPER_OPTIONS_ENABLED, false)
|
||||||
|
val removableTMAPIFlow = bindFlow(PreferenceKeys.REMOVABLE_TMAPI, false)
|
||||||
val unfilteredProfileListFlow = bindFlow(PreferenceKeys.UNFILTERED_PROFILE_LIST, false)
|
val unfilteredProfileListFlow = bindFlow(PreferenceKeys.UNFILTERED_PROFILE_LIST, false)
|
||||||
val ignoreTLSCertificateFlow = bindFlow(PreferenceKeys.IGNORE_TLS_CERTIFICATE, false)
|
val ignoreTLSCertificateFlow = bindFlow(PreferenceKeys.IGNORE_TLS_CERTIFICATE, false)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -144,4 +144,6 @@
|
||||||
<string name="profile_class_testing">テスティング</string>
|
<string name="profile_class_testing">テスティング</string>
|
||||||
<string name="profile_class_provisioning">準備中</string>
|
<string name="profile_class_provisioning">準備中</string>
|
||||||
<string name="profile_class_operational">動作中</string>
|
<string name="profile_class_operational">動作中</string>
|
||||||
|
<string name="profile_download_required_confirmation_code">要確認コード</string>
|
||||||
|
<string name="profile_download_required_confirmation_code_message">スキャンされた QR コード、又はクリップボードからペーストされた LPA コードには、確認コードの必要が表示されています。</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -144,4 +144,6 @@
|
||||||
<string name="pref_developer_ignore_tls_certificate">无视 SM-DP+ 的 TLS 证书</string>
|
<string name="pref_developer_ignore_tls_certificate">无视 SM-DP+ 的 TLS 证书</string>
|
||||||
<string name="pref_developer_ignore_tls_certificate_desc">允许 RSP 服务器使用任意证书</string>
|
<string name="pref_developer_ignore_tls_certificate_desc">允许 RSP 服务器使用任意证书</string>
|
||||||
<string name="information_unavailable">无信息</string>
|
<string name="information_unavailable">无信息</string>
|
||||||
|
<string name="profile_download_required_confirmation_code">需要确认码</string>
|
||||||
|
<string name="profile_download_required_confirmation_code_message">您扫描的二维码或粘贴的 LPA 码需要一个额外的确认码</string>
|
||||||
</resources>
|
</resources>
|
147
app-common/src/main/res/values-zh-rTW/strings.xml
Normal file
147
app-common/src/main/res/values-zh-rTW/strings.xml
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
<?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>
|
|
@ -31,6 +31,7 @@
|
||||||
<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>
|
||||||
|
|
||||||
|
@ -57,6 +58,8 @@
|
||||||
<string name="profile_download_low_nvram_title">This download may fail</string>
|
<string name="profile_download_low_nvram_title">This download may fail</string>
|
||||||
<string name="profile_download_low_nvram_message">This download may fail due to low remaining capacity.</string>
|
<string name="profile_download_low_nvram_message">This download may fail due to low remaining capacity.</string>
|
||||||
<string name="profile_download_no_lpa_string">No LPA code found in clipboard</string>
|
<string name="profile_download_no_lpa_string">No LPA code found in clipboard</string>
|
||||||
|
<string name="profile_download_required_confirmation_code">Confirmation Code Required</string>
|
||||||
|
<string name="profile_download_required_confirmation_code_message">Please provide a confirmation code as required by the scanned QR code or LPA code from clipboard.</string>
|
||||||
<string name="profile_download_incorrect_lpa_string">Unable to parse</string>
|
<string name="profile_download_incorrect_lpa_string">Unable to parse</string>
|
||||||
<string name="profile_download_incorrect_lpa_string_message">Could not parse QR code or clipboard content as a LPA code.</string>
|
<string name="profile_download_incorrect_lpa_string_message">Could not parse QR code or clipboard content as a LPA code.</string>
|
||||||
|
|
||||||
|
@ -123,6 +126,10 @@
|
||||||
<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>
|
||||||
|
|
|
@ -52,7 +52,7 @@
|
||||||
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
<PreferenceCategory
|
<im.angry.openeuicc.ui.preference.LongSummaryPreferenceCategory
|
||||||
app:key="pref_developer"
|
app:key="pref_developer"
|
||||||
app:title="@string/pref_developer"
|
app:title="@string/pref_developer"
|
||||||
app:iconSpaceReserved="false">
|
app:iconSpaceReserved="false">
|
||||||
|
@ -69,7 +69,7 @@
|
||||||
app:summary="@string/pref_developer_ignore_tls_certificate_desc"
|
app:summary="@string/pref_developer_ignore_tls_certificate_desc"
|
||||||
app:title="@string/pref_developer_ignore_tls_certificate" />
|
app:title="@string/pref_developer_ignore_tls_certificate" />
|
||||||
|
|
||||||
</PreferenceCategory>
|
</im.angry.openeuicc.ui.preference.LongSummaryPreferenceCategory>
|
||||||
|
|
||||||
<PreferenceCategory
|
<PreferenceCategory
|
||||||
app:key="pref_info"
|
app:key="pref_info"
|
||||||
|
|
32
app-unpriv/src/main/res/values-zh-rTW/strings.xml
Normal file
32
app-unpriv/src/main/res/values-zh-rTW/strings.xml
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
<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>
|
|
@ -5,6 +5,7 @@ import android.util.Log
|
||||||
import im.angry.openeuicc.OpenEuiccApplication
|
import im.angry.openeuicc.OpenEuiccApplication
|
||||||
import im.angry.openeuicc.R
|
import im.angry.openeuicc.R
|
||||||
import im.angry.openeuicc.util.*
|
import im.angry.openeuicc.util.*
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import java.lang.IllegalArgumentException
|
import java.lang.IllegalArgumentException
|
||||||
|
|
||||||
class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFactory(context) {
|
class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFactory(context) {
|
||||||
|
@ -21,7 +22,7 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto
|
||||||
super.tryOpenEuiccChannel(port)?.let { return it }
|
super.tryOpenEuiccChannel(port)?.let { return it }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (port.card.isEuicc) {
|
if (port.card.isEuicc || context.preferenceRepository.removableTMAPIFlow.first()) {
|
||||||
Log.i(
|
Log.i(
|
||||||
DefaultEuiccChannelManager.TAG,
|
DefaultEuiccChannelManager.TAG,
|
||||||
"Trying TelephonyManager for slot ${port.card.physicalSlotIndex} port ${port.portIndex}"
|
"Trying TelephonyManager for slot ${port.card.physicalSlotIndex} port ${port.portIndex}"
|
||||||
|
|
|
@ -18,12 +18,10 @@ class TelephonyManagerApduInterface(
|
||||||
const val TAG = "TelephonyManagerApduInterface"
|
const val TAG = "TelephonyManagerApduInterface"
|
||||||
}
|
}
|
||||||
|
|
||||||
private var lastChannel: Int = -1
|
|
||||||
|
|
||||||
override val valid: Boolean
|
override val valid: Boolean
|
||||||
// TelephonyManager channels will never become truly "invalid",
|
get() = channels.isNotEmpty()
|
||||||
// just that transactions might return errors or nonsense
|
|
||||||
get() = lastChannel != -1
|
private var channels = mutableSetOf<Int>()
|
||||||
|
|
||||||
override fun connect() {
|
override fun connect() {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
|
@ -31,52 +29,39 @@ class TelephonyManagerApduInterface(
|
||||||
|
|
||||||
override fun disconnect() {
|
override fun disconnect() {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
lastChannel = -1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun logicalChannelOpen(aid: ByteArray): Int {
|
override fun logicalChannelOpen(aid: ByteArray): Int {
|
||||||
check(lastChannel == -1) { "Already initialized" }
|
|
||||||
val hex = aid.encodeHex()
|
val hex = aid.encodeHex()
|
||||||
val channel = tm.iccOpenLogicalChannelByPortCompat(port.card.physicalSlotIndex, port.portIndex, hex, 0)
|
val channel = tm.iccOpenLogicalChannelByPortCompat(port.card.physicalSlotIndex, port.portIndex, hex, 0)
|
||||||
if (channel.status != IccOpenLogicalChannelResponse.STATUS_NO_ERROR || channel.channel == IccOpenLogicalChannelResponse.INVALID_CHANNEL) {
|
if (channel.status != IccOpenLogicalChannelResponse.STATUS_NO_ERROR || channel.channel == IccOpenLogicalChannelResponse.INVALID_CHANNEL) {
|
||||||
throw IllegalArgumentException("Cannot open logical channel $hex via TelephonManager on slot ${port.card.physicalSlotIndex} port ${port.portIndex}")
|
throw IllegalArgumentException("Cannot open logical channel $hex via TelephonyManager on slot ${port.card.physicalSlotIndex} port ${port.portIndex}")
|
||||||
}
|
}
|
||||||
lastChannel = channel.channel
|
channels.add(channel.channel)
|
||||||
return lastChannel
|
return channel.channel
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun logicalChannelClose(handle: Int) {
|
override fun logicalChannelClose(handle: Int) {
|
||||||
check(handle == lastChannel) { "Invalid channel handle " }
|
check(channels.contains(handle)) {
|
||||||
|
"Invalid logical channel handle $handle"
|
||||||
|
}
|
||||||
tm.iccCloseLogicalChannelByPortCompat(port.card.physicalSlotIndex, port.portIndex, handle)
|
tm.iccCloseLogicalChannelByPortCompat(port.card.physicalSlotIndex, port.portIndex, handle)
|
||||||
lastChannel = -1
|
channels.remove(handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun transmit(tx: ByteArray): ByteArray {
|
override fun transmit(handle: Int, tx: ByteArray): ByteArray {
|
||||||
check(lastChannel != -1) { "Uninitialized" }
|
check(channels.contains(handle)) {
|
||||||
|
"Invalid logical channel handle $handle"
|
||||||
|
}
|
||||||
if (runBlocking { verboseLoggingFlow.first() }) {
|
if (runBlocking { verboseLoggingFlow.first() }) {
|
||||||
Log.d(TAG, "TelephonyManager APDU: ${tx.encodeHex()}")
|
Log.d(TAG, "TelephonyManager APDU: ${tx.encodeHex()}")
|
||||||
}
|
}
|
||||||
|
val result = tm.iccTransmitApduLogicalChannelByPortCompat(
|
||||||
val cla = tx[0].toUByte().toInt()
|
port.card.physicalSlotIndex, port.portIndex, handle,
|
||||||
val instruction = tx[1].toUByte().toInt()
|
tx,
|
||||||
val p1 = tx[2].toUByte().toInt()
|
)
|
||||||
val p2 = tx[3].toUByte().toInt()
|
if (runBlocking { verboseLoggingFlow.first() })
|
||||||
val p3 = tx[4].toUByte().toInt()
|
Log.d(TAG, "TelephonyManager APDU response: $result")
|
||||||
val p4 = tx.drop(5).toByteArray().encodeHex()
|
return result?.decodeHex() ?: byteArrayOf()
|
||||||
|
|
||||||
return tm.iccTransmitApduLogicalChannelByPortCompat(port.card.physicalSlotIndex, port.portIndex, lastChannel,
|
|
||||||
cla,
|
|
||||||
instruction,
|
|
||||||
p1,
|
|
||||||
p2,
|
|
||||||
p3,
|
|
||||||
p4
|
|
||||||
).also {
|
|
||||||
if (runBlocking { verboseLoggingFlow.first() }) {
|
|
||||||
Log.d(TAG, "TelephonyManager APDU response: $it")
|
|
||||||
}
|
}
|
||||||
}?.decodeHex() ?: byteArrayOf()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,11 +1,17 @@
|
||||||
package im.angry.openeuicc.ui
|
package im.angry.openeuicc.ui
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.preference.CheckBoxPreference
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
|
import im.angry.openeuicc.R
|
||||||
|
import im.angry.openeuicc.util.preferenceRepository
|
||||||
|
|
||||||
class PrivilegedSettingsFragment : SettingsFragment() {
|
class PrivilegedSettingsFragment : SettingsFragment() {
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
super.onCreatePreferences(savedInstanceState, rootKey)
|
super.onCreatePreferences(savedInstanceState, rootKey)
|
||||||
|
addPreferencesFromResource(R.xml.pref_privileged_settings)
|
||||||
|
mergePreferenceOverlay("pref_developer_overlay", "pref_developer")
|
||||||
|
|
||||||
// It's stupid to _disable_ things for privileged, but for now, the per-app locale picker
|
// It's stupid to _disable_ things for privileged, but for now, the per-app locale picker
|
||||||
// is not usable for apps signed with the platform key.
|
// is not usable for apps signed with the platform key.
|
||||||
// ref: <https://android.googlesource.com/platform/packages/apps/Settings/+/refs/tags/android-15.0.0_r6/src/com/android/settings/applications/AppLocaleUtil.java#60>
|
// ref: <https://android.googlesource.com/platform/packages/apps/Settings/+/refs/tags/android-15.0.0_r6/src/com/android/settings/applications/AppLocaleUtil.java#60>
|
||||||
|
@ -13,5 +19,9 @@ class PrivilegedSettingsFragment : SettingsFragment() {
|
||||||
// eventually work for platform-signed apps. Or, at some point we might introduce our own
|
// eventually work for platform-signed apps. Or, at some point we might introduce our own
|
||||||
// locale picker, which hopefully works whether privileged or not.
|
// locale picker, which hopefully works whether privileged or not.
|
||||||
requirePreference<Preference>("pref_advanced_language").isVisible = false
|
requirePreference<Preference>("pref_advanced_language").isVisible = false
|
||||||
|
|
||||||
|
// Force use TelephonyManager API
|
||||||
|
requirePreference<CheckBoxPreference>("pref_developer_tmapi_removable")
|
||||||
|
.bindBooleanFlow(preferenceRepository.removableTMAPIFlow)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -111,15 +111,26 @@ fun TelephonyManager.iccCloseLogicalChannelByPortCompat(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun TelephonyManager.iccTransmitApduLogicalChannelByPortCompat(
|
fun TelephonyManager.iccTransmitApduLogicalChannelByPortCompat(
|
||||||
slotIndex: Int, portIndex: Int, channel: Int,
|
slotIndex: Int,
|
||||||
cla: Int, inst: Int, p1: Int, p2: Int, p3: Int, data: String?
|
portIndex: Int,
|
||||||
): String? =
|
channel: Int,
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
tx: ByteArray
|
||||||
|
): String? {
|
||||||
|
val cla = tx[0].toUByte().toInt()
|
||||||
|
val ins = tx[1].toUByte().toInt()
|
||||||
|
val p1 = tx[2].toUByte().toInt()
|
||||||
|
val p2 = tx[3].toUByte().toInt()
|
||||||
|
val p3 = tx[4].toUByte().toInt()
|
||||||
|
val p4 = tx.drop(5).toByteArray().encodeHex()
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
iccTransmitApduLogicalChannelByPort(
|
iccTransmitApduLogicalChannelByPort(
|
||||||
slotIndex, portIndex, channel, cla, inst, p1, p2, p3, data
|
slotIndex, portIndex, channel,
|
||||||
|
cla, ins, p1, p2, p3, p4
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
iccTransmitApduLogicalChannelBySlot(
|
iccTransmitApduLogicalChannelBySlot(
|
||||||
slotIndex, channel, cla, inst, p1, p2, p3, data
|
slotIndex, channel,
|
||||||
|
cla, ins, p1, p2, p3, p4
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
20
app/src/main/res/values-zh-rTW/strings.xml
Normal file
20
app/src/main/res/values-zh-rTW/strings.xml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<?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>
|
|
@ -22,4 +22,8 @@
|
||||||
<string name="lui_desc">Your device supports eSIMs. To connect to mobile network, download your eSIM issued by a carrier, or insert a physical SIM.</string>
|
<string name="lui_desc">Your device supports eSIMs. To connect to mobile network, download your eSIM issued by a carrier, or insert a physical SIM.</string>
|
||||||
<string name="lui_skip">Skip</string>
|
<string name="lui_skip">Skip</string>
|
||||||
<string name="lui_download">Download eSIM</string>
|
<string name="lui_download">Download eSIM</string>
|
||||||
|
|
||||||
|
<!-- Preference -->
|
||||||
|
<string name="pref_developer_tmapi_removable">Using TMAPI for removable</string>
|
||||||
|
<string name="pref_developer_tmapi_removable_desc">TMAPI (TelephonyManager API) will not be enabled in non-euicc mode and some removable eSIM cards may not work.\nEnable this option to skip detection and force TMAPI to be enabled.</string>
|
||||||
</resources>
|
</resources>
|
12
app/src/main/res/xml/pref_privileged_settings.xml
Normal file
12
app/src/main/res/xml/pref_privileged_settings.xml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
<PreferenceCategory
|
||||||
|
app:isPreferenceVisible="false"
|
||||||
|
app:key="pref_developer_overlay">
|
||||||
|
<CheckBoxPreference
|
||||||
|
app:iconSpaceReserved="false"
|
||||||
|
app:key="pref_developer_tmapi_removable"
|
||||||
|
app:summary="@string/pref_developer_tmapi_removable_desc"
|
||||||
|
app:title="@string/pref_developer_tmapi_removable" />
|
||||||
|
</PreferenceCategory>
|
||||||
|
</PreferenceScreen>
|
|
@ -8,7 +8,7 @@ interface ApduInterface {
|
||||||
fun disconnect()
|
fun disconnect()
|
||||||
fun logicalChannelOpen(aid: ByteArray): Int
|
fun logicalChannelOpen(aid: ByteArray): Int
|
||||||
fun logicalChannelClose(handle: Int)
|
fun logicalChannelClose(handle: Int)
|
||||||
fun transmit(tx: ByteArray): ByteArray
|
fun transmit(handle: Int, tx: ByteArray): ByteArray
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is this APDU connection still valid?
|
* Is this APDU connection still valid?
|
||||||
|
@ -16,4 +16,13 @@ interface ApduInterface {
|
||||||
* callers should further check with the LPA to fully determine the validity of a channel
|
* callers should further check with the LPA to fully determine the validity of a channel
|
||||||
*/
|
*/
|
||||||
val valid: Boolean
|
val valid: Boolean
|
||||||
|
|
||||||
|
fun <T> withLogicalChannel(aid: ByteArray, cb: ((ByteArray) -> ByteArray) -> T): T {
|
||||||
|
val handle = logicalChannelOpen(aid)
|
||||||
|
return try {
|
||||||
|
cb { transmit(handle, it) }
|
||||||
|
} finally {
|
||||||
|
logicalChannelClose(handle)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -2,14 +2,31 @@ 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: String,
|
val sgp22Version: Version,
|
||||||
val profileVersion: String,
|
val profileVersion: Version,
|
||||||
val euiccFirmwareVersion: String,
|
val euiccFirmwareVersion: Version,
|
||||||
val globalPlatformVersion: String,
|
val globalPlatformVersion: Version,
|
||||||
val sasAccreditationNumber: String,
|
val sasAccreditationNumber: String,
|
||||||
val ppVersion: String,
|
val ppVersion: Version,
|
||||||
val freeNvram: Int,
|
val freeNvram: Int,
|
||||||
val freeRam: Int,
|
val freeRam: Int,
|
||||||
val euiccCiPKIdListForSigning: Array<String>,
|
val euiccCiPKIdListForSigning: Set<String>,
|
||||||
val euiccCiPKIdListForVerification: Array<String>,
|
val euiccCiPKIdListForVerification: Set<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"
|
||||||
|
}
|
||||||
|
|
|
@ -5,7 +5,11 @@ internal object LpacJni {
|
||||||
System.loadLibrary("lpac-jni")
|
System.loadLibrary("lpac-jni")
|
||||||
}
|
}
|
||||||
|
|
||||||
external fun createContext(apduInterface: ApduInterface, httpInterface: HttpInterface): Long
|
external fun createContext(
|
||||||
|
isdrAid: ByteArray,
|
||||||
|
apduInterface: ApduInterface,
|
||||||
|
httpInterface: HttpInterface
|
||||||
|
): Long
|
||||||
external fun destroyContext(handle: Long)
|
external fun destroyContext(handle: Long)
|
||||||
|
|
||||||
external fun euiccInit(handle: Long): Int
|
external fun euiccInit(handle: Long): Int
|
||||||
|
|
|
@ -10,8 +10,10 @@ 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,
|
||||||
rawApduInterface: ApduInterface,
|
rawApduInterface: ApduInterface,
|
||||||
rawHttpInterface: HttpInterface
|
rawHttpInterface: HttpInterface
|
||||||
): LocalProfileAssistant {
|
): LocalProfileAssistant {
|
||||||
|
@ -27,9 +29,9 @@ class LocalProfileAssistantImpl(
|
||||||
var lastApduResponse: ByteArray? = null
|
var lastApduResponse: ByteArray? = null
|
||||||
var lastApduException: Exception? = null
|
var lastApduException: Exception? = null
|
||||||
|
|
||||||
override fun transmit(tx: ByteArray): ByteArray =
|
override fun transmit(handle: Int, tx: ByteArray): ByteArray =
|
||||||
try {
|
try {
|
||||||
apduInterface.transmit(tx).also {
|
apduInterface.transmit(handle, tx).also {
|
||||||
lastApduException = null
|
lastApduException = null
|
||||||
lastApduResponse = it
|
lastApduResponse = it
|
||||||
}
|
}
|
||||||
|
@ -76,15 +78,15 @@ class LocalProfileAssistantImpl(
|
||||||
private val httpInterface = HttpInterfaceWrapper(rawHttpInterface)
|
private val httpInterface = HttpInterfaceWrapper(rawHttpInterface)
|
||||||
|
|
||||||
private var finalized = false
|
private var finalized = false
|
||||||
private var contextHandle: Long = LpacJni.createContext(apduInterface, httpInterface)
|
private var contextHandle: Long = LpacJni.createContext(isdrAid, apduInterface, httpInterface)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
if (LpacJni.euiccInit(contextHandle) < 0) {
|
if (LpacJni.euiccInit(contextHandle) < 0) {
|
||||||
throw IllegalArgumentException("Failed to initialize LPA")
|
throw IllegalArgumentException("Failed to initialize LPA")
|
||||||
}
|
}
|
||||||
|
|
||||||
val pkids = euiccInfo2?.euiccCiPKIdListForVerification ?: arrayOf()
|
val pkids = euiccInfo2?.euiccCiPKIdListForVerification ?: setOf()
|
||||||
httpInterface.usePublicKeyIds(pkids)
|
httpInterface.usePublicKeyIds(pkids.toTypedArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setEs10xMss(mss: Byte) {
|
override fun setEs10xMss(mss: Byte) {
|
||||||
|
@ -156,31 +158,29 @@ 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(
|
||||||
LpacJni.euiccInfo2GetSGP22Version(cInfo),
|
Version(LpacJni.euiccInfo2GetSGP22Version(cInfo)),
|
||||||
LpacJni.euiccInfo2GetProfileVersion(cInfo),
|
Version(LpacJni.euiccInfo2GetProfileVersion(cInfo)),
|
||||||
LpacJni.euiccInfo2GetEuiccFirmwareVersion(cInfo),
|
Version(LpacJni.euiccInfo2GetEuiccFirmwareVersion(cInfo)),
|
||||||
LpacJni.euiccInfo2GetGlobalPlatformVersion(cInfo),
|
Version(LpacJni.euiccInfo2GetGlobalPlatformVersion(cInfo)),
|
||||||
LpacJni.euiccInfo2GetSasAcreditationNumber(cInfo),
|
LpacJni.euiccInfo2GetSasAcreditationNumber(cInfo),
|
||||||
LpacJni.euiccInfo2GetPpVersion(cInfo),
|
Version(LpacJni.euiccInfo2GetPpVersion(cInfo)),
|
||||||
LpacJni.euiccInfo2GetFreeNonVolatileMemory(cInfo).toInt(),
|
LpacJni.euiccInfo2GetFreeNonVolatileMemory(cInfo).toInt(),
|
||||||
LpacJni.euiccInfo2GetFreeVolatileMemory(cInfo).toInt(),
|
LpacJni.euiccInfo2GetFreeVolatileMemory(cInfo).toInt(),
|
||||||
euiccCiPKIdListForSigning.toTypedArray(),
|
buildSet {
|
||||||
euiccCiPKIdListForVerification.toTypedArray()
|
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)
|
LpacJni.euiccInfo2Free(cInfo)
|
||||||
|
|
|
@ -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 = arrayOf(
|
val PKID_GSMA_LIVE_CI = setOf(
|
||||||
// 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 = arrayOf(
|
||||||
|
|
||||||
// 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 = arrayOf(
|
val PKID_GSMA_TEST_CI = setOf(
|
||||||
// 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",
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit a5a0516f084936e7e87cf7420fb99283fa3052ef
|
Subproject commit 90f7104847d4bb392b275746da20a55177a67573
|
|
@ -22,7 +22,7 @@ void interface_wrapper_init() {
|
||||||
"([B)I");
|
"([B)I");
|
||||||
method_apdu_logical_channel_close = (*env)->GetMethodID(env, apdu_class, "logicalChannelClose",
|
method_apdu_logical_channel_close = (*env)->GetMethodID(env, apdu_class, "logicalChannelClose",
|
||||||
"(I)V");
|
"(I)V");
|
||||||
method_apdu_transmit = (*env)->GetMethodID(env, apdu_class, "transmit", "([B)[B");
|
method_apdu_transmit = (*env)->GetMethodID(env, apdu_class, "transmit", "(I[B)[B");
|
||||||
|
|
||||||
jclass http_class = (*env)->FindClass(env, "net/typeblog/lpac_jni/HttpInterface");
|
jclass http_class = (*env)->FindClass(env, "net/typeblog/lpac_jni/HttpInterface");
|
||||||
method_http_transmit = (*env)->GetMethodID(env, http_class, "transmit",
|
method_http_transmit = (*env)->GetMethodID(env, http_class, "transmit",
|
||||||
|
@ -53,24 +53,30 @@ apdu_interface_logical_channel_open(struct euicc_ctx *ctx, const uint8_t *aid, u
|
||||||
jint ret = (*env)->CallIntMethod(env, LPAC_JNI_CTX(ctx)->apdu_interface,
|
jint ret = (*env)->CallIntMethod(env, LPAC_JNI_CTX(ctx)->apdu_interface,
|
||||||
method_apdu_logical_channel_open, jbarr);
|
method_apdu_logical_channel_open, jbarr);
|
||||||
LPAC_JNI_EXCEPTION_RETURN;
|
LPAC_JNI_EXCEPTION_RETURN;
|
||||||
|
LPAC_JNI_CTX(ctx)->logical_channel_id = ret;
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void apdu_interface_logical_channel_close(struct euicc_ctx *ctx, uint8_t channel) {
|
static void apdu_interface_logical_channel_close(struct euicc_ctx *ctx,
|
||||||
|
__attribute__((unused)) uint8_t channel) {
|
||||||
LPAC_JNI_SETUP_ENV;
|
LPAC_JNI_SETUP_ENV;
|
||||||
|
jint logical_channel_id = LPAC_JNI_CTX(ctx)->logical_channel_id;
|
||||||
(*env)->CallVoidMethod(env, LPAC_JNI_CTX(ctx)->apdu_interface,
|
(*env)->CallVoidMethod(env, LPAC_JNI_CTX(ctx)->apdu_interface,
|
||||||
method_apdu_logical_channel_close, channel);
|
method_apdu_logical_channel_close, logical_channel_id);
|
||||||
(*env)->ExceptionClear(env);
|
(*env)->ExceptionClear(env);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
apdu_interface_transmit(struct euicc_ctx *ctx, uint8_t **rx, uint32_t *rx_len, const uint8_t *tx,
|
apdu_interface_transmit(struct euicc_ctx *ctx, uint8_t **rx, uint32_t *rx_len, const uint8_t *tx,
|
||||||
uint32_t tx_len) {
|
uint32_t tx_len) {
|
||||||
|
const int logic_channel = LPAC_JNI_CTX(ctx)->logical_channel_id;
|
||||||
LPAC_JNI_SETUP_ENV;
|
LPAC_JNI_SETUP_ENV;
|
||||||
jbyteArray txArr = (*env)->NewByteArray(env, tx_len);
|
jbyteArray txArr = (*env)->NewByteArray(env, tx_len);
|
||||||
(*env)->SetByteArrayRegion(env, txArr, 0, tx_len, (const jbyte *) tx);
|
(*env)->SetByteArrayRegion(env, txArr, 0, tx_len, (const jbyte *) tx);
|
||||||
jbyteArray ret = (jbyteArray) (*env)->CallObjectMethod(env, LPAC_JNI_CTX(ctx)->apdu_interface,
|
jbyteArray ret = (jbyteArray) (*env)->CallObjectMethod(
|
||||||
method_apdu_transmit, txArr);
|
env, LPAC_JNI_CTX(ctx)->apdu_interface,
|
||||||
|
method_apdu_transmit, logic_channel, txArr
|
||||||
|
);
|
||||||
LPAC_JNI_EXCEPTION_RETURN;
|
LPAC_JNI_EXCEPTION_RETURN;
|
||||||
*rx_len = (*env)->GetArrayLength(env, ret);
|
*rx_len = (*env)->GetArrayLength(env, ret);
|
||||||
*rx = calloc(*rx_len, sizeof(uint8_t));
|
*rx = calloc(*rx_len, sizeof(uint8_t));
|
||||||
|
|
|
@ -28,7 +28,7 @@ jint JNI_OnLoad(JavaVM *vm, void *reserved) {
|
||||||
string_constructor = (*env)->GetMethodID(env, string_class, "<init>",
|
string_constructor = (*env)->GetMethodID(env, string_class, "<init>",
|
||||||
"([BLjava/lang/String;)V");
|
"([BLjava/lang/String;)V");
|
||||||
|
|
||||||
const char _unused[1];
|
const jchar _unused[1];
|
||||||
empty_string = (*env)->NewString(env, _unused, 0);
|
empty_string = (*env)->NewString(env, _unused, 0);
|
||||||
empty_string = (*env)->NewGlobalRef(env, empty_string);
|
empty_string = (*env)->NewGlobalRef(env, empty_string);
|
||||||
|
|
||||||
|
@ -37,17 +37,30 @@ jint JNI_OnLoad(JavaVM *vm, void *reserved) {
|
||||||
|
|
||||||
JNIEXPORT jlong JNICALL
|
JNIEXPORT jlong JNICALL
|
||||||
Java_net_typeblog_lpac_1jni_LpacJni_createContext(JNIEnv *env, jobject thiz,
|
Java_net_typeblog_lpac_1jni_LpacJni_createContext(JNIEnv *env, jobject thiz,
|
||||||
|
jbyteArray isdr_aid,
|
||||||
jobject apdu_interface,
|
jobject apdu_interface,
|
||||||
jobject http_interface) {
|
jobject http_interface) {
|
||||||
struct lpac_jni_ctx *jni_ctx = NULL;
|
struct lpac_jni_ctx *jni_ctx = NULL;
|
||||||
struct euicc_ctx *ctx = NULL;
|
struct euicc_ctx *ctx = NULL;
|
||||||
|
jbyte *isdr_java = NULL;
|
||||||
|
uint32_t isdr_len = 0;
|
||||||
|
uint8_t *isdr_c = NULL;
|
||||||
|
|
||||||
ctx = calloc(1, sizeof(struct euicc_ctx));
|
ctx = calloc(1, sizeof(struct euicc_ctx));
|
||||||
jni_ctx = calloc(1, sizeof(struct lpac_jni_ctx));
|
jni_ctx = calloc(1, sizeof(struct lpac_jni_ctx));
|
||||||
|
|
||||||
|
isdr_java = (*env)->GetByteArrayElements(env, isdr_aid, JNI_FALSE);
|
||||||
|
isdr_len = (*env)->GetArrayLength(env, isdr_aid);
|
||||||
|
isdr_c = calloc(isdr_len, sizeof(uint8_t));
|
||||||
|
memcpy(isdr_c, isdr_java, isdr_len);
|
||||||
|
(*env)->ReleaseByteArrayElements(env, isdr_aid, isdr_java, JNI_ABORT);
|
||||||
|
|
||||||
ctx->apdu.interface = &lpac_jni_apdu_interface;
|
ctx->apdu.interface = &lpac_jni_apdu_interface;
|
||||||
ctx->http.interface = &lpac_jni_http_interface;
|
ctx->http.interface = &lpac_jni_http_interface;
|
||||||
jni_ctx->apdu_interface = (*env)->NewGlobalRef(env, apdu_interface);
|
jni_ctx->apdu_interface = (*env)->NewGlobalRef(env, apdu_interface);
|
||||||
jni_ctx->http_interface = (*env)->NewGlobalRef(env, http_interface);
|
jni_ctx->http_interface = (*env)->NewGlobalRef(env, http_interface);
|
||||||
|
ctx->aid = (const uint8_t *) isdr_c;
|
||||||
|
ctx->aid_len = isdr_len;
|
||||||
ctx->userdata = (void *) jni_ctx;
|
ctx->userdata = (void *) jni_ctx;
|
||||||
return (jlong) ctx;
|
return (jlong) ctx;
|
||||||
}
|
}
|
||||||
|
@ -60,6 +73,7 @@ Java_net_typeblog_lpac_1jni_LpacJni_destroyContext(JNIEnv *env, jobject thiz, jl
|
||||||
(*env)->DeleteGlobalRef(env, jni_ctx->apdu_interface);
|
(*env)->DeleteGlobalRef(env, jni_ctx->apdu_interface);
|
||||||
(*env)->DeleteGlobalRef(env, jni_ctx->http_interface);
|
(*env)->DeleteGlobalRef(env, jni_ctx->http_interface);
|
||||||
free(jni_ctx);
|
free(jni_ctx);
|
||||||
|
free((void *) ctx->aid);
|
||||||
free(ctx);
|
free(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ _Static_assert(sizeof(void *) <= sizeof(jlong),
|
||||||
"jlong must be big enough to hold a platform raw pointer");
|
"jlong must be big enough to hold a platform raw pointer");
|
||||||
|
|
||||||
struct lpac_jni_ctx {
|
struct lpac_jni_ctx {
|
||||||
|
jint logical_channel_id;
|
||||||
jobject apdu_interface;
|
jobject apdu_interface;
|
||||||
jobject http_interface;
|
jobject http_interface;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Reference in a new issue