Compare commits
23 commits
unpriv-v1.
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 3926108ea6 | |||
| aeb837b50c | |||
| 297880fa53 | |||
| 0f369b3b12 | |||
| ba45a7eb7f | |||
| 6837aba211 | |||
| eec2a105ff | |||
| d4855f130c | |||
| 2108696646 | |||
| 6210c81201 | |||
| 0a353a3df6 | |||
| 713ffec26d | |||
| c676c27338 | |||
| 81810b22fb | |||
| fdde41329b | |||
| 2cf2d9490a | |||
| b9863e2e54 | |||
| 56fbd34616 | |||
| 419db21b12 | |||
| 9496370135 | |||
| 84f57a4ef7 | |||
| a75478dd31 | |||
| f17e171372 |
59 changed files with 905 additions and 394 deletions
3
.gitmodules
vendored
3
.gitmodules
vendored
|
|
@ -1,3 +1,6 @@
|
|||
[submodule "libs/lpac-jni/src/main/jni/lpac"]
|
||||
path = libs/lpac-jni/src/main/jni/lpac
|
||||
url = https://github.com/estkme-group/lpac.git
|
||||
[submodule "libs/lpac-jni/src/main/jni/cJSON"]
|
||||
path = libs/lpac-jni/src/main/jni/cjson/cjson
|
||||
url = https://github.com/DaveGamble/cJSON
|
||||
|
|
|
|||
34
.idea/codeStyles/Project.xml
generated
34
.idea/codeStyles/Project.xml
generated
|
|
@ -1,5 +1,39 @@
|
|||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<JavaCodeStyleSettings>
|
||||
<option name="IMPORT_LAYOUT_TABLE">
|
||||
<value>
|
||||
<package name="android" withSubpackages="true" static="true" />
|
||||
<package name="androidx" withSubpackages="true" static="true" />
|
||||
<package name="com" withSubpackages="true" static="true" />
|
||||
<package name="junit" withSubpackages="true" static="true" />
|
||||
<package name="net" withSubpackages="true" static="true" />
|
||||
<package name="org" withSubpackages="true" static="true" />
|
||||
<package name="java" withSubpackages="true" static="true" />
|
||||
<package name="javax" withSubpackages="true" static="true" />
|
||||
<package name="" withSubpackages="true" static="true" />
|
||||
<emptyLine />
|
||||
<package name="android" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="androidx" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="com" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="junit" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="net" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="org" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="java" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="javax" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
</value>
|
||||
</option>
|
||||
</JavaCodeStyleSettings>
|
||||
<JetCodeStyleSettings>
|
||||
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
||||
<value>
|
||||
|
|
|
|||
3
.idea/vcs.xml
generated
3
.idea/vcs.xml
generated
|
|
@ -2,6 +2,7 @@
|
|||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/libs/lpac-jni/src/main/jni/cjson/cjson" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/libs/lpac-jni/src/main/jni/lpac" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
</project>
|
||||
|
|
@ -38,7 +38,6 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
|
|||
EuiccChannelImpl(
|
||||
context.getString(R.string.channel_type_omapi),
|
||||
port,
|
||||
intrinsicChannelName = null,
|
||||
OmapiApduInterface(
|
||||
seService!!,
|
||||
port,
|
||||
|
|
@ -67,7 +66,6 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
|
|||
EuiccChannelImpl(
|
||||
context.getString(R.string.channel_type_usb),
|
||||
FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)),
|
||||
intrinsicChannelName = ccidCtx.productName,
|
||||
UsbApduInterface(
|
||||
ccidCtx
|
||||
),
|
||||
|
|
|
|||
|
|
@ -52,7 +52,6 @@ open class DefaultEuiccChannelManager(
|
|||
get() = (0..<tm.activeModemCountCompat).map { FakeUiccCardInfoCompat(it) }
|
||||
|
||||
private suspend inline fun tryOpenChannelWithKnownAids(
|
||||
supportsMultiSE: Boolean,
|
||||
openFn: (ByteArray, EuiccChannel.SecureElementId) -> EuiccChannel?
|
||||
): List<EuiccChannel> {
|
||||
var isdrAidList =
|
||||
|
|
@ -100,10 +99,9 @@ open class DefaultEuiccChannelManager(
|
|||
ret.add(channel)
|
||||
openedAids.add(aid)
|
||||
|
||||
// Don't try opening more than 1 channel unless we support multi SE or
|
||||
// there is a vendor implementation for deciding when we should stop
|
||||
// opening more channels
|
||||
if (!supportsMultiSE || vendorDecider == null) {
|
||||
// Don't try opening more than 1 channel unless there is a vendor
|
||||
// implementation for deciding when we should stop opening more channels
|
||||
if (vendorDecider == null) {
|
||||
break@outer
|
||||
}
|
||||
}
|
||||
|
|
@ -149,9 +147,9 @@ open class DefaultEuiccChannelManager(
|
|||
return null
|
||||
}
|
||||
|
||||
// This function is not responsible for managing USB channels (see the initial check), so supportsMultiSE is true.
|
||||
// This function is not responsible for managing USB channels (see the initial check)
|
||||
val channels =
|
||||
tryOpenChannelWithKnownAids(supportsMultiSE = true) { isdrAid, seId ->
|
||||
tryOpenChannelWithKnownAids { isdrAid, seId ->
|
||||
euiccChannelFactory.tryOpenEuiccChannel(
|
||||
port,
|
||||
isdrAid,
|
||||
|
|
@ -379,8 +377,7 @@ open class DefaultEuiccChannelManager(
|
|||
UsbCcidContext.createFromUsbDevice(context, device, iface) ?: return@forEach
|
||||
|
||||
try {
|
||||
// TODO: We should also support multiple SEs over USB readers (the code here already does, UI doesn't yet)
|
||||
val channels = tryOpenChannelWithKnownAids(supportsMultiSE = false) { isdrAid, seId ->
|
||||
val channels = tryOpenChannelWithKnownAids { isdrAid, seId ->
|
||||
euiccChannelFactory.tryOpenUsbEuiccChannel(ccidCtx, isdrAid, seId)
|
||||
}
|
||||
if (channels.isNotEmpty() && channels[0].valid) {
|
||||
|
|
|
|||
|
|
@ -85,13 +85,6 @@ interface EuiccChannel {
|
|||
*/
|
||||
val atr: ByteArray?
|
||||
|
||||
/**
|
||||
* Intrinsic name of this channel. For device-internal SIM slots,
|
||||
* this should be null; for USB readers, this should be the name of
|
||||
* the reader device.
|
||||
*/
|
||||
val intrinsicChannelName: String?
|
||||
|
||||
/**
|
||||
* The underlying APDU interface for this channel
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import net.typeblog.lpac_jni.impl.LocalProfileAssistantImpl
|
|||
class EuiccChannelImpl(
|
||||
override val type: String,
|
||||
override val port: UiccPortInfoCompat,
|
||||
override val intrinsicChannelName: String?,
|
||||
override val apduInterface: ApduInterface,
|
||||
override val isdrAid: ByteArray,
|
||||
override val seId: EuiccChannel.SecureElementId,
|
||||
|
|
|
|||
|
|
@ -34,8 +34,6 @@ class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel {
|
|||
override val lpa: LocalProfileAssistant by lpaDelegate
|
||||
override val valid: Boolean
|
||||
get() = channel.valid
|
||||
override val intrinsicChannelName: String?
|
||||
get() = channel.intrinsicChannelName
|
||||
override val apduInterface: ApduInterface
|
||||
get() = channel.apduInterface
|
||||
override val atr: ByteArray?
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package im.angry.openeuicc.core
|
||||
|
||||
import net.typeblog.lpac_jni.ProfileDownloadInput
|
||||
import net.typeblog.lpac_jni.EuiccInfo2
|
||||
import net.typeblog.lpac_jni.LocalProfileAssistant
|
||||
import net.typeblog.lpac_jni.LocalProfileInfo
|
||||
|
|
@ -40,13 +41,8 @@ class LocalProfileAssistantWrapper(orig: LocalProfileAssistant) :
|
|||
|
||||
override fun deleteProfile(iccid: String): Boolean = lpa.deleteProfile(iccid)
|
||||
|
||||
override fun downloadProfile(
|
||||
smdp: String,
|
||||
matchingId: String?,
|
||||
imei: String?,
|
||||
confirmationCode: String?,
|
||||
callback: ProfileDownloadCallback
|
||||
) = lpa.downloadProfile(smdp, matchingId, imei, confirmationCode, callback)
|
||||
override fun downloadProfile(input: ProfileDownloadInput, callback: ProfileDownloadCallback) =
|
||||
lpa.downloadProfile(input, callback)
|
||||
|
||||
override fun deleteNotification(seqNumber: Long): Boolean = lpa.deleteNotification(seqNumber)
|
||||
|
||||
|
|
@ -63,4 +59,4 @@ class LocalProfileAssistantWrapper(orig: LocalProfileAssistant) :
|
|||
fun invalidateWrapper() {
|
||||
_inner = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,9 +21,70 @@ class UsbApduInterface(
|
|||
|
||||
private var channels = mutableSetOf<Int>()
|
||||
|
||||
// ATR parser
|
||||
// Specs: ISO/IEC 7816-3:2006 8.2 Answer-to-Reset
|
||||
// See also: https://en.wikipedia.org/wiki/Answer_to_reset
|
||||
class ParsedAtr private constructor(val ts: Byte?, val t0: Byte?, val ta1: Byte?, val tb1: Byte?, val tc1: Byte?, val td1: Byte?, val ta2: Byte?, val tb2: Byte?, val tc2: Byte?, val td2: Byte?) {
|
||||
companion object {
|
||||
fun parse(atr: ByteArray): ParsedAtr {
|
||||
val ts = atr[0]
|
||||
val t0 = atr[1]
|
||||
val tx1 = arrayOf<Byte?>(null, null, null, null)
|
||||
val tx2 = arrayOf<Byte?>(null, null, null, null)
|
||||
var pointer = 2
|
||||
|
||||
for (i in 0..3) {
|
||||
if (t0.toInt() and (0x10 shl i) != 0) {
|
||||
tx1[i] = atr[pointer]
|
||||
pointer++
|
||||
}
|
||||
}
|
||||
|
||||
val td1 = tx1[3] ?: 0
|
||||
|
||||
for (i in 0..3) {
|
||||
if (td1.toInt() and (0x10 shl i) != 0) {
|
||||
tx2[i] = atr[pointer]
|
||||
pointer++
|
||||
}
|
||||
}
|
||||
|
||||
return ParsedAtr(ts=ts, t0=t0, ta1=tx1[0], tb1=tx1[1], tc1=tx1[2], td1=tx1[3],
|
||||
ta2=tx2[0], tb2=tx2[1], tc2=tx2[2], td2=tx2[3],
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun connect() {
|
||||
ccidCtx.connect()
|
||||
|
||||
if (ccidCtx.transceiver.isTpdu) {
|
||||
// Send parameter selection
|
||||
// Specs: USB-CCID 3.2.1 TPDU level of exchange
|
||||
val parsedAtr = ParsedAtr.parse(atr!!)
|
||||
val ta1 = parsedAtr.ta1 ?: 0x11.toByte()
|
||||
val pts1 = ta1 // TODO: Check that reader supports baud rate proposed by the card
|
||||
val pps = byteArrayOf(0xff.toByte(), 0x10.toByte(), pts1, 0x00.toByte())
|
||||
Log.d(TAG, "PTS1=${pts1} PPS: ${pps.encodeHex()}")
|
||||
ccidCtx.transceiver.sendXfrBlock(pps)
|
||||
|
||||
// Send Set Parameters
|
||||
// Specs: USB-CCID 6.1.7 PC_to_RDR_SetParameters
|
||||
|
||||
val param = byteArrayOf(
|
||||
pts1,
|
||||
(if (parsedAtr.ts == 0x3F.toByte()) 0x02 else 0x00),
|
||||
parsedAtr.tc1 ?: 0,
|
||||
parsedAtr.tc2 ?: 0x0A,
|
||||
0x00
|
||||
)
|
||||
|
||||
Log.d(TAG, "Param: ${param.encodeHex()}")
|
||||
|
||||
ccidCtx.transceiver.sendParamBlock(param)
|
||||
}
|
||||
|
||||
// Send Terminal Capabilities
|
||||
// Specs: ETSI TS 102 221 v15.0.0 - 11.1.19 TERMINAL CAPABILITY
|
||||
val terminalCapabilities = buildCmd(
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ class UsbCcidContext private constructor(
|
|||
private val conn: UsbDeviceConnection,
|
||||
private val bulkIn: UsbEndpoint,
|
||||
private val bulkOut: UsbEndpoint,
|
||||
val productName: String,
|
||||
val verboseLoggingFlow: Flow<Boolean>
|
||||
) {
|
||||
companion object {
|
||||
|
|
@ -38,7 +37,6 @@ class UsbCcidContext private constructor(
|
|||
conn,
|
||||
bulkIn,
|
||||
bulkOut,
|
||||
usbDevice.productName ?: "USB",
|
||||
context.preferenceRepository.verboseLoggingFlow
|
||||
)
|
||||
}.getOrNull()
|
||||
|
|
|
|||
|
|
@ -84,6 +84,8 @@ data class UsbCcidDescription(
|
|||
|
||||
private fun hasFeature(feature: Int) = (dwFeatures and feature) != 0
|
||||
|
||||
val isTpdu = hasFeature(0x10000)
|
||||
|
||||
val voltages: List<Voltage>
|
||||
get() {
|
||||
if (hasFeature(FEATURE_AUTOMATIC_VOLTAGE)) return listOf(Voltage.AUTO)
|
||||
|
|
@ -95,4 +97,4 @@ data class UsbCcidDescription(
|
|||
|
||||
val hasT0Protocol: Boolean
|
||||
get() = (dwProtocols and MASK_T0_PROTO) != 0
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -143,6 +143,8 @@ class UsbCcidTransceiver(
|
|||
|
||||
val hasAutomaticPps = usbCcidDescription.hasAutomaticPps
|
||||
|
||||
val isTpdu = usbCcidDescription.isTpdu
|
||||
|
||||
private val inputBuffer = ByteArray(usbBulkIn.maxPacketSize)
|
||||
|
||||
private var currentSequenceNumber: Byte = 0
|
||||
|
|
@ -158,6 +160,46 @@ class UsbCcidTransceiver(
|
|||
}
|
||||
}
|
||||
|
||||
private fun receiveParamBlock(expectedSequenceNumber: Byte): ByteArray {
|
||||
var response: ByteArray?
|
||||
do {
|
||||
response = receiveParamBlockImmediate(expectedSequenceNumber)
|
||||
} while (response!![7] == 0x80.toByte())
|
||||
return response
|
||||
}
|
||||
|
||||
private fun receiveParamBlockImmediate(expectedSequenceNumber: Byte): ByteArray {
|
||||
/*
|
||||
* Some USB CCID devices (notably NitroKey 3) may time-out and need a subsequent poke to
|
||||
* carry on communications. No particular reason why the number 3 was chosen. If we get a
|
||||
* zero-sized reply (or a time-out), we try again. Clamped retries prevent an infinite loop
|
||||
* if things really turn sour.
|
||||
*/
|
||||
var attempts = 3
|
||||
Log.d(TAG, "Receive data block immediate seq=$expectedSequenceNumber")
|
||||
var readBytes: Int
|
||||
do {
|
||||
readBytes = usbConnection.bulkTransfer(
|
||||
usbBulkIn, inputBuffer, inputBuffer.size, DEVICE_COMMUNICATE_TIMEOUT_MILLIS
|
||||
)
|
||||
if (runBlocking { verboseLoggingFlow.first() }) {
|
||||
Log.d(TAG, "Received $readBytes bytes: ${inputBuffer.encodeHex()}")
|
||||
}
|
||||
} while (readBytes <= 0 && attempts-- > 0)
|
||||
if (inputBuffer[0] != 0x82.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]) {
|
||||
append(", sequence number ")
|
||||
append("%d (expected %d)".format(inputBuffer[6], expectedSequenceNumber))
|
||||
}
|
||||
})
|
||||
}
|
||||
return inputBuffer
|
||||
}
|
||||
|
||||
private fun receiveDataBlock(expectedSequenceNumber: Byte): CcidDataBlock {
|
||||
var response: CcidDataBlock?
|
||||
do {
|
||||
|
|
@ -283,6 +325,38 @@ class UsbCcidTransceiver(
|
|||
return ccidDataBlock
|
||||
}
|
||||
|
||||
fun sendParamBlock(
|
||||
payload: ByteArray
|
||||
): ByteArray {
|
||||
val startTime = SystemClock.elapsedRealtime()
|
||||
val l = payload.size
|
||||
val sequenceNumber: Byte = currentSequenceNumber++
|
||||
val headerData = byteArrayOf(
|
||||
0x61.toByte(),
|
||||
l.toByte(),
|
||||
(l shr 8).toByte(),
|
||||
(l shr 16).toByte(),
|
||||
(l shr 24).toByte(),
|
||||
SLOT_NUMBER.toByte(),
|
||||
sequenceNumber,
|
||||
0x00.toByte(),
|
||||
0x00.toByte(),
|
||||
0x00.toByte()
|
||||
)
|
||||
val data: ByteArray = headerData + payload
|
||||
Log.d(TAG, "USB ParamBlock: ${data.encodeHex()}")
|
||||
var sentBytes = 0
|
||||
while (sentBytes < data.size) {
|
||||
val bytesToSend = usbBulkOut.maxPacketSize.coerceAtMost(data.size - sentBytes)
|
||||
sendRaw(data, sentBytes, bytesToSend)
|
||||
sentBytes += bytesToSend
|
||||
}
|
||||
val ccidDataBlock = receiveParamBlock(sequenceNumber)
|
||||
val elapsedTime = SystemClock.elapsedRealtime() - startTime
|
||||
Log.d(TAG, "USB ParamBlock call took ${elapsedTime}ms")
|
||||
return ccidDataBlock
|
||||
}
|
||||
|
||||
fun iccPowerOn(): CcidDataBlock {
|
||||
val startTime = SystemClock.elapsedRealtime()
|
||||
skipAvailableInput()
|
||||
|
|
|
|||
|
|
@ -17,7 +17,10 @@ import im.angry.openeuicc.core.EuiccChannelManager
|
|||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.trySendBlocking
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
|
|
@ -34,10 +37,13 @@ import kotlinx.coroutines.flow.takeWhile
|
|||
import kotlinx.coroutines.flow.transformWhile
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlinx.coroutines.yield
|
||||
import net.typeblog.lpac_jni.ProfileDownloadCallback
|
||||
import net.typeblog.lpac_jni.ProfileDownloadInput
|
||||
import net.typeblog.lpac_jni.ProfileDownloadState
|
||||
|
||||
/**
|
||||
* An Android Service wrapper for EuiccChannelManager.
|
||||
|
|
@ -104,7 +110,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
*/
|
||||
sealed interface ForegroundTaskState {
|
||||
data object Idle : ForegroundTaskState
|
||||
data class InProgress(val progress: Int) : ForegroundTaskState
|
||||
data class InProgress(val progress: Int, val context: Any? = null) : ForegroundTaskState
|
||||
data class Done(val error: Throwable?) : ForegroundTaskState
|
||||
}
|
||||
|
||||
|
|
@ -379,13 +385,14 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
}
|
||||
|
||||
fun launchProfileDownloadTask(
|
||||
slotId: Int,
|
||||
portId: Int,
|
||||
seId: EuiccChannel.SecureElementId,
|
||||
smdp: String,
|
||||
matchingId: String?,
|
||||
confirmationCode: String?,
|
||||
imei: String?
|
||||
slotId: Int, portId: Int, seId: EuiccChannel.SecureElementId,
|
||||
input: ProfileDownloadInput,
|
||||
// Optionally, a Channel to send confirmation signal when metadata preview is received.
|
||||
// When we emit a ForegroundTaskState.InProgress with ProfileDownloadState.ConfirmingMetadata,
|
||||
// the caller can send a true/false value into this channel to either continue immediately or cancel the download.
|
||||
// Note that there is a timeout of 1 minute, after which we default to cancelling.
|
||||
// When absent, the default value is just a buffered channel with 1 true value in it, so effectively no-op.
|
||||
confirmationSignal: Channel<Boolean> = Channel<Boolean>(1).apply { trySendBlocking(true) }
|
||||
): ForegroundTaskSubscriberFlow =
|
||||
launchForegroundTask(
|
||||
getString(R.string.task_profile_download),
|
||||
|
|
@ -394,18 +401,40 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
) {
|
||||
euiccChannelManager.beginTrackedOperation(slotId, portId, seId) {
|
||||
euiccChannelManager.withEuiccChannel(slotId, portId, seId) { channel ->
|
||||
channel.lpa.downloadProfile(
|
||||
smdp,
|
||||
matchingId,
|
||||
imei,
|
||||
confirmationCode,
|
||||
object : ProfileDownloadCallback {
|
||||
override fun onStateUpdate(state: ProfileDownloadCallback.DownloadState) {
|
||||
if (state.progress == 0) return
|
||||
foregroundTaskState.value =
|
||||
ForegroundTaskState.InProgress(state.progress)
|
||||
channel.lpa.downloadProfile(input) { state ->
|
||||
val progress = state.downloadProgress
|
||||
foregroundTaskState.value = ForegroundTaskState.InProgress(
|
||||
progress,
|
||||
state
|
||||
)
|
||||
|
||||
if (state is ProfileDownloadState.ConfirmingDownload) {
|
||||
state.metadata?.let { metadata ->
|
||||
// TODO: Actually do something here and not just logging?
|
||||
Log.i(
|
||||
TAG,
|
||||
"Downloading profile provider=${metadata.providerName} name=${metadata.name}"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// Try to receive a signal for confirmation while blocking this thread
|
||||
// This of course assumes we're NOT on the main thread here. We aren't,
|
||||
// because we don't run download on the main thread; see withEuiccChannel.
|
||||
return@downloadProfile runBlocking {
|
||||
try {
|
||||
// We can't wait indefinitely; just time out after 1 minute.
|
||||
withTimeout(60 * 1000) {
|
||||
confirmationSignal.receive()
|
||||
}
|
||||
} catch (_: TimeoutCancellationException) {
|
||||
// Default to cancelling / aborting here if we didn't receive a confirmation signal
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
preferenceRepository.notificationDownloadFlow.first()
|
||||
|
|
|
|||
|
|
@ -76,13 +76,11 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
intent.getParcelableExtra("seId")
|
||||
} ?: EuiccChannel.SecureElementId.DEFAULT
|
||||
|
||||
val channelTitle = if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
getString(R.string.channel_type_usb)
|
||||
} else {
|
||||
appContainer.customizableTextProvider.formatNonUsbChannelName(logicalSlotId)
|
||||
}
|
||||
|
||||
title = getString(R.string.euicc_info_activity_title, channelTitle)
|
||||
setChannelTitle(
|
||||
if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID)
|
||||
getString(R.string.channel_name_format_usb) else
|
||||
appContainer.customizableTextProvider.formatNonUsbChannelName(logicalSlotId)
|
||||
)
|
||||
|
||||
swipeRefresh.setOnRefreshListener { refresh() }
|
||||
|
||||
|
|
@ -103,6 +101,10 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun setChannelTitle(title: CharSequence) {
|
||||
super.setTitle(getString(R.string.euicc_info_activity_title, title))
|
||||
}
|
||||
|
||||
override fun onInit() {
|
||||
refresh()
|
||||
}
|
||||
|
|
@ -112,13 +114,14 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
|
||||
lifecycleScope.launch {
|
||||
euiccChannelManager.withEuiccChannel(logicalSlotId, seId) { channel ->
|
||||
// When the chip multi-SE, we need to include seId in the title (because we don't have access
|
||||
// to hasMultipleSE in the onCreate() function, we need to do it here).
|
||||
// TODO: Move channel formatting to somewhere centralized and remove this hack. (And also, of course, add support for USB)
|
||||
if (channel.hasMultipleSE && logicalSlotId != EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
if (channel.hasMultipleSE) {
|
||||
withContext(Dispatchers.Main) {
|
||||
title =
|
||||
val title = if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
getString(R.string.channel_name_format_usb_se, seId.id)
|
||||
} else {
|
||||
appContainer.customizableTextProvider.formatNonUsbChannelNameWithSeId(logicalSlotId, seId)
|
||||
}
|
||||
setChannelTitle(title)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -166,7 +169,7 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
// FS.27 v2.0, Security Guidelines for UICC Profiles (Page 25 of 27, 2024-01-30)
|
||||
// https://www.gsma.com/solutions-and-impact/technologies/security/wp-content/uploads/2024/01/FS.27-Security-Guidelines-for-UICC-Credentials-v2.0-FINAL-23-July.pdf#page=25
|
||||
val resId = when {
|
||||
signers.isEmpty() -> R.string.euicc_info_unknown // the case is not mp, but it's is not common
|
||||
signers.isEmpty() -> R.string.euicc_info_unknown // the case is not mp, but it is not common
|
||||
PKID_GSMA_LIVE_CI.any(signers::contains) -> R.string.euicc_info_ci_gsma_live
|
||||
PKID_GSMA_TEST_CI.any(signers::contains) -> R.string.euicc_info_ci_gsma_test
|
||||
else -> R.string.euicc_info_ci_unknown
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import kotlinx.coroutines.flow.stateIn
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.typeblog.lpac_jni.LocalProfileInfo
|
||||
import net.typeblog.lpac_jni.ProfileClass
|
||||
|
||||
open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
||||
EuiccChannelFragmentMarker {
|
||||
|
|
@ -119,7 +120,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false)
|
||||
|
||||
fab.setOnClickListener {
|
||||
val intent = DownloadWizardActivity.newIntent(requireContext(), slotId, seId)
|
||||
val intent = DownloadWizardActivity.newIntent(requireContext(), logicalSlotId, seId)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
|
@ -395,9 +396,9 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
profileClass.isVisible = unfilteredProfileListFlow.value
|
||||
profileClass.setText(
|
||||
when (profile.profileClass) {
|
||||
LocalProfileInfo.Clazz.Testing -> R.string.profile_class_testing
|
||||
LocalProfileInfo.Clazz.Provisioning -> R.string.profile_class_provisioning
|
||||
LocalProfileInfo.Clazz.Operational -> R.string.profile_class_operational
|
||||
ProfileClass.Testing -> R.string.profile_class_testing
|
||||
ProfileClass.Provisioning -> R.string.profile_class_provisioning
|
||||
ProfileClass.Operational -> R.string.profile_class_operational
|
||||
}
|
||||
)
|
||||
iccid.text = profile.iccid
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import androidx.viewpager2.widget.ViewPager2
|
|||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
import im.angry.openeuicc.core.EuiccChannelManager
|
||||
import im.angry.openeuicc.ui.wizard.DownloadWizardActivity
|
||||
import im.angry.openeuicc.util.*
|
||||
|
|
@ -51,18 +52,30 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
private var refreshing = false
|
||||
|
||||
private data class Page(
|
||||
val id: Long,
|
||||
val logicalSlotId: Int,
|
||||
val title: String,
|
||||
val createFragment: () -> Fragment
|
||||
)
|
||||
|
||||
private val pages: MutableList<Page> = mutableListOf()
|
||||
private var nextPageId = 0L
|
||||
|
||||
private fun newPage(
|
||||
logicalSlotId: Int,
|
||||
title: String,
|
||||
createFragment: () -> Fragment
|
||||
): Page = Page(nextPageId++, logicalSlotId, title, createFragment)
|
||||
|
||||
private val pagerAdapter by lazy {
|
||||
object : FragmentStateAdapter(this) {
|
||||
override fun getItemCount() = pages.size
|
||||
|
||||
override fun createFragment(position: Int): Fragment = pages[position].createFragment()
|
||||
|
||||
override fun getItemId(position: Int): Long = pages[position].id
|
||||
|
||||
override fun containsItem(itemId: Long): Boolean = pages.any { it.id == itemId }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -178,7 +191,7 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
} else {
|
||||
appContainer.customizableTextProvider.formatNonUsbChannelName(channel.logicalSlotId)
|
||||
}
|
||||
newPages.add(Page(channel.logicalSlotId, channelName) {
|
||||
newPages.add(newPage(channel.logicalSlotId, channelName) {
|
||||
appContainer.uiComponentFactory.createEuiccManagementFragment(
|
||||
slotId,
|
||||
portId,
|
||||
|
|
@ -192,9 +205,8 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
// If USB readers exist, add them at the very last
|
||||
// We use a wrapper fragment to handle logic specific to USB readers
|
||||
usbDevice?.let {
|
||||
val productName = it.productName ?: getString(R.string.channel_type_usb)
|
||||
newPages.add(Page(EuiccChannelManager.USB_CHANNEL_ID, productName) {
|
||||
UsbCcidReaderFragment()
|
||||
newPages.add(newPage(EuiccChannelManager.USB_CHANNEL_ID, getString(R.string.channel_name_format_usb)) {
|
||||
UsbCcidReaderPermissionFragment()
|
||||
})
|
||||
}
|
||||
viewPager.visibility = View.VISIBLE
|
||||
|
|
@ -202,7 +214,7 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
if (newPages.size > 1) {
|
||||
tabs.visibility = View.VISIBLE
|
||||
} else if (newPages.isEmpty()) {
|
||||
newPages.add(Page(-1, "") {
|
||||
newPages.add(newPage(-1, "") {
|
||||
appContainer.uiComponentFactory.createNoEuiccPlaceholderFragment()
|
||||
})
|
||||
}
|
||||
|
|
@ -260,4 +272,35 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
.build()
|
||||
return listOf(downloadShortcut)
|
||||
}
|
||||
|
||||
fun instantiateUsbTabs(seIds: List<EuiccChannel.SecureElementId>) {
|
||||
val existingUsbPageIndex = pages.indexOfFirst { it.logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID }
|
||||
if (existingUsbPageIndex == -1) return
|
||||
|
||||
val usbPages =
|
||||
seIds.map { seId ->
|
||||
val name = if (seIds.size == 1) {
|
||||
getString(R.string.channel_name_format_usb)
|
||||
} else {
|
||||
getString(R.string.channel_name_format_usb_se, seId.id)
|
||||
}
|
||||
newPage(EuiccChannelManager.USB_CHANNEL_ID, name) {
|
||||
appContainer.uiComponentFactory.createEuiccManagementFragment(
|
||||
EuiccChannelManager.USB_CHANNEL_ID,
|
||||
0,
|
||||
seId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Add before removing to avoid out-of-bounds problems
|
||||
pages.addAll(existingUsbPageIndex, usbPages)
|
||||
// Remove the old USB reader page
|
||||
pages.removeAt(existingUsbPageIndex + usbPages.size)
|
||||
|
||||
if (pages.size > 1) {
|
||||
tabs.visibility = View.VISIBLE
|
||||
}
|
||||
pagerAdapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,15 +72,11 @@ class NotificationsActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker
|
|||
intent.getParcelableExtra("seId")
|
||||
} ?: EuiccChannel.SecureElementId.DEFAULT
|
||||
|
||||
// This is slightly different from the MainActivity logic
|
||||
// due to the length (we don't want to display the full USB product name)
|
||||
val channelTitle = if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
getString(R.string.channel_type_usb)
|
||||
} else {
|
||||
appContainer.customizableTextProvider.formatNonUsbChannelName(logicalSlotId)
|
||||
}
|
||||
|
||||
title = getString(R.string.profile_notifications_detailed_format, channelTitle)
|
||||
setChannelTitle(
|
||||
if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID)
|
||||
getString(R.string.channel_name_format_usb) else
|
||||
appContainer.customizableTextProvider.formatNonUsbChannelName(logicalSlotId)
|
||||
)
|
||||
|
||||
swipeRefresh.setOnRefreshListener {
|
||||
refresh()
|
||||
|
|
@ -116,6 +112,10 @@ class NotificationsActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker
|
|||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun setChannelTitle(title: CharSequence) {
|
||||
super.setTitle(getString(R.string.profile_notifications_detailed_format, title))
|
||||
}
|
||||
|
||||
private fun launchTask(task: suspend () -> Unit) {
|
||||
swipeRefresh.isRefreshing = true
|
||||
|
||||
|
|
@ -133,10 +133,14 @@ class NotificationsActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker
|
|||
private fun refresh() {
|
||||
launchTask {
|
||||
notificationAdapter.notifications = withEuiccChannel { channel ->
|
||||
if (channel.hasMultipleSE && logicalSlotId != EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
if (channel.hasMultipleSE) {
|
||||
withContext(Dispatchers.Main) {
|
||||
title =
|
||||
val channelTitle = if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
getString(R.string.channel_name_format_usb_se, seId.id)
|
||||
} else {
|
||||
appContainer.customizableTextProvider.formatNonUsbChannelNameWithSeId(logicalSlotId, seId)
|
||||
}
|
||||
setChannelTitle(channelTitle)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -47,10 +47,9 @@ open class SettingsFragment : PreferenceFragmentCompat(), OpenEuiccContextMarker
|
|||
|
||||
requirePreference<Preference>("pref_advanced_language").apply {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return@apply
|
||||
isVisible = true
|
||||
intent = Intent(Settings.ACTION_APP_LOCALE_SETTINGS).apply {
|
||||
data = Uri.fromParts("package", requireContext().packageName, null)
|
||||
}
|
||||
val uri = Uri.fromParts("package", requireContext().packageName, null)
|
||||
intent = Intent(Settings.ACTION_APP_LOCALE_SETTINGS, uri)
|
||||
isVisible = intent!!.resolveActivity(requireContext().packageManager) != null
|
||||
}
|
||||
|
||||
requirePreference<Preference>("pref_advanced_logs").apply {
|
||||
|
|
|
|||
|
|
@ -14,24 +14,22 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
import im.angry.openeuicc.core.EuiccChannelManager
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* A wrapper fragment over EuiccManagementFragment where we handle
|
||||
* logic specific to USB devices. This is mainly USB permission
|
||||
* requests, and the fact that USB devices may or may not be
|
||||
* available by the time the user selects it from MainActivity.
|
||||
* A fragment to handle USB reader-specific permission flow. If/after
|
||||
* permission is granted, this fragment simply calls back to MainActivity
|
||||
* to instantiate the corresponding EuiccManagementFragment(s) for the USB
|
||||
* reader.
|
||||
*
|
||||
* Having this fragment allows MainActivity to be (mostly) agnostic
|
||||
* of the underlying implementation of different types of channels.
|
||||
|
|
@ -41,7 +39,7 @@ import kotlinx.coroutines.withContext
|
|||
* Note that for now we assume there will only be one USB card reader
|
||||
* device. This is also an implicit assumption in EuiccChannelManager.
|
||||
*/
|
||||
class UsbCcidReaderFragment : Fragment(), OpenEuiccContextMarker {
|
||||
class UsbCcidReaderPermissionFragment : Fragment(), OpenEuiccContextMarker {
|
||||
companion object {
|
||||
const val ACTION_USB_PERMISSION = "im.angry.openeuicc.USB_PERMISSION"
|
||||
}
|
||||
|
|
@ -70,7 +68,7 @@ class UsbCcidReaderFragment : Fragment(), OpenEuiccContextMarker {
|
|||
|
||||
private lateinit var text: TextView
|
||||
private lateinit var permissionButton: Button
|
||||
private lateinit var loadingProgress: ProgressBar
|
||||
private lateinit var loadingProgress: View
|
||||
|
||||
private var usbDevice: UsbDevice? = null
|
||||
|
||||
|
|
@ -143,27 +141,20 @@ class UsbCcidReaderFragment : Fragment(), OpenEuiccContextMarker {
|
|||
euiccChannelManager.tryOpenUsbEuiccChannel()
|
||||
}
|
||||
|
||||
loadingProgress.visibility = View.GONE
|
||||
|
||||
usbDevice = device
|
||||
|
||||
if (device != null && !canOpen && !usbManager.hasPermission(device)) {
|
||||
loadingProgress.visibility = View.GONE
|
||||
text.text = getString(R.string.usb_permission_needed)
|
||||
text.visibility = View.VISIBLE
|
||||
permissionButton.visibility = View.VISIBLE
|
||||
} else if (device != null && canOpen) {
|
||||
childFragmentManager.commit {
|
||||
replace(
|
||||
R.id.child_container,
|
||||
appContainer.uiComponentFactory.createEuiccManagementFragment(
|
||||
slotId = EuiccChannelManager.USB_CHANNEL_ID,
|
||||
portId = 0,
|
||||
// TODO: What if a USB card has multiple SEs?
|
||||
seId = EuiccChannel.SecureElementId.DEFAULT
|
||||
)
|
||||
)
|
||||
val seIds = withContext(Dispatchers.IO) {
|
||||
euiccChannelManager.flowEuiccSecureElements(EuiccChannelManager.USB_CHANNEL_ID, 0).toList()
|
||||
}
|
||||
(requireActivity() as MainActivity).instantiateUsbTabs(seIds)
|
||||
} else {
|
||||
loadingProgress.visibility = View.GONE
|
||||
text.text = getString(R.string.usb_failed)
|
||||
text.visibility = View.VISIBLE
|
||||
permissionButton.visibility = View.GONE
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package im.angry.openeuicc.ui.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.ViewGroup
|
||||
import com.google.android.material.R
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
|
||||
/**
|
||||
* A TabLayout that automatically switches to MODE_SCROLLABLE when
|
||||
* child tabs overflow the full width of the layout.
|
||||
*/
|
||||
class DynamicModeTabLayout @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = R.attr.tabStyle
|
||||
) : TabLayout(context, attrs, defStyleAttr) {
|
||||
init {
|
||||
addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
|
||||
updateModeIfNecessary()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateModeIfNecessary() {
|
||||
if (width <= 0 || tabCount == 0) return
|
||||
|
||||
val tabStrip = getChildAt(0) as? ViewGroup ?: return
|
||||
val totalTabWidth = (0 until tabStrip.childCount).sumOf { index ->
|
||||
val tabView = tabStrip.getChildAt(index)
|
||||
val layoutParams = tabView.layoutParams as? MarginLayoutParams
|
||||
tabView.measure(
|
||||
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
|
||||
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
|
||||
)
|
||||
tabView.measuredWidth + (layoutParams?.leftMargin ?: 0) + (layoutParams?.rightMargin ?: 0)
|
||||
}
|
||||
|
||||
val availableWidth = width - paddingLeft - paddingRight
|
||||
val shouldScroll = totalTabWidth > availableWidth
|
||||
val targetMode = if (shouldScroll) MODE_SCROLLABLE else MODE_FIXED
|
||||
val targetGravity = if (shouldScroll) GRAVITY_START else GRAVITY_FILL
|
||||
|
||||
if (tabMode != targetMode) {
|
||||
tabMode = targetMode
|
||||
}
|
||||
|
||||
if (tabGravity != targetGravity) {
|
||||
tabGravity = targetGravity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,9 +4,11 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.util.*
|
||||
|
||||
class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepFragment() {
|
||||
private var inputComplete = false
|
||||
|
|
@ -16,17 +18,25 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF
|
|||
override val hasPrev: Boolean
|
||||
get() = true
|
||||
|
||||
private lateinit var smdp: TextInputLayout
|
||||
private lateinit var matchingId: TextInputLayout
|
||||
private lateinit var confirmationCode: TextInputLayout
|
||||
private lateinit var imei: TextInputLayout
|
||||
private val address: EditText by lazy {
|
||||
requireView().requireViewById<TextInputLayout>(R.id.profile_download_server).editText!!
|
||||
}
|
||||
private val matchingId: EditText by lazy {
|
||||
requireView().requireViewById<TextInputLayout>(R.id.profile_download_code).editText!!
|
||||
}
|
||||
private val confirmationCode: EditText by lazy {
|
||||
requireView().requireViewById<TextInputLayout>(R.id.profile_download_confirmation_code).editText!!
|
||||
}
|
||||
private val imei: EditText by lazy {
|
||||
requireView().requireViewById<TextInputLayout>(R.id.profile_download_imei).editText!!
|
||||
}
|
||||
|
||||
private fun saveState() {
|
||||
state.smdp = smdp.editText!!.text.toString().trim()
|
||||
state.smdp = address.text.toString().trim()
|
||||
// Treat empty inputs as null -- this is important for the download step
|
||||
state.matchingId = matchingId.editText!!.text.toString().trim().ifBlank { null }
|
||||
state.confirmationCode = confirmationCode.editText!!.text.toString().trim().ifBlank { null }
|
||||
state.imei = imei.editText!!.text.toString().ifBlank { null }
|
||||
state.matchingId = matchingId.text.toString().trim().ifBlank { null }
|
||||
state.confirmationCode = confirmationCode.text.toString().trim().ifBlank { null }
|
||||
state.imei = imei.text.toString().ifBlank { null }
|
||||
}
|
||||
|
||||
override fun beforeNext() = saveState()
|
||||
|
|
@ -41,40 +51,30 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF
|
|||
DownloadWizardMethodSelectFragment()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = inflater.inflate(R.layout.fragment_download_details, container, false)
|
||||
smdp = view.requireViewById(R.id.profile_download_server)
|
||||
matchingId = view.requireViewById(R.id.profile_download_code)
|
||||
confirmationCode = view.requireViewById(R.id.profile_download_confirmation_code)
|
||||
imei = view.requireViewById(R.id.profile_download_imei)
|
||||
smdp.editText!!.addTextChangedListener {
|
||||
updateInputCompleteness()
|
||||
}
|
||||
confirmationCode.editText!!.addTextChangedListener {
|
||||
updateInputCompleteness()
|
||||
}
|
||||
return view
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
|
||||
inflater.inflate(R.layout.fragment_download_details, container, /* attachToRoot = */ false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
address.addTextChangedListener(onTextChanged = ::handlePasteLPAString)
|
||||
address.addTextChangedListener { updateInputCompleteness() }
|
||||
matchingId.addTextChangedListener(onTextChanged = ::handlePasteLPAString)
|
||||
confirmationCode.addTextChangedListener { updateInputCompleteness() }
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
smdp.editText!!.setText(state.smdp)
|
||||
matchingId.editText!!.setText(state.matchingId)
|
||||
confirmationCode.editText!!.setText(state.confirmationCode)
|
||||
imei.editText!!.setText(state.imei)
|
||||
address.setText(state.smdp)
|
||||
matchingId.setText(state.matchingId)
|
||||
confirmationCode.setText(state.confirmationCode)
|
||||
imei.setText(state.imei)
|
||||
updateInputCompleteness()
|
||||
|
||||
if (state.confirmationCodeRequired) {
|
||||
confirmationCode.editText!!.requestFocus()
|
||||
confirmationCode.editText!!.hint =
|
||||
getString(R.string.profile_download_confirmation_code_required)
|
||||
confirmationCode.requestFocus()
|
||||
confirmationCode.setHint(R.string.profile_download_confirmation_code_required)
|
||||
} else {
|
||||
confirmationCode.editText!!.hint =
|
||||
getString(R.string.profile_download_confirmation_code)
|
||||
confirmationCode.setHint(R.string.profile_download_confirmation_code)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -83,10 +83,19 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF
|
|||
saveState()
|
||||
}
|
||||
|
||||
private fun handlePasteLPAString(text: CharSequence?, start: Int, before: Int, count: Int) {
|
||||
if (start > 0 || before > 0) return // only handle insertions at the beginning
|
||||
if (text == null || !text.startsWith("LPA:", ignoreCase = true)) return
|
||||
val parsed = LPAString.parse(text)
|
||||
address.setText(parsed.address)
|
||||
matchingId.setText(parsed.matchingId)
|
||||
if (parsed.confirmationCodeRequired) confirmationCode.requestFocus()
|
||||
}
|
||||
|
||||
private fun updateInputCompleteness() {
|
||||
inputComplete = isValidAddress(smdp.editText!!.text)
|
||||
inputComplete = isValidAddress(address.text)
|
||||
if (state.confirmationCodeRequired) {
|
||||
inputComplete = inputComplete && confirmationCode.editText!!.text.isNotEmpty()
|
||||
inputComplete = inputComplete && confirmationCode.text.isNotEmpty()
|
||||
}
|
||||
refreshButtons()
|
||||
}
|
||||
|
|
@ -98,11 +107,11 @@ private fun isValidAddress(input: CharSequence): Boolean {
|
|||
var port = 443
|
||||
if (input.contains(':')) {
|
||||
val portIndex = input.lastIndexOf(':')
|
||||
fqdn = input.substring(0, portIndex)
|
||||
fqdn = input.take(portIndex)
|
||||
port = input.substring(portIndex + 1, input.length).toIntOrNull(10) ?: 0
|
||||
}
|
||||
// see https://en.wikipedia.org/wiki/Port_(computer_networking)
|
||||
if (port < 1 || port > 0xffff) return false
|
||||
if (port !in 1..0xffff) return false
|
||||
// see https://en.wikipedia.org/wiki/Fully_qualified_domain_name
|
||||
if (fqdn.isEmpty() || fqdn.length > 255) return false
|
||||
for (part in fqdn.split('.')) {
|
||||
|
|
@ -114,4 +123,4 @@ private fun isValidAddress(input: CharSequence): Boolean {
|
|||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,23 +18,11 @@ import im.angry.openeuicc.util.*
|
|||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import net.typeblog.lpac_jni.ProfileDownloadInput
|
||||
import net.typeblog.lpac_jni.ProfileDownloadState
|
||||
import net.typeblog.lpac_jni.LocalProfileAssistant
|
||||
import net.typeblog.lpac_jni.ProfileDownloadCallback
|
||||
|
||||
class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStepFragment() {
|
||||
companion object {
|
||||
/**
|
||||
* An array of LPA-side state types, mapping 1:1 to progressItems
|
||||
*/
|
||||
val LPA_PROGRESS_STATES = arrayOf(
|
||||
ProfileDownloadCallback.DownloadState.Preparing,
|
||||
ProfileDownloadCallback.DownloadState.Connecting,
|
||||
ProfileDownloadCallback.DownloadState.Authenticating,
|
||||
ProfileDownloadCallback.DownloadState.Downloading,
|
||||
ProfileDownloadCallback.DownloadState.Finalizing,
|
||||
)
|
||||
}
|
||||
|
||||
private enum class ProgressState {
|
||||
NotStarted,
|
||||
InProgress,
|
||||
|
|
@ -138,7 +126,7 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep
|
|||
}
|
||||
|
||||
is EuiccChannelManagerService.ForegroundTaskState.InProgress ->
|
||||
updateProgress(it.progress)
|
||||
updateProgress(it.context as? ProfileDownloadState ?: return@onEach)
|
||||
|
||||
else -> {}
|
||||
}
|
||||
|
|
@ -166,13 +154,8 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep
|
|||
state.downloadStarted = true
|
||||
|
||||
val ret = euiccChannelManagerService.launchProfileDownloadTask(
|
||||
slotId,
|
||||
portId,
|
||||
seId,
|
||||
state.smdp,
|
||||
state.matchingId,
|
||||
state.confirmationCode,
|
||||
state.imei
|
||||
slotId, portId, seId,
|
||||
ProfileDownloadInput(state.smdp, state.matchingId, state.imei, state.confirmationCode)
|
||||
)
|
||||
|
||||
state.downloadTaskID = ret.taskId
|
||||
|
|
@ -180,11 +163,19 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep
|
|||
ret
|
||||
}
|
||||
|
||||
private fun updateProgress(progress: Int) {
|
||||
private fun updateProgress(state: ProfileDownloadState) {
|
||||
val progress = state.downloadProgress
|
||||
showProgressBar(progress)
|
||||
|
||||
val lpaState = ProfileDownloadCallback.lookupStateFromProgress(progress)
|
||||
val stateIndex = LPA_PROGRESS_STATES.indexOf(lpaState)
|
||||
val stateIndex = when (state) {
|
||||
is ProfileDownloadState.Preparing -> 0
|
||||
is ProfileDownloadState.Connecting -> 1
|
||||
is ProfileDownloadState.Authenticating -> 2
|
||||
// TODO: Actually implement metadata confirmation (a dialog or something else)
|
||||
is ProfileDownloadState.ConfirmingDownload -> 2
|
||||
is ProfileDownloadState.Downloading -> 3
|
||||
is ProfileDownloadState.Finalizing -> 4
|
||||
}
|
||||
|
||||
if (stateIndex > 0) {
|
||||
for (i in (0..<stateIndex)) {
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
|||
val freeSpace: Int,
|
||||
val imei: String,
|
||||
val enabledProfileName: String?,
|
||||
val intrinsicChannelName: String?,
|
||||
) {
|
||||
// A synthetic slot ID used to uniquely identify this slot + SE chip in the download process
|
||||
// We assume we don't have anywhere near 2^16 ports...
|
||||
|
|
@ -115,7 +114,6 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
|||
""
|
||||
},
|
||||
channel.lpa.profiles.enabled?.displayName,
|
||||
channel.intrinsicChannelName,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -188,7 +186,11 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
|||
}
|
||||
|
||||
title.text = if (item.logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
item.intrinsicChannelName ?: root.context.getString(R.string.channel_type_usb)
|
||||
if (item.hasMultipleSEs) {
|
||||
root.context.getString(R.string.channel_name_format_usb_se, item.seId.id)
|
||||
} else {
|
||||
root.context.getString(R.string.channel_name_format_usb)
|
||||
}
|
||||
} else if (item.hasMultipleSEs) {
|
||||
appContainer.customizableTextProvider.formatNonUsbChannelNameWithSeId(
|
||||
item.logicalSlotId,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ data class LPAString(
|
|||
val confirmationCodeRequired: Boolean,
|
||||
) {
|
||||
companion object {
|
||||
fun parse(input: String): LPAString {
|
||||
fun parse(input: CharSequence): LPAString {
|
||||
var token = input
|
||||
if (token.startsWith("LPA:", ignoreCase = true)) token = token.drop(4)
|
||||
val components = token.split('$').map { it.trim().ifBlank { null } }
|
||||
|
|
@ -31,4 +31,4 @@ data class LPAString(
|
|||
)
|
||||
return parts.joinToString("$").trimEnd('$')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import im.angry.openeuicc.core.EuiccChannel
|
|||
import im.angry.openeuicc.core.EuiccChannelManager
|
||||
import net.typeblog.lpac_jni.LocalProfileAssistant
|
||||
import net.typeblog.lpac_jni.LocalProfileInfo
|
||||
import net.typeblog.lpac_jni.ProfileClass
|
||||
import net.typeblog.lpac_jni.ProfileDownloadState
|
||||
|
||||
const val TAG = "LPAUtils"
|
||||
|
||||
|
|
@ -16,11 +18,21 @@ val LocalProfileInfo.isEnabled: Boolean
|
|||
get() = state == LocalProfileInfo.State.Enabled
|
||||
|
||||
val List<LocalProfileInfo>.operational: List<LocalProfileInfo>
|
||||
get() = filter { it.profileClass == LocalProfileInfo.Clazz.Operational || it.isEnabled }
|
||||
get() = filter { it.profileClass == ProfileClass.Operational || it.isEnabled }
|
||||
|
||||
val List<LocalProfileInfo>.enabled: LocalProfileInfo?
|
||||
get() = find { it.isEnabled }
|
||||
|
||||
val ProfileDownloadState.downloadProgress: Int
|
||||
get() = when (this) {
|
||||
is ProfileDownloadState.Preparing -> 0
|
||||
is ProfileDownloadState.Connecting -> 20
|
||||
is ProfileDownloadState.Authenticating -> 40
|
||||
is ProfileDownloadState.ConfirmingDownload -> 50
|
||||
is ProfileDownloadState.Downloading -> 60
|
||||
is ProfileDownloadState.Finalizing -> 80
|
||||
}
|
||||
|
||||
val List<EuiccChannel>.hasMultipleChips: Boolean
|
||||
get() = distinctBy { it.slotId }.size > 1
|
||||
|
||||
|
|
@ -104,4 +116,4 @@ suspend inline fun EuiccChannelManager.beginTrackedOperation(
|
|||
}
|
||||
}
|
||||
Log.d(TAG, "Operation complete")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
<include layout="@layout/toolbar_activity" />
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
<im.angry.openeuicc.ui.widget.DynamicModeTabLayout
|
||||
android:id="@+id/main_tabs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
|
|||
|
|
@ -4,15 +4,21 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ProgressBar
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/loading"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
<ProgressBar
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/usb_reader_text"
|
||||
|
|
@ -37,11 +43,4 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/usb_reader_text" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/child_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
|||
|
|
@ -195,4 +195,6 @@
|
|||
<string name="pref_info_website">公式サイト</string>
|
||||
<string name="pref_info_source_code">ソースコード</string>
|
||||
<string name="channel_name_format_se">論理スロット %1$d, SE %2$d</string>
|
||||
<string name="channel_name_format_usb">USB リーダー</string>
|
||||
<string name="channel_name_format_usb_se">USB リーダー, SE %d</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -193,4 +193,6 @@
|
|||
<string name="download_wizard_error_suggest_contact_carrier">请联系您的运营商寻求帮助。</string>
|
||||
<string name="download_wizard_error_suggest_contact_reissue">请联系您的运营商重新签发此 eSIM 配置文件。</string>
|
||||
<string name="channel_name_format_se">逻辑卡槽 %1$d, SE %2$d</string>
|
||||
<string name="channel_name_format_usb">USB 读卡器</string>
|
||||
<string name="channel_name_format_usb_se">USB 读卡器, SE %d</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -189,4 +189,6 @@
|
|||
<string name="download_wizard_error_suggest_contact_carrier">請聯絡您的電信業者尋求協助。</string>
|
||||
<string name="download_wizard_error_suggest_contact_reissue">請聯絡您的電信業者重新簽發此 eSIM 設定檔。</string>
|
||||
<string name="channel_name_format_se">虛擬卡槽 %1$d, SE %2$d</string>
|
||||
<string name="channel_name_format_usb">USB 讀卡機</string>
|
||||
<string name="channel_name_format_usb_se">USB 讀卡機, SE %d</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@
|
|||
<string name="channel_name_format">Logical Slot %d</string>
|
||||
<string name="channel_name_format_se">Logical Slot %1$d, SE %2$d</string>
|
||||
<string name="channel_type_usb" translatable="false">USB</string>
|
||||
<string name="channel_name_format_usb">USB Reader</string>
|
||||
<string name="channel_name_format_usb_se">USB Reader, SE %d</string>
|
||||
<string name="channel_type_omapi" translatable="false">OpenMobile API (OMAPI)</string>
|
||||
|
||||
<!-- Profile -->
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ android {
|
|||
applicationId = "im.angry.easyeuicc"
|
||||
minSdk = 28
|
||||
targetSdk = 35
|
||||
|
||||
emitAssetStatements("https://easyeuicc.org", "https://preview.easyeuicc.org")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
|
@ -46,4 +48,4 @@ android {
|
|||
|
||||
dependencies {
|
||||
implementation(project(":app-common"))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,11 @@
|
|||
android:theme="@style/Theme.OpenEUICC"
|
||||
tools:targetApi="tiramisu">
|
||||
|
||||
<!-- https://web.dev/get-installed-related-apps/?hl=en -->
|
||||
<meta-data
|
||||
android:name="asset_statements"
|
||||
android:resource="@string/asset_statements" />
|
||||
|
||||
<activity
|
||||
android:name="im.angry.openeuicc.ui.UnprivilegedMainActivity"
|
||||
android:exported="true">
|
||||
|
|
|
|||
|
|
@ -173,6 +173,8 @@ open class QuickCompatibilityFragment : Fragment(), UnprivilegedEuiccContextMark
|
|||
appendLine("MODEL: ${Build.MODEL}")
|
||||
appendLine("VERSION.RELEASE: ${Build.VERSION.RELEASE}")
|
||||
appendLine("VERSION.SDK_INT: ${Build.VERSION.SDK_INT}")
|
||||
val carrier = getSystemProperty("ro.carrier")
|
||||
if (carrier != "unknown") appendLine("CARRIER: $carrier")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -181,3 +183,8 @@ private inline val Reader.isSIM: Boolean
|
|||
|
||||
private inline val Reader.slotIndex: Int
|
||||
get() = (name.replace("SIM", "").toIntOrNull() ?: 1) - 1 // 0-based index
|
||||
|
||||
fun getSystemProperty(name: String): String =
|
||||
Runtime.getRuntime().exec(arrayOf("getprop", name))
|
||||
.inputStream.bufferedReader()
|
||||
.use { it.readLine() }.ifEmpty { "unknown" }
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class UnprivilegedEuiccManagementFragment : EuiccManagementFragment() {
|
|||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
menu.findItem(R.id.open_sim_toolkit).apply {
|
||||
intent = stk[slotId]?.intent
|
||||
intent = stk[slotId]
|
||||
isVisible = intent != null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package im.angry.openeuicc.util
|
|||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.provider.Settings
|
||||
|
|
@ -13,84 +12,52 @@ import im.angry.openeuicc.core.EuiccChannelManager
|
|||
|
||||
class SIMToolkit(private val context: Context) {
|
||||
private val slots = buildMap {
|
||||
fun getComponentNames(@ArrayRes id: Int) = context.resources
|
||||
.getStringArray(id).mapNotNull(ComponentName::unflattenFromString).toSet()
|
||||
put(-1, getComponentNames(R.array.sim_toolkit_slot_selection))
|
||||
put(0, getComponentNames(R.array.sim_toolkit_slot_1))
|
||||
put(1, getComponentNames(R.array.sim_toolkit_slot_2))
|
||||
fun getIntents(@ArrayRes id: Int) = context.resources.getStringArray(id)
|
||||
.mapNotNull(ComponentName::unflattenFromString)
|
||||
.map(Intent::makeMainActivity)
|
||||
put(-1, getIntents(R.array.sim_toolkit_slot_selection))
|
||||
put(0, getIntents(R.array.sim_toolkit_slot_1))
|
||||
put(1, getIntents(R.array.sim_toolkit_slot_2))
|
||||
}
|
||||
|
||||
val intents: Iterable<Intent?>
|
||||
get() = listOf(get(0)?.intent, get(1)?.intent)
|
||||
get() = listOf(get(0), get(1))
|
||||
|
||||
operator fun get(slotId: Int): Slot? = when (slotId) {
|
||||
-1, EuiccChannelManager.USB_CHANNEL_ID -> null
|
||||
else -> Slot(context.packageManager, buildSet {
|
||||
addAll(slots.getOrDefault(slotId, emptySet()))
|
||||
addAll(slots.getOrDefault(-1, emptySet()))
|
||||
})
|
||||
operator fun get(slotId: Int): Intent? {
|
||||
if (slotId == -1 || slotId == EuiccChannelManager.USB_CHANNEL_ID) return null
|
||||
val intents = (slots[slotId] ?: emptyList()) + slots[-1]!!
|
||||
val packageNames = intents.mapNotNull(Intent::getPackage).toSet()
|
||||
return getIntent(context.packageManager, intents) // try to find an exported activity first
|
||||
?: getLaunchIntent(context.packageManager, packageNames) // fallback to launch intent
|
||||
?: getDisabledPackageIntent(context.packageManager, packageNames) // app settings if disabled
|
||||
}
|
||||
|
||||
data class Slot(private val packageManager: PackageManager, private val components: Set<ComponentName>) {
|
||||
private val packageNames: Iterable<String>
|
||||
get() = components.map(ComponentName::getPackageName).toSet()
|
||||
.filter(packageManager::isInstalledApp)
|
||||
|
||||
private val launchIntent: Intent?
|
||||
get() = packageNames.firstNotNullOfOrNull(packageManager::getLaunchIntentForPackage)
|
||||
|
||||
private val activities: Iterable<ComponentName>
|
||||
get() = packageNames.flatMap(packageManager::getActivities)
|
||||
.filter(ActivityInfo::exported).map { ComponentName(it.packageName, it.name) }
|
||||
|
||||
private fun getActivityIntent(): Intent? {
|
||||
for (activity in activities) {
|
||||
if (!components.contains(activity)) continue
|
||||
if (isDisabledState(packageManager.getComponentEnabledSetting(activity))) continue
|
||||
return Intent.makeMainActivity(activity)
|
||||
}
|
||||
return launchIntent
|
||||
}
|
||||
|
||||
private fun getDisabledPackageIntent(): Intent? {
|
||||
val disabledPackageName = packageNames
|
||||
.find { isDisabledState(packageManager.getApplicationEnabledSetting(it)) }
|
||||
?: return null
|
||||
val uri = Uri.fromParts("package", disabledPackageName, null)
|
||||
return Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri)
|
||||
}
|
||||
|
||||
val intent: Intent?
|
||||
get() {
|
||||
val intent = getActivityIntent() ?: getDisabledPackageIntent() ?: return null
|
||||
if (intent.resolveActivity(packageManager) == null) return null
|
||||
return intent
|
||||
}
|
||||
}
|
||||
|
||||
fun isSelection(intent: Intent) =
|
||||
slots.getOrDefault(-1, emptySet()).contains(intent.component)
|
||||
fun isSelection(intent: Intent) = intent in slots[-1]!!
|
||||
|
||||
companion object {
|
||||
fun getDisabledPackageName(intent: Intent?): String? {
|
||||
if (intent?.action != Settings.ACTION_APPLICATION_DETAILS_SETTINGS) return null
|
||||
return intent.data?.schemeSpecificPart
|
||||
return intent.data!!.schemeSpecificPart
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isDisabledState(state: Int) = when (state) {
|
||||
PackageManager.COMPONENT_ENABLED_STATE_DISABLED -> true
|
||||
PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER -> true
|
||||
else -> false
|
||||
|
||||
private fun getIntent(packageManager: PackageManager, intents: Iterable<Intent>) =
|
||||
intents.firstOrNull { it.resolveActivityInfo(packageManager, 0)?.exported ?: false }
|
||||
|
||||
private fun getLaunchIntent(packageManager: PackageManager, packageNames: Iterable<String>) =
|
||||
packageNames.firstNotNullOfOrNull(packageManager::getLaunchIntentForPackage)
|
||||
|
||||
private fun getDisabledPackageIntent(packageManager: PackageManager, packageNames: Iterable<String>): Intent? {
|
||||
val packageName = packageNames.firstOrNull(packageManager::isDisabledState) ?: return null
|
||||
val uri = Uri.fromParts("package", packageName, null)
|
||||
return Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri)
|
||||
}
|
||||
|
||||
private fun PackageManager.isInstalledApp(packageName: String) = try {
|
||||
getPackageInfo(packageName, 0)
|
||||
true
|
||||
} catch (_: PackageManager.NameNotFoundException) {
|
||||
false
|
||||
}
|
||||
|
||||
private fun PackageManager.getActivities(packageName: String) =
|
||||
getPackageInfo(packageName, PackageManager.GET_ACTIVITIES).activities?.toList() ?: emptyList()
|
||||
private fun PackageManager.isDisabledState(packageName: String) =
|
||||
when (getApplicationEnabledSetting(packageName)) {
|
||||
PackageManager.COMPONENT_ENABLED_STATE_DISABLED -> true
|
||||
PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER -> true
|
||||
else -> false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="compatibility_check">互換性の確認</string>
|
||||
<string name="open_sim_toolkit">SIM ツールキットを開く</string>
|
||||
<string name="open_sim_toolkit">STK メニューを開く</string>
|
||||
<!-- Settings -->
|
||||
<!-- Toast -->
|
||||
<string name="toast_ara_m_copied">ARA-M SHA-1 をクリップボードにコピーしました</string>
|
||||
|
|
@ -18,4 +18,6 @@
|
|||
<string name="quick_compatibility_button_continue">続行</string>
|
||||
<string name="quick_compatibility_skip">このメッセージを再度表示しない</string>
|
||||
<string name="quick_compatibility_unknown">不明</string>
|
||||
<string name="shortcut_sim_toolkit">STK メニュー</string>
|
||||
<string name="shortcut_sim_toolkit_with_slot">STK メニュー #%d</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
<resources>
|
||||
<string name="compatibility_check">兼容性检查</string>
|
||||
<string name="open_sim_toolkit">打开 SIM 卡应用程序</string>
|
||||
<string name="open_sim_toolkit">打开 STK 菜单</string>
|
||||
<string name="toast_ara_m_copied">ARA-M SHA-1 已拷贝到剪贴板</string>
|
||||
<string name="toast_prompt_to_enable_sim_toolkit">请启用您的“%s”应用程序</string>
|
||||
<string name="toast_prompt_to_enable_sim_toolkit">请启用您的 “%s” 应用程序</string>
|
||||
<string name="quick_compatibility">简易兼容性检测</string>
|
||||
<string name="quick_compatibility_compatible">您的手机可以管理兼容 %s 的卡片</string>
|
||||
<string name="quick_compatibility_not_compatible">您的手机与 %s 不兼容</string>
|
||||
|
|
@ -14,4 +14,6 @@
|
|||
<string name="quick_compatibility_button_continue">继续</string>
|
||||
<string name="quick_compatibility_skip">不再显示此消息</string>
|
||||
<string name="quick_compatibility_unknown">未知</string>
|
||||
</resources>
|
||||
<string name="shortcut_sim_toolkit">STK 菜单</string>
|
||||
<string name="shortcut_sim_toolkit_with_slot">STK 菜单 #%d</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
<resources>
|
||||
<string name="compatibility_check">相容性檢查</string>
|
||||
<string name="open_sim_toolkit">啟動 SIM 卡應用程式</string>
|
||||
<string name="open_sim_toolkit">啟動 STK 選單</string>
|
||||
<string name="toast_ara_m_copied">ARA-M SHA-1 已複製到剪貼簿</string>
|
||||
<string name="toast_prompt_to_enable_sim_toolkit">請啟用您的“%s”應用程式</string>
|
||||
<string name="toast_prompt_to_enable_sim_toolkit">請啟用您的「%s」應用程式</string>
|
||||
<string name="quick_compatibility">簡易相容性檢測</string>
|
||||
<string name="quick_compatibility_compatible">您的手機可以管理相容 %s 的卡片</string>
|
||||
<string name="quick_compatibility_not_compatible">您的手機與 %s 不相容</string>
|
||||
|
|
@ -14,4 +14,6 @@
|
|||
<string name="quick_compatibility_button_continue">繼續</string>
|
||||
<string name="quick_compatibility_skip">不再顯示此訊息</string>
|
||||
<string name="quick_compatibility_unknown">未知</string>
|
||||
</resources>
|
||||
<string name="shortcut_sim_toolkit">STK 選單</string>
|
||||
<string name="shortcut_sim_toolkit_with_slot">STK 選單 #%d</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto
|
|||
return EuiccChannelImpl(
|
||||
context.getString(R.string.channel_type_telephony_manager),
|
||||
port,
|
||||
intrinsicChannelName = null,
|
||||
TelephonyManagerApduInterface(
|
||||
port,
|
||||
telephonyManager,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import kotlinx.coroutines.delay
|
|||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.typeblog.lpac_jni.LocalProfileInfo
|
||||
import net.typeblog.lpac_jni.ProfileClass
|
||||
|
||||
class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
|
||||
companion object {
|
||||
|
|
@ -210,9 +211,9 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
|
|||
)
|
||||
setProfileClass(
|
||||
when (it.profileClass) {
|
||||
LocalProfileInfo.Clazz.Testing -> EuiccProfileInfo.PROFILE_CLASS_TESTING
|
||||
LocalProfileInfo.Clazz.Provisioning -> EuiccProfileInfo.PROFILE_CLASS_PROVISIONING
|
||||
LocalProfileInfo.Clazz.Operational -> EuiccProfileInfo.PROFILE_CLASS_OPERATIONAL
|
||||
ProfileClass.Testing -> EuiccProfileInfo.PROFILE_CLASS_TESTING
|
||||
ProfileClass.Provisioning -> EuiccProfileInfo.PROFILE_CLASS_PROVISIONING
|
||||
ProfileClass.Operational -> EuiccProfileInfo.PROFILE_CLASS_OPERATIONAL
|
||||
}
|
||||
)
|
||||
}.build()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package im.angry.openeuicc.ui
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import androidx.preference.CheckBoxPreference
|
||||
import androidx.preference.Preference
|
||||
|
|
@ -7,6 +8,13 @@ import im.angry.openeuicc.R
|
|||
import im.angry.openeuicc.util.*
|
||||
|
||||
class PrivilegedSettingsFragment : SettingsFragment(), PrivilegedEuiccContextMarker {
|
||||
private val isSignedWithPlatformKey by lazy {
|
||||
val info = with(requireContext()) {
|
||||
packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA)
|
||||
}
|
||||
info.javaClass.getMethod("isSignedWithPlatformKey").invoke(info) as Boolean
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
super.onCreatePreferences(savedInstanceState, rootKey)
|
||||
addPreferencesFromResource(R.xml.pref_privileged_settings)
|
||||
|
|
@ -18,7 +26,9 @@ class PrivilegedSettingsFragment : SettingsFragment(), PrivilegedEuiccContextMar
|
|||
// This is disabled here, not moved to unprivileged, because I have hope that this will
|
||||
// eventually work for platform-signed apps. Or, at some point we might introduce our own
|
||||
// locale picker, which hopefully works whether privileged or not.
|
||||
requirePreference<Preference>("pref_advanced_language").isVisible = false
|
||||
requirePreference<Preference>("pref_advanced_language").apply {
|
||||
isVisible = isVisible && !isSignedWithPlatformKey
|
||||
}
|
||||
|
||||
// Force use TelephonyManager API
|
||||
requirePreference<CheckBoxPreference>("pref_developer_removable_telephony_manager")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
package im.angry.openeuicc.build
|
||||
|
||||
import com.android.build.api.dsl.VariantDimension
|
||||
import groovy.json.JsonOutput
|
||||
import groovy.json.StringEscapeUtils
|
||||
|
||||
/**
|
||||
* https://web.dev/get-installed-related-apps/?hl=en
|
||||
*/
|
||||
fun VariantDimension.emitAssetStatements(vararg sites: String) {
|
||||
val relation = listOf("delegate_permission/common.handle_all_urls")
|
||||
val statements = sites.map {
|
||||
val target = mapOf("namespace" to "web", "site" to it)
|
||||
mapOf("relation" to relation, "target" to target)
|
||||
}
|
||||
val output = JsonOutput.toJson(statements)
|
||||
resValue(type = "string", name = "asset_statements", value = StringEscapeUtils.escapeJava(output))
|
||||
}
|
||||
|
|
@ -20,32 +20,39 @@ val Project.gitVersionCode: Int
|
|||
0
|
||||
}
|
||||
|
||||
val Project.gitVersionName: String
|
||||
get() =
|
||||
try {
|
||||
val stdout = ByteArrayOutputStream()
|
||||
exec {
|
||||
commandLine("git", "describe", "--always", "--tags", "--dirty")
|
||||
standardOutput = stdout
|
||||
}
|
||||
stdout.toString("utf-8").trim('\n')
|
||||
} catch (_: Exception) {
|
||||
"Unknown"
|
||||
fun Project.getGitVersionName(vararg args: String): String =
|
||||
try {
|
||||
val stdout = ByteArrayOutputStream()
|
||||
exec {
|
||||
commandLine("git", "describe", "--always", "--tags", "--dirty", *args)
|
||||
standardOutput = stdout
|
||||
}
|
||||
stdout.toString("utf-8").trim('\n').removePrefix("unpriv-")
|
||||
} catch (_: Exception) {
|
||||
"Unknown"
|
||||
}
|
||||
|
||||
|
||||
class MyVersioningPlugin : Plugin<Project> {
|
||||
override fun apply(target: Project) {
|
||||
target.configure<BaseAppModuleExtension> {
|
||||
defaultConfig {
|
||||
versionCode = target.gitVersionCode
|
||||
versionName = target.gitVersionName.removePrefix("unpriv-")
|
||||
// format: <tag>[-<commits>-g<hash>][-dirty][-suffix]
|
||||
versionName = target.getGitVersionName()
|
||||
}
|
||||
|
||||
applicationVariants.all {
|
||||
if (name == "debug") {
|
||||
val versionCode = (System.currentTimeMillis() / 1000).toInt()
|
||||
// format: <tag>-<commits>-g<hash>[-dirty][-suffix]
|
||||
val versionName = target.getGitVersionName("--long")
|
||||
val versionNameSuffix = mergedFlavor.versionNameSuffix
|
||||
outputs.forEach {
|
||||
(it as ApkVariantOutputImpl).versionCodeOverride =
|
||||
(System.currentTimeMillis() / 1000).toInt()
|
||||
with(it as ApkVariantOutputImpl) {
|
||||
versionCodeOverride = versionCode
|
||||
versionNameOverride = versionName + versionNameSuffix
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,10 +37,7 @@ interface LocalProfileAssistant {
|
|||
fun disableProfile(iccid: String, refresh: Boolean = true): Boolean
|
||||
fun deleteProfile(iccid: String): Boolean
|
||||
|
||||
fun downloadProfile(
|
||||
smdp: String, matchingId: String?, imei: String?,
|
||||
confirmationCode: String?, callback: ProfileDownloadCallback
|
||||
)
|
||||
fun downloadProfile(input: ProfileDownloadInput, callback: ProfileDownloadCallback)
|
||||
|
||||
fun deleteNotification(seqNumber: Long): Boolean
|
||||
fun handleNotification(seqNumber: Long): Boolean
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ data class LocalProfileInfo(
|
|||
val nickName: String,
|
||||
val providerName: String,
|
||||
val isdpAID: String,
|
||||
val profileClass: Clazz
|
||||
val profileClass: ProfileClass
|
||||
) {
|
||||
enum class State {
|
||||
Enabled,
|
||||
|
|
@ -24,20 +24,4 @@ data class LocalProfileInfo(
|
|||
}
|
||||
}
|
||||
|
||||
enum class Clazz {
|
||||
Testing,
|
||||
Provisioning,
|
||||
Operational;
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun fromString(str: String?) =
|
||||
when (str?.lowercase()) {
|
||||
"test" -> Testing
|
||||
"provisioning" -> Provisioning
|
||||
"operational" -> Operational
|
||||
else -> Operational
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,9 +31,9 @@ internal object LpacJni {
|
|||
external fun es10bDeleteNotification(handle: Long, seqNumber: Long): Int
|
||||
|
||||
// es9p + es10b
|
||||
// We do not expose all of the functions because of tediousness :)
|
||||
// We do not expose all the functions because of tediousness :)
|
||||
external fun downloadProfile(
|
||||
handle: Long, smdp: String, matchingId: String?, imei: String?,
|
||||
handle: Long, address: String, matchingId: String?, imei: String?,
|
||||
confirmationCode: String?, callback: ProfileDownloadCallback
|
||||
): Int
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
package net.typeblog.lpac_jni
|
||||
|
||||
enum class ProfileClass {
|
||||
Testing,
|
||||
Provisioning,
|
||||
Operational;
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun fromString(str: String?) =
|
||||
when (str?.lowercase()) {
|
||||
"test" -> Testing
|
||||
"provisioning" -> Provisioning
|
||||
"operational" -> Operational
|
||||
else -> Operational
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +1,5 @@
|
|||
package net.typeblog.lpac_jni
|
||||
|
||||
interface ProfileDownloadCallback {
|
||||
companion object {
|
||||
fun lookupStateFromProgress(progress: Int): DownloadState =
|
||||
when (progress) {
|
||||
0 -> DownloadState.Preparing
|
||||
20 -> DownloadState.Connecting
|
||||
40 -> DownloadState.Authenticating
|
||||
60 -> DownloadState.Downloading
|
||||
80 -> DownloadState.Finalizing
|
||||
else -> throw IllegalArgumentException("Unknown state")
|
||||
}
|
||||
}
|
||||
|
||||
enum class DownloadState(val progress: Int) {
|
||||
Preparing(0),
|
||||
Connecting(20), // Before {server,client} authentication
|
||||
Authenticating(40), // {server,client} authentication
|
||||
Downloading(60), // prepare download, get bpp from es9p
|
||||
Finalizing(80), // load bpp
|
||||
}
|
||||
|
||||
fun onStateUpdate(state: DownloadState)
|
||||
}
|
||||
fun interface ProfileDownloadCallback {
|
||||
fun onStatusUpdate(state: ProfileDownloadState): Boolean
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
package net.typeblog.lpac_jni
|
||||
|
||||
data class ProfileDownloadInput(
|
||||
val address: String,
|
||||
val matchingId: String?,
|
||||
val imei: String?,
|
||||
val confirmationCode: String?
|
||||
)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package net.typeblog.lpac_jni
|
||||
|
||||
sealed class ProfileDownloadState {
|
||||
class Preparing : ProfileDownloadState()
|
||||
class Connecting : ProfileDownloadState()
|
||||
class Authenticating : ProfileDownloadState()
|
||||
class ConfirmingDownload(val metadata: RemoteProfileInfo?) : ProfileDownloadState()
|
||||
class Downloading : ProfileDownloadState()
|
||||
class Finalizing : ProfileDownloadState()
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package net.typeblog.lpac_jni
|
||||
|
||||
// TODO: We need to export profilePolicyRules here as well (currently unsupported by lpac)
|
||||
data class RemoteProfileInfo(
|
||||
val iccid: String,
|
||||
val name: String,
|
||||
val providerName: String,
|
||||
val profileClass: ProfileClass,
|
||||
)
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package net.typeblog.lpac_jni.impl
|
||||
|
||||
import android.util.Log
|
||||
import net.typeblog.lpac_jni.ProfileDownloadInput
|
||||
import net.typeblog.lpac_jni.ApduInterface
|
||||
import net.typeblog.lpac_jni.EuiccInfo2
|
||||
import net.typeblog.lpac_jni.HttpInterface
|
||||
|
|
@ -9,6 +10,7 @@ import net.typeblog.lpac_jni.LocalProfileAssistant
|
|||
import net.typeblog.lpac_jni.LocalProfileInfo
|
||||
import net.typeblog.lpac_jni.LocalProfileNotification
|
||||
import net.typeblog.lpac_jni.LpacJni
|
||||
import net.typeblog.lpac_jni.ProfileClass
|
||||
import net.typeblog.lpac_jni.ProfileDownloadCallback
|
||||
import net.typeblog.lpac_jni.Version
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
|
|
@ -117,7 +119,7 @@ class LocalProfileAssistantImpl(
|
|||
val ret = mutableListOf<LocalProfileInfo>()
|
||||
while (curr != 0L) {
|
||||
val state = LocalProfileInfo.State.fromString(LpacJni.profileGetStateString(curr))
|
||||
val clazz = LocalProfileInfo.Clazz.fromString(LpacJni.profileGetClassString(curr))
|
||||
val clazz = ProfileClass.fromString(LpacJni.profileGetClassString(curr))
|
||||
ret.add(
|
||||
LocalProfileInfo(
|
||||
LpacJni.profileGetIccid(curr),
|
||||
|
|
@ -214,16 +216,13 @@ class LocalProfileAssistantImpl(
|
|||
LpacJni.es10cDeleteProfile(contextHandle, iccid) == 0
|
||||
}
|
||||
|
||||
override fun downloadProfile(
|
||||
smdp: String, matchingId: String?, imei: String?,
|
||||
confirmationCode: String?, callback: ProfileDownloadCallback
|
||||
) = lock.withLock {
|
||||
override fun downloadProfile(input: ProfileDownloadInput, callback: ProfileDownloadCallback) = lock.withLock {
|
||||
val res = LpacJni.downloadProfile(
|
||||
contextHandle,
|
||||
smdp,
|
||||
matchingId,
|
||||
imei,
|
||||
confirmationCode,
|
||||
input.address,
|
||||
input.matchingId,
|
||||
input.imei,
|
||||
input.confirmationCode,
|
||||
callback
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
cc_defaults {
|
||||
name: "lpac-jni-defaults",
|
||||
local_include_dirs: ["lpac"],
|
||||
local_include_dirs: [
|
||||
"cjson",
|
||||
"lpac",
|
||||
"lpac/cjson-ext",
|
||||
],
|
||||
sdk_version: "current",
|
||||
cflags: ["-Wno-error"],
|
||||
}
|
||||
|
|
@ -9,7 +13,8 @@ cc_library_static {
|
|||
name: "liblpac-cjson",
|
||||
defaults: ["lpac-jni-defaults"],
|
||||
srcs: [
|
||||
"lpac/cjson/*.c",
|
||||
"cjson/cjson/*.c",
|
||||
"lpac/cjson-ext/cjson-ext/*.c",
|
||||
],
|
||||
}
|
||||
|
||||
|
|
|
|||
1
libs/lpac-jni/src/main/jni/cjson/cjson
Submodule
1
libs/lpac-jni/src/main/jni/cjson/cjson
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit c859b25da02955fef659d658b8f324b5cde87be3
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit 90f7104847d4bb392b275746da20a55177a67573
|
||||
Subproject commit d214738fa0bdb23faf5833d3d798963079a00468
|
||||
|
|
@ -11,8 +11,11 @@ endef
|
|||
include $(CLEAR_VARS)
|
||||
# libcjson
|
||||
LOCAL_MODULE := lpac-cjson
|
||||
LOCAL_C_INCLUDES := \
|
||||
$(LOCAL_PATH)/cjson
|
||||
LOCAL_SRC_FILES := \
|
||||
$(call all-c-files-under, lpac/cjson)
|
||||
$(call all-c-files-under, cjson/cjson) \
|
||||
$(call all-c-files-under, lpac/cjson-ext/cjson-ext)
|
||||
include $(BUILD_STATIC_LIBRARY)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
|
|
@ -20,7 +23,9 @@ include $(CLEAR_VARS)
|
|||
LOCAL_MODULE := lpac-euicc
|
||||
LOCAL_STATIC_LIBRARIES := lpac-cjson
|
||||
LOCAL_C_INCLUDES := \
|
||||
$(LOCAL_PATH)/lpac
|
||||
$(LOCAL_PATH)/lpac \
|
||||
$(LOCAL_PATH)/lpac/cjson-ext \
|
||||
$(LOCAL_PATH)/cjson
|
||||
LOCAL_SRC_FILES := \
|
||||
$(call all-c-files-under, lpac/euicc)
|
||||
include $(BUILD_STATIC_LIBRARY)
|
||||
|
|
@ -32,4 +37,4 @@ LOCAL_C_INCLUDES := \
|
|||
$(LOCAL_PATH)/lpac
|
||||
LOCAL_SRC_FILES := \
|
||||
$(call all-c-files-under, lpac-jni)
|
||||
include $(BUILD_SHARED_LIBRARY)
|
||||
include $(BUILD_SHARED_LIBRARY)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include <euicc/es9p.h>
|
||||
#include <euicc/es10b.h>
|
||||
#include <euicc/es8p.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <syslog.h>
|
||||
|
|
@ -12,48 +13,148 @@ jobject download_state_downloading;
|
|||
jobject download_state_finalizing;
|
||||
|
||||
jmethodID on_state_update;
|
||||
jclass confirming_download_class;
|
||||
jmethodID confirming_download_constructor;
|
||||
jclass remote_profile_info_class;
|
||||
jmethodID remote_profile_info_constructor;
|
||||
jobject profile_class_testing;
|
||||
jobject profile_class_provisioning;
|
||||
jobject profile_class_operational;
|
||||
|
||||
void lpac_download_init() {
|
||||
LPAC_JNI_SETUP_ENV;
|
||||
|
||||
jclass download_state_class = (*env)->FindClass(env,
|
||||
"net/typeblog/lpac_jni/ProfileDownloadCallback$DownloadState");
|
||||
jfieldID download_state_preparing_field = (*env)->GetStaticFieldID(env, download_state_class,
|
||||
"Preparing",
|
||||
"Lnet/typeblog/lpac_jni/ProfileDownloadCallback$DownloadState;");
|
||||
download_state_preparing = (*env)->GetStaticObjectField(env, download_state_class,
|
||||
download_state_preparing_field);
|
||||
download_state_preparing = (*env)->NewGlobalRef(env, download_state_preparing);
|
||||
jfieldID download_state_connecting_field = (*env)->GetStaticFieldID(env, download_state_class,
|
||||
"Connecting",
|
||||
"Lnet/typeblog/lpac_jni/ProfileDownloadCallback$DownloadState;");
|
||||
download_state_connecting = (*env)->GetStaticObjectField(env, download_state_class,
|
||||
download_state_connecting_field);
|
||||
download_state_connecting = (*env)->NewGlobalRef(env, download_state_connecting);
|
||||
jfieldID download_state_authenticating_field = (*env)->GetStaticFieldID(env,
|
||||
download_state_class,
|
||||
"Authenticating",
|
||||
"Lnet/typeblog/lpac_jni/ProfileDownloadCallback$DownloadState;");
|
||||
download_state_authenticating = (*env)->GetStaticObjectField(env, download_state_class,
|
||||
download_state_authenticating_field);
|
||||
download_state_authenticating = (*env)->NewGlobalRef(env, download_state_authenticating);
|
||||
jfieldID download_state_downloading_field = (*env)->GetStaticFieldID(env, download_state_class,
|
||||
"Downloading",
|
||||
"Lnet/typeblog/lpac_jni/ProfileDownloadCallback$DownloadState;");
|
||||
download_state_downloading = (*env)->GetStaticObjectField(env, download_state_class,
|
||||
download_state_downloading_field);
|
||||
download_state_downloading = (*env)->NewGlobalRef(env, download_state_downloading);
|
||||
jfieldID download_state_finalizng_field = (*env)->GetStaticFieldID(env, download_state_class,
|
||||
"Finalizing",
|
||||
"Lnet/typeblog/lpac_jni/ProfileDownloadCallback$DownloadState;");
|
||||
download_state_finalizing = (*env)->GetStaticObjectField(env, download_state_class,
|
||||
download_state_finalizng_field);
|
||||
download_state_finalizing = (*env)->NewGlobalRef(env, download_state_finalizing);
|
||||
jclass preparing_class = (*env)->FindClass(env,
|
||||
"net/typeblog/lpac_jni/ProfileDownloadState$Preparing");
|
||||
jmethodID preparing_constructor = (*env)->GetMethodID(env, preparing_class,
|
||||
"<init>",
|
||||
"()V");
|
||||
jobject _download_state_preparing = (*env)->NewObject(env, preparing_class,
|
||||
preparing_constructor);
|
||||
download_state_preparing = (*env)->NewGlobalRef(env, _download_state_preparing);
|
||||
(*env)->DeleteLocalRef(env, _download_state_preparing);
|
||||
|
||||
jclass connecting_class = (*env)->FindClass(env,
|
||||
"net/typeblog/lpac_jni/ProfileDownloadState$Connecting");
|
||||
jmethodID connecting_constructor = (*env)->GetMethodID(env, connecting_class,
|
||||
"<init>",
|
||||
"()V");
|
||||
jobject _download_state_connecting = (*env)->NewObject(env, connecting_class,
|
||||
connecting_constructor);
|
||||
download_state_connecting = (*env)->NewGlobalRef(env, _download_state_connecting);
|
||||
(*env)->DeleteLocalRef(env, _download_state_connecting);
|
||||
|
||||
jclass authenticating_class = (*env)->FindClass(env,
|
||||
"net/typeblog/lpac_jni/ProfileDownloadState$Authenticating");
|
||||
jmethodID authenticating_constructor = (*env)->GetMethodID(env, authenticating_class,
|
||||
"<init>",
|
||||
"()V");
|
||||
jobject _download_state_authenticating = (*env)->NewObject(env, authenticating_class,
|
||||
authenticating_constructor);
|
||||
download_state_authenticating = (*env)->NewGlobalRef(env, _download_state_authenticating);
|
||||
(*env)->DeleteLocalRef(env, _download_state_authenticating);
|
||||
|
||||
jclass downloading_class = (*env)->FindClass(env,
|
||||
"net/typeblog/lpac_jni/ProfileDownloadState$Downloading");
|
||||
jmethodID downloading_constructor = (*env)->GetMethodID(env, downloading_class,
|
||||
"<init>",
|
||||
"()V");
|
||||
jobject _download_state_downloading = (*env)->NewObject(env, downloading_class,
|
||||
downloading_constructor);
|
||||
download_state_downloading = (*env)->NewGlobalRef(env, _download_state_downloading);
|
||||
(*env)->DeleteLocalRef(env, _download_state_downloading);
|
||||
|
||||
jclass finalizing_class = (*env)->FindClass(env,
|
||||
"net/typeblog/lpac_jni/ProfileDownloadState$Finalizing");
|
||||
jmethodID finalizing_constructor = (*env)->GetMethodID(env, finalizing_class,
|
||||
"<init>",
|
||||
"()V");
|
||||
jobject _download_state_finalizing = (*env)->NewObject(env, finalizing_class,
|
||||
finalizing_constructor);
|
||||
download_state_finalizing = (*env)->NewGlobalRef(env, _download_state_finalizing);
|
||||
(*env)->DeleteLocalRef(env, _download_state_finalizing);
|
||||
|
||||
jclass download_callback_class = (*env)->FindClass(env,
|
||||
"net/typeblog/lpac_jni/ProfileDownloadCallback");
|
||||
on_state_update = (*env)->GetMethodID(env, download_callback_class, "onStateUpdate",
|
||||
"(Lnet/typeblog/lpac_jni/ProfileDownloadCallback$DownloadState;)V");
|
||||
on_state_update = (*env)->GetMethodID(env, download_callback_class, "onStatusUpdate",
|
||||
"(Lnet/typeblog/lpac_jni/ProfileDownloadState;)Z");
|
||||
|
||||
jclass _confirming_download_class = (*env)->FindClass(env,
|
||||
"net/typeblog/lpac_jni/ProfileDownloadState$ConfirmingDownload");
|
||||
confirming_download_class = (*env)->NewGlobalRef(env, _confirming_download_class);
|
||||
confirming_download_constructor = (*env)->GetMethodID(env,
|
||||
confirming_download_class,
|
||||
"<init>",
|
||||
"(Lnet/typeblog/lpac_jni/RemoteProfileInfo;)V");
|
||||
|
||||
jclass profile_class_class = (*env)->FindClass(env, "net/typeblog/lpac_jni/ProfileClass");
|
||||
jfieldID profile_class_testing_field = (*env)->GetStaticFieldID(env, profile_class_class,
|
||||
"Testing",
|
||||
"Lnet/typeblog/lpac_jni/ProfileClass;");
|
||||
profile_class_testing = (*env)->GetStaticObjectField(env, profile_class_class,
|
||||
profile_class_testing_field);
|
||||
profile_class_testing = (*env)->NewGlobalRef(env, profile_class_testing);
|
||||
jfieldID profile_class_provisioning_field = (*env)->GetStaticFieldID(env, profile_class_class,
|
||||
"Provisioning",
|
||||
"Lnet/typeblog/lpac_jni/ProfileClass;");
|
||||
profile_class_provisioning = (*env)->GetStaticObjectField(env, profile_class_class,
|
||||
profile_class_provisioning_field);
|
||||
profile_class_provisioning = (*env)->NewGlobalRef(env, profile_class_provisioning);
|
||||
jfieldID profile_class_operational_field = (*env)->GetStaticFieldID(env, profile_class_class,
|
||||
"Operational",
|
||||
"Lnet/typeblog/lpac_jni/ProfileClass;");
|
||||
profile_class_operational = (*env)->GetStaticObjectField(env, profile_class_class,
|
||||
profile_class_operational_field);
|
||||
profile_class_operational = (*env)->NewGlobalRef(env, profile_class_operational);
|
||||
|
||||
jclass _remote_profile_info_class = (*env)->FindClass(env,
|
||||
"net/typeblog/lpac_jni/RemoteProfileInfo");
|
||||
remote_profile_info_class = (*env)->NewGlobalRef(env, _remote_profile_info_class);
|
||||
remote_profile_info_constructor = (*env)->GetMethodID(env, remote_profile_info_class, "<init>",
|
||||
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lnet/typeblog/lpac_jni/ProfileClass;)V");
|
||||
}
|
||||
|
||||
static jobject profile_class_from_es10c_profile_class(enum es10c_profile_class profile_class) {
|
||||
switch (profile_class) {
|
||||
case ES10C_PROFILE_CLASS_TEST:
|
||||
return profile_class_testing;
|
||||
case ES10C_PROFILE_CLASS_PROVISIONING:
|
||||
return profile_class_provisioning;
|
||||
case ES10C_PROFILE_CLASS_OPERATIONAL:
|
||||
default:
|
||||
// In es10c profiles are considered operational if the field is missing (null).
|
||||
return profile_class_operational;
|
||||
}
|
||||
}
|
||||
|
||||
static jobject create_remote_profile_info(JNIEnv *env,
|
||||
struct es8p_metadata *profile_metadata) {
|
||||
jobject profile_class = NULL;
|
||||
jstring metadata_iccid = NULL;
|
||||
jstring metadata_profile_name = NULL;
|
||||
jstring metadata_provider_name = NULL;
|
||||
jobject remote_profile_info = NULL;
|
||||
|
||||
metadata_iccid = toJString(env, profile_metadata->iccid);
|
||||
metadata_profile_name = toJString(env, profile_metadata->profileName);
|
||||
metadata_provider_name = toJString(env, profile_metadata->serviceProviderName);
|
||||
profile_class = profile_class_from_es10c_profile_class(profile_metadata->profileClass);
|
||||
|
||||
remote_profile_info = (*env)->NewObject(env, remote_profile_info_class,
|
||||
remote_profile_info_constructor,
|
||||
metadata_iccid,
|
||||
metadata_profile_name,
|
||||
metadata_provider_name,
|
||||
profile_class);
|
||||
|
||||
if (metadata_iccid != NULL)
|
||||
(*env)->DeleteLocalRef(env, metadata_iccid);
|
||||
if (metadata_profile_name != NULL)
|
||||
(*env)->DeleteLocalRef(env, metadata_profile_name);
|
||||
if (metadata_provider_name != NULL)
|
||||
(*env)->DeleteLocalRef(env, metadata_provider_name);
|
||||
|
||||
return remote_profile_info;
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
|
|
@ -62,11 +163,15 @@ Java_net_typeblog_lpac_1jni_LpacJni_downloadProfile(JNIEnv *env, jobject thiz, j
|
|||
jstring imei, jstring confirmation_code,
|
||||
jobject callback) {
|
||||
struct euicc_ctx *ctx = (struct euicc_ctx *) handle;
|
||||
struct es8p_metadata *profile_metadata = NULL;
|
||||
struct es10b_load_bound_profile_package_result es10b_load_bound_profile_package_result;
|
||||
const char *_confirmation_code = NULL;
|
||||
const char *_matching_id = NULL;
|
||||
const char *_smdp = NULL;
|
||||
const char *_imei = NULL;
|
||||
jobject remote_profile_info = NULL;
|
||||
jobject confirming_download_state = NULL;
|
||||
jboolean confirmed = JNI_TRUE;
|
||||
int ret;
|
||||
|
||||
if (confirmation_code != NULL)
|
||||
|
|
@ -79,7 +184,12 @@ Java_net_typeblog_lpac_1jni_LpacJni_downloadProfile(JNIEnv *env, jobject thiz, j
|
|||
|
||||
ctx->http.server_address = _smdp;
|
||||
|
||||
(*env)->CallVoidMethod(env, callback, on_state_update, download_state_preparing);
|
||||
confirmed = (*env)->CallBooleanMethod(env, callback, on_state_update, download_state_preparing);
|
||||
if (!confirmed) {
|
||||
ret = -ES10B_ERROR_REASON_UNDEFINED;
|
||||
goto out;
|
||||
}
|
||||
|
||||
ret = es10b_get_euicc_challenge_and_info(ctx);
|
||||
syslog(LOG_INFO, "es10b_get_euicc_challenge_and_info %d", ret);
|
||||
if (ret < 0) {
|
||||
|
|
@ -87,7 +197,12 @@ Java_net_typeblog_lpac_1jni_LpacJni_downloadProfile(JNIEnv *env, jobject thiz, j
|
|||
goto out;
|
||||
}
|
||||
|
||||
(*env)->CallVoidMethod(env, callback, on_state_update, download_state_connecting);
|
||||
confirmed = (*env)->CallBooleanMethod(env, callback, on_state_update, download_state_connecting);
|
||||
if (!confirmed) {
|
||||
ret = -ES10B_ERROR_REASON_UNDEFINED;
|
||||
goto out;
|
||||
}
|
||||
|
||||
ret = es9p_initiate_authentication(ctx);
|
||||
syslog(LOG_INFO, "es9p_initiate_authentication %d", ret);
|
||||
if (ret < 0) {
|
||||
|
|
@ -95,7 +210,12 @@ Java_net_typeblog_lpac_1jni_LpacJni_downloadProfile(JNIEnv *env, jobject thiz, j
|
|||
goto out;
|
||||
}
|
||||
|
||||
(*env)->CallVoidMethod(env, callback, on_state_update, download_state_authenticating);
|
||||
confirmed = (*env)->CallBooleanMethod(env, callback, on_state_update, download_state_authenticating);
|
||||
if (!confirmed) {
|
||||
ret = -ES10B_ERROR_REASON_UNDEFINED;
|
||||
goto out;
|
||||
}
|
||||
|
||||
ret = es10b_authenticate_server(ctx, _matching_id, _imei);
|
||||
syslog(LOG_INFO, "es10b_authenticate_server %d", ret);
|
||||
if (ret < 0) {
|
||||
|
|
@ -109,7 +229,53 @@ Java_net_typeblog_lpac_1jni_LpacJni_downloadProfile(JNIEnv *env, jobject thiz, j
|
|||
goto out;
|
||||
}
|
||||
|
||||
(*env)->CallVoidMethod(env, callback, on_state_update, download_state_downloading);
|
||||
if (ctx->http._internal.prepare_download_param != NULL &&
|
||||
ctx->http._internal.prepare_download_param->b64_profileMetadata != NULL) {
|
||||
ret = es8p_metadata_parse(&profile_metadata,
|
||||
ctx->http._internal.prepare_download_param->b64_profileMetadata);
|
||||
if (ret < 0) {
|
||||
ret = -ES10B_ERROR_REASON_UNDEFINED;
|
||||
goto out;
|
||||
}
|
||||
|
||||
remote_profile_info = create_remote_profile_info(env, profile_metadata);
|
||||
if (remote_profile_info == NULL) {
|
||||
ret = -ES10B_ERROR_REASON_UNDEFINED;
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
|
||||
confirming_download_state = (*env)->NewObject(env,
|
||||
confirming_download_class,
|
||||
confirming_download_constructor,
|
||||
remote_profile_info);
|
||||
if (confirming_download_state == NULL) {
|
||||
ret = -ES10B_ERROR_REASON_UNDEFINED;
|
||||
goto out;
|
||||
}
|
||||
|
||||
confirmed = (*env)->CallBooleanMethod(env, callback, on_state_update, confirming_download_state);
|
||||
|
||||
if (remote_profile_info != NULL) {
|
||||
(*env)->DeleteLocalRef(env, remote_profile_info);
|
||||
remote_profile_info = NULL;
|
||||
}
|
||||
if (confirming_download_state != NULL) {
|
||||
(*env)->DeleteLocalRef(env, confirming_download_state);
|
||||
confirming_download_state = NULL;
|
||||
}
|
||||
|
||||
if (!confirmed) {
|
||||
ret = -ES10B_ERROR_REASON_UNDEFINED;
|
||||
goto out;
|
||||
}
|
||||
|
||||
confirmed = (*env)->CallBooleanMethod(env, callback, on_state_update, download_state_downloading);
|
||||
if (!confirmed) {
|
||||
ret = -ES10B_ERROR_REASON_UNDEFINED;
|
||||
goto out;
|
||||
}
|
||||
|
||||
ret = es10b_prepare_download(ctx, _confirmation_code);
|
||||
syslog(LOG_INFO, "es10b_prepare_download %d", ret);
|
||||
if (ret < 0) {
|
||||
|
|
@ -121,7 +287,12 @@ Java_net_typeblog_lpac_1jni_LpacJni_downloadProfile(JNIEnv *env, jobject thiz, j
|
|||
if (ret < 0)
|
||||
goto out;
|
||||
|
||||
(*env)->CallVoidMethod(env, callback, on_state_update, download_state_finalizing);
|
||||
confirmed = (*env)->CallBooleanMethod(env, callback, on_state_update, download_state_finalizing);
|
||||
if (!confirmed) {
|
||||
ret = -ES10B_ERROR_REASON_UNDEFINED;
|
||||
goto out;
|
||||
}
|
||||
|
||||
ret = es10b_load_bound_profile_package(ctx, &es10b_load_bound_profile_package_result);
|
||||
syslog(LOG_INFO, "es10b_load_bound_profile_package %d, reason %d", ret, es10b_load_bound_profile_package_result.errorReason);
|
||||
if (ret < 0) {
|
||||
|
|
@ -141,6 +312,12 @@ Java_net_typeblog_lpac_1jni_LpacJni_downloadProfile(JNIEnv *env, jobject thiz, j
|
|||
(*env)->ReleaseStringUTFChars(env, smdp, _smdp);
|
||||
if (_imei != NULL)
|
||||
(*env)->ReleaseStringUTFChars(env, imei, _imei);
|
||||
if (remote_profile_info != NULL)
|
||||
(*env)->DeleteLocalRef(env, remote_profile_info);
|
||||
if (confirming_download_state != NULL)
|
||||
(*env)->DeleteLocalRef(env, confirming_download_state);
|
||||
if (profile_metadata != NULL)
|
||||
es8p_metadata_free(&profile_metadata);
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
|
@ -181,4 +358,4 @@ Java_net_typeblog_lpac_1jni_LpacJni_downloadErrCodeToString(JNIEnv *env, jobject
|
|||
default:
|
||||
return toJString(env, "ES10B_ERROR_REASON_UNDEFINED");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue