Compare commits

..

42 commits

Author SHA1 Message Date
6f8aef8ea8 EuiccChannelManager: Remove last trace of blocking methods 2024-11-10 20:56:56 -05:00
8e806c3ae5 EuiccChannelManager: slot -> logicalSlot 2024-11-10 20:55:12 -05:00
42c870192c Remove last trace of findEuiccChannelBySlotBlocking() public usage 2024-11-10 20:53:35 -05:00
9201ee416e OpenEuiccService: Remove more useless stuff 2024-11-10 20:45:48 -05:00
7105c43ae4 EuiccChannelManager: Remove findEuiccChannelByPort() as public method 2024-11-10 20:42:21 -05:00
d846f0cdc4 LPAUtils: Remove usage of findEuiccChannelByPort() 2024-11-10 20:40:05 -05:00
5dacb75717 EuiccChannelManager: Remove some unused methods 2024-11-10 20:35:01 -05:00
f28867ef2e OpenEuiccService: Remove traces of EuiccChannel usage 2024-11-10 20:31:09 -05:00
7215a2351b OpenEuiccService: Switch onSwitchToSubscriptionWithPort() to use the new service 2024-11-10 20:29:25 -05:00
837c34ba70 Add convience "Done" subscriber method for Flow<ForegroundTaskState> 2024-11-10 17:11:55 -05:00
fe6d4264e3 OpenEuiccService: switch onDeleteSubscription to use
EuiccChannelManagerService
2024-11-10 17:00:41 -05:00
13085ec202 Add findAvailablePorts() to EuiccChannelManager
For use with OpenEuiccService
2024-11-10 15:57:32 -05:00
9d8e58a95d refactor: open sim toolkit logical (#58)
Reviewed-on: PeterCxy/OpenEUICC#58
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2024-11-09 01:10:55 +01:00
22ec3e3baf OpenEuiccService: Start migrating to withEuiccChannel() 2024-11-03 10:53:24 -05:00
32f5e3f71a PrivilegedTelephonyUtils: Nuke direct EuiccChannel usage 2024-11-02 21:36:08 -04:00
04debd62d5 MainActivity: Fixup ViewPager update 2024-11-02 20:58:56 -04:00
0ef435956c EuiccChannelManager: retire enumerateEuiccChannels() 2024-11-02 19:09:57 -04:00
573dce56a6 EuiccChannelManager: Stop emitting real EuiccChannel for USB 2024-11-02 19:06:31 -04:00
272ab953e0 PrivilegedTelephonyUtils: Switch to flowEuiccPorts() 2024-11-02 18:09:12 -04:00
6257a03058 DirectProfileDownloadActivity: use flowEuiccPorts() 2024-11-02 17:16:43 -04:00
5e5210ae2d MainActivity: switch to flowEuiccPorts() 2024-11-02 16:54:30 -04:00
87eb497f40 feat: open stk from menu (#57)
Reviewed-on: PeterCxy/OpenEUICC#57
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2024-11-02 19:29:37 +01:00
1dc5004681 feat: enhanced visual hints for available slots (#54)
Reviewed-on: PeterCxy/OpenEUICC#54
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2024-11-02 19:27:42 +01:00
2ece6af174 Add EuiccChannelManager.flowEuiccPorts()
This is to prepare for refactoring EuiccChannel usage out everywhere
2024-10-29 20:18:23 -04:00
59b4b9e4ab Fix EuiccInfoActivity crash 2024-10-28 21:10:01 -04:00
826c120ca5 EuiccInfoActivity: support back button 2024-10-27 15:58:33 -04:00
5cefbc24f5 OpenEuiccService: fixup 2024-10-27 15:57:46 -04:00
f285eacd55 Show channel access mode and removable status 2024-10-27 15:17:00 -04:00
481b9ce196 Show slot ID in EuiccInfoActivity 2024-10-27 11:24:22 -04:00
ce7fb29c14 Display supported certificates (GSMA Test or Prod) in EuiccInfoActivity 2024-10-27 11:22:10 -04:00
c2cc8ceb2a feat: EuiccInfoActivity 2024-10-27 11:04:45 -04:00
3d4704e77b Remove more EuiccChannel usage in PrivilegedEuiccManagementFragment 2024-10-26 22:15:02 -04:00
6a2d4d66dd Move EuiccChannelManagerService to withEuiccChannel() 2024-10-26 21:52:33 -04:00
8ac46bd778 Move findEuiccChannelBySlot to non-blocking 2024-10-26 21:46:13 -04:00
0961ef70f4 New withEuiccChannel() variant with logical slot ID 2024-10-26 21:45:14 -04:00
3b868e4f9a Move some fragments to withEuiccChannel() 2024-10-26 21:41:56 -04:00
95b24e6151 Add withEuiccChannel helper for EuiccChannelFragment 2024-10-26 21:29:09 -04:00
ef62274057 Wrappers shouldn't hold references indefinitely 2024-10-26 15:49:38 -04:00
76e8fbd56b Use wrappers to enforce that withEuiccChannel can't leak references 2024-10-26 15:46:43 -04:00
d54fcf2589 refactor: Make EuiccChannel abstract
This allows wrapping to control reference lifetime outside of
EuiccChannelManager.
2024-10-26 15:32:31 -04:00
7cb872a664 Add new withEuiccChannel() method to EuiccChannelManager 2024-10-26 15:32:31 -04:00
65c9a7dc39 fix: refer url (#53)
Reviewed-on: PeterCxy/OpenEUICC#53
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2024-10-23 04:17:48 +02:00
44 changed files with 1182 additions and 483 deletions

View file

@ -24,6 +24,10 @@
android:label="@string/profile_download" android:label="@string/profile_download"
android:theme="@style/Theme.AppCompat.Translucent" /> android:theme="@style/Theme.AppCompat.Translucent" />
<activity
android:name="im.angry.openeuicc.ui.EuiccInfoActivity"
android:label="@string/euicc_info" />
<activity <activity
android:name="im.angry.openeuicc.ui.LogsActivity" android:name="im.angry.openeuicc.ui.LogsActivity"
android:label="@string/pref_advanced_logs" /> android:label="@string/pref_advanced_logs" />

View file

@ -6,6 +6,7 @@ import android.hardware.usb.UsbInterface
import android.hardware.usb.UsbManager import android.hardware.usb.UsbManager
import android.se.omapi.SEService import android.se.omapi.SEService
import android.util.Log import android.util.Log
import im.angry.openeuicc.common.R
import im.angry.openeuicc.core.usb.UsbApduInterface import im.angry.openeuicc.core.usb.UsbApduInterface
import im.angry.openeuicc.core.usb.getIoEndpoints import im.angry.openeuicc.core.usb.getIoEndpoints
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
@ -33,7 +34,8 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
Log.i(DefaultEuiccChannelManager.TAG, "Trying OMAPI for physical slot ${port.card.physicalSlotIndex}") Log.i(DefaultEuiccChannelManager.TAG, "Trying OMAPI for physical slot ${port.card.physicalSlotIndex}")
try { try {
return EuiccChannel( return EuiccChannelImpl(
context.getString(R.string.omapi),
port, port,
OmapiApduInterface( OmapiApduInterface(
seService!!, seService!!,
@ -61,7 +63,8 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
if (bulkIn == null || bulkOut == null) return null if (bulkIn == null || bulkOut == null) return null
val conn = usbManager.openDevice(usbDevice) ?: return null val conn = usbManager.openDevice(usbDevice) ?: return null
if (!conn.claimInterface(usbInterface, true)) return null if (!conn.claimInterface(usbInterface, true)) return null
return EuiccChannel( return EuiccChannelImpl(
context.getString(R.string.usb),
FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)), FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)),
UsbApduInterface( UsbApduInterface(
conn, conn,

View file

@ -10,6 +10,9 @@ import im.angry.openeuicc.di.AppContainer
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
@ -88,44 +91,24 @@ open class DefaultEuiccChannelManager(
} }
} }
override fun findEuiccChannelBySlotBlocking(logicalSlotId: Int): EuiccChannel? = protected suspend fun findEuiccChannelByLogicalSlot(logicalSlotId: Int): EuiccChannel? =
runBlocking { withContext(Dispatchers.IO) {
withContext(Dispatchers.IO) { if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { return@withContext usbChannel
return@withContext usbChannel }
}
for (card in uiccCards) { for (card in uiccCards) {
for (port in card.ports) { for (port in card.ports) {
if (port.logicalSlotIndex == logicalSlotId) { if (port.logicalSlotIndex == logicalSlotId) {
return@withContext tryOpenEuiccChannel(port) return@withContext tryOpenEuiccChannel(port)
}
} }
} }
null
} }
null
} }
override fun findEuiccChannelByPhysicalSlotBlocking(physicalSlotId: Int): EuiccChannel? = private suspend fun findAllEuiccChannelsByPhysicalSlot(physicalSlotId: Int): List<EuiccChannel>? {
runBlocking {
withContext(Dispatchers.IO) {
if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
return@withContext usbChannel
}
for (card in uiccCards) {
if (card.physicalSlotIndex != physicalSlotId) continue
for (port in card.ports) {
tryOpenEuiccChannel(port)?.let { return@withContext it }
}
}
null
}
}
override suspend fun findAllEuiccChannelsByPhysicalSlot(physicalSlotId: Int): List<EuiccChannel>? {
if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
return usbChannel?.let { listOf(it) } return usbChannel?.let { listOf(it) }
} }
@ -138,12 +121,7 @@ open class DefaultEuiccChannelManager(
return null return null
} }
override fun findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId: Int): List<EuiccChannel>? = private suspend fun findEuiccChannelByPort(physicalSlotId: Int, portId: Int): EuiccChannel? =
runBlocking {
findAllEuiccChannelsByPhysicalSlot(physicalSlotId)
}
override suspend fun findEuiccChannelByPort(physicalSlotId: Int, portId: Int): EuiccChannel? =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
return@withContext usbChannel return@withContext usbChannel
@ -154,11 +132,57 @@ open class DefaultEuiccChannelManager(
} }
} }
override fun findEuiccChannelByPortBlocking(physicalSlotId: Int, portId: Int): EuiccChannel? = override suspend fun findFirstAvailablePort(physicalSlotId: Int): Int =
runBlocking { withContext(Dispatchers.IO) {
findEuiccChannelByPort(physicalSlotId, portId) if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
return@withContext 0
}
findAllEuiccChannelsByPhysicalSlot(physicalSlotId)?.getOrNull(0)?.portId ?: -1
} }
override suspend fun findAvailablePorts(physicalSlotId: Int): List<Int> =
withContext(Dispatchers.IO) {
if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
return@withContext listOf(0)
}
findAllEuiccChannelsByPhysicalSlot(physicalSlotId)?.map { it.portId } ?: listOf()
}
override suspend fun <R> withEuiccChannel(
physicalSlotId: Int,
portId: Int,
fn: suspend (EuiccChannel) -> R
): R {
val channel = findEuiccChannelByPort(physicalSlotId, portId)
?: throw EuiccChannelManager.EuiccChannelNotFoundException()
val wrapper = EuiccChannelWrapper(channel)
try {
return withContext(Dispatchers.IO) {
fn(wrapper)
}
} finally {
wrapper.invalidateWrapper()
}
}
override suspend fun <R> withEuiccChannel(
logicalSlotId: Int,
fn: suspend (EuiccChannel) -> R
): R {
val channel = findEuiccChannelByLogicalSlot(logicalSlotId)
?: throw EuiccChannelManager.EuiccChannelNotFoundException()
val wrapper = EuiccChannelWrapper(channel)
try {
return withContext(Dispatchers.IO) {
fn(wrapper)
}
} finally {
wrapper.invalidateWrapper()
}
}
override suspend fun waitForReconnect(physicalSlotId: Int, portId: Int, timeoutMillis: Long) { override suspend fun waitForReconnect(physicalSlotId: Int, portId: Int, timeoutMillis: Long) {
if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) return if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) return
@ -184,34 +208,35 @@ open class DefaultEuiccChannelManager(
} }
} }
override suspend fun enumerateEuiccChannels(): List<EuiccChannel> = override fun flowEuiccPorts(): Flow<Pair<Int, Int>> = flow {
withContext(Dispatchers.IO) { uiccCards.forEach { info ->
uiccCards.flatMap { info -> info.ports.forEach { port ->
info.ports.mapNotNull { port -> tryOpenEuiccChannel(port)?.also {
tryOpenEuiccChannel(port)?.also { Log.d(
Log.d( TAG,
TAG, "Found eUICC on slot ${info.physicalSlotIndex} port ${port.portIndex}"
"Found eUICC on slot ${info.physicalSlotIndex} port ${port.portIndex}" )
)
} emit(Pair(info.physicalSlotIndex, port.portIndex))
} }
} }
} }
}.flowOn(Dispatchers.IO)
override suspend fun enumerateUsbEuiccChannel(): Pair<UsbDevice?, EuiccChannel?> = override suspend fun tryOpenUsbEuiccChannel(): Pair<UsbDevice?, Boolean> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
usbManager.deviceList.values.forEach { device -> usbManager.deviceList.values.forEach { device ->
Log.i(TAG, "Scanning USB device ${device.deviceId}:${device.vendorId}") Log.i(TAG, "Scanning USB device ${device.deviceId}:${device.vendorId}")
val iface = device.getSmartCardInterface() ?: return@forEach val iface = device.getSmartCardInterface() ?: return@forEach
// If we don't have permission, tell UI code that we found a candidate device, but we // If we don't have permission, tell UI code that we found a candidate device, but we
// need permission to be able to do anything with it // need permission to be able to do anything with it
if (!usbManager.hasPermission(device)) return@withContext Pair(device, null) if (!usbManager.hasPermission(device)) return@withContext Pair(device, false)
Log.i(TAG, "Found CCID interface on ${device.deviceId}:${device.vendorId}, and has permission; trying to open channel") Log.i(TAG, "Found CCID interface on ${device.deviceId}:${device.vendorId}, and has permission; trying to open channel")
try { try {
val channel = euiccChannelFactory.tryOpenUsbEuiccChannel(device, iface) val channel = euiccChannelFactory.tryOpenUsbEuiccChannel(device, iface)
if (channel != null && channel.lpa.valid) { if (channel != null && channel.lpa.valid) {
usbChannel = channel usbChannel = channel
return@withContext Pair(device, channel) return@withContext Pair(device, true)
} }
} catch (e: Exception) { } catch (e: Exception) {
// Ignored -- skip forward // Ignored -- skip forward
@ -219,7 +244,7 @@ open class DefaultEuiccChannelManager(
} }
Log.i(TAG, "No valid eUICC channel found on USB device ${device.deviceId}:${device.vendorId}") Log.i(TAG, "No valid eUICC channel found on USB device ${device.deviceId}:${device.vendorId}")
} }
return@withContext Pair(null, null) return@withContext Pair(null, false)
} }
override fun invalidate() { override fun invalidate() {

View file

@ -1,26 +1,20 @@
package im.angry.openeuicc.core package im.angry.openeuicc.core
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.flow.Flow
import net.typeblog.lpac_jni.ApduInterface
import net.typeblog.lpac_jni.LocalProfileAssistant import net.typeblog.lpac_jni.LocalProfileAssistant
import net.typeblog.lpac_jni.impl.HttpInterfaceImpl
import net.typeblog.lpac_jni.impl.LocalProfileAssistantImpl
class EuiccChannel( interface EuiccChannel {
val port: UiccPortInfoCompat, val type: String
apduInterface: ApduInterface,
verboseLoggingFlow: Flow<Boolean>
) {
val slotId = port.card.physicalSlotIndex // PHYSICAL slot
val logicalSlotId = port.logicalSlotIndex
val portId = port.portIndex
val lpa: LocalProfileAssistant = val port: UiccPortInfoCompat
LocalProfileAssistantImpl(apduInterface, HttpInterfaceImpl(verboseLoggingFlow))
val slotId: Int // PHYSICAL slot
val logicalSlotId: Int
val portId: Int
val lpa: LocalProfileAssistant
val valid: Boolean val valid: Boolean
get() = lpa.valid
fun close() = lpa.close() fun close()
} }

View file

@ -0,0 +1,27 @@
package im.angry.openeuicc.core
import im.angry.openeuicc.util.*
import kotlinx.coroutines.flow.Flow
import net.typeblog.lpac_jni.ApduInterface
import net.typeblog.lpac_jni.LocalProfileAssistant
import net.typeblog.lpac_jni.impl.HttpInterfaceImpl
import net.typeblog.lpac_jni.impl.LocalProfileAssistantImpl
class EuiccChannelImpl(
override val type: String,
override val port: UiccPortInfoCompat,
apduInterface: ApduInterface,
verboseLoggingFlow: Flow<Boolean>
) : EuiccChannel {
override val slotId = port.card.physicalSlotIndex
override val logicalSlotId = port.logicalSlotIndex
override val portId = port.portIndex
override val lpa: LocalProfileAssistant =
LocalProfileAssistantImpl(apduInterface, HttpInterfaceImpl(verboseLoggingFlow))
override val valid: Boolean
get() = lpa.valid
override fun close() = lpa.close()
}

View file

@ -1,6 +1,7 @@
package im.angry.openeuicc.core package im.angry.openeuicc.core
import android.hardware.usb.UsbDevice import android.hardware.usb.UsbDevice
import kotlinx.coroutines.flow.Flow
/** /**
* EuiccChannelManager holds references to, and manages the lifecycles of, individual * EuiccChannelManager holds references to, and manages the lifecycles of, individual
@ -18,19 +19,26 @@ interface EuiccChannelManager {
} }
/** /**
* Scan all possible _device internal_ sources for EuiccChannels, return them and have all * Scan all possible _device internal_ sources for EuiccChannels, as a flow, return their physical
* scanned channels cached; these channels will remain open for the entire lifetime of * (slotId, portId) and have all scanned channels cached; these channels will remain open
* this EuiccChannelManager object, unless disconnected externally or invalidate()'d * for the entire lifetime of this EuiccChannelManager object, unless disconnected externally
* or invalidate()'d.
*
* To obtain a temporary reference to a EuiccChannel, use `withEuiccChannel()`.
*/ */
suspend fun enumerateEuiccChannels(): List<EuiccChannel> fun flowEuiccPorts(): Flow<Pair<Int, Int>>
/** /**
* Scan all possible USB devices for CCID readers that may contain eUICC cards. * Scan all possible USB devices for CCID readers that may contain eUICC cards.
* If found, try to open it for access, and add it to the internal EuiccChannel cache * If found, try to open it for access, and add it to the internal EuiccChannel cache
* as a "port" with id 99. When user interaction is required to obtain permission * as a "port" with id 99. When user interaction is required to obtain permission
* to interact with the device, the second return value (EuiccChannel) will be null. * to interact with the device, the second return value will be false.
*
* Returns (usbDevice, canOpen). canOpen is false if either (1) no usb reader is found;
* or (2) usb reader is found, but user interaction is required for access;
* or (3) usb reader is found, but we are unable to open ISD-R.
*/ */
suspend fun enumerateUsbEuiccChannel(): Pair<UsbDevice?, EuiccChannel?> suspend fun tryOpenUsbEuiccChannel(): Pair<UsbDevice?, Boolean>
/** /**
* Wait for a slot + port to reconnect (i.e. become valid again) * Wait for a slot + port to reconnect (i.e. become valid again)
@ -40,29 +48,40 @@ interface EuiccChannelManager {
suspend fun waitForReconnect(physicalSlotId: Int, portId: Int, timeoutMillis: Long = 1000) suspend fun waitForReconnect(physicalSlotId: Int, portId: Int, timeoutMillis: Long = 1000)
/** /**
* Returns the EuiccChannel corresponding to a **logical** slot * Returns the first mapped & available port ID for a physical slot, or -1 if
* not found.
*/ */
fun findEuiccChannelBySlotBlocking(logicalSlotId: Int): EuiccChannel? suspend fun findFirstAvailablePort(physicalSlotId: Int): Int
/** /**
* Returns the first EuiccChannel corresponding to a **physical** slot * Returns all mapped & available port IDs for a physical slot.
* If the physical slot supports MEP and has multiple ports, it is undefined
* which of the two channels will be returned.
*/ */
fun findEuiccChannelByPhysicalSlotBlocking(physicalSlotId: Int): EuiccChannel? suspend fun findAvailablePorts(physicalSlotId: Int): List<Int>
class EuiccChannelNotFoundException: Exception("EuiccChannel not found")
/** /**
* Returns all EuiccChannels corresponding to a **physical** slot * Find a EuiccChannel by its slot and port, then run a callback with a reference to it.
* Multiple channels are possible in the case of MEP * The reference is not supposed to be held outside of the callback. This is enforced via
* a wrapper object.
*
* The callback is run on Dispatchers.IO by default.
*
* If a channel for that slot / port is not found, EuiccChannelNotFoundException is thrown
*/ */
suspend fun findAllEuiccChannelsByPhysicalSlot(physicalSlotId: Int): List<EuiccChannel>? suspend fun <R> withEuiccChannel(
fun findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId: Int): List<EuiccChannel>? physicalSlotId: Int,
portId: Int,
fn: suspend (EuiccChannel) -> R
): R
/** /**
* Returns the EuiccChannel corresponding to a **physical** slot and a port ID * Same as withEuiccChannel(Int, Int, (EuiccChannel) -> R) but instead uses logical slot ID
*/ */
suspend fun findEuiccChannelByPort(physicalSlotId: Int, portId: Int): EuiccChannel? suspend fun <R> withEuiccChannel(
fun findEuiccChannelByPortBlocking(physicalSlotId: Int, portId: Int): EuiccChannel? logicalSlotId: Int,
fn: suspend (EuiccChannel) -> R
): R
/** /**
* Invalidate all EuiccChannels previously cached by this Manager * Invalidate all EuiccChannels previously cached by this Manager
@ -74,7 +93,7 @@ interface EuiccChannelManager {
* This is only expected to be implemented when the application is privileged * This is only expected to be implemented when the application is privileged
* TODO: Remove this from the common interface * TODO: Remove this from the common interface
*/ */
fun notifyEuiccProfilesChanged(logicalSlotId: Int) { suspend fun notifyEuiccProfilesChanged(logicalSlotId: Int) {
// no-op by default // no-op by default
} }
} }

View file

@ -0,0 +1,44 @@
package im.angry.openeuicc.core
import im.angry.openeuicc.util.*
import net.typeblog.lpac_jni.LocalProfileAssistant
class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel {
private var _inner: EuiccChannel? = orig
private val channel: EuiccChannel
get() {
if (_inner == null) {
throw IllegalStateException("This wrapper has been invalidated")
}
return _inner!!
}
override val type: String
get() = channel.type
override val port: UiccPortInfoCompat
get() = channel.port
override val slotId: Int
get() = channel.slotId
override val logicalSlotId: Int
get() = channel.logicalSlotId
override val portId: Int
get() = channel.portId
private val lpaDelegate = lazy {
LocalProfileAssistantWrapper(channel.lpa)
}
override val lpa: LocalProfileAssistant by lpaDelegate
override val valid: Boolean
get() = channel.valid
override fun close() = channel.close()
fun invalidateWrapper() {
_inner = null
if (lpaDelegate.isInitialized()) {
(lpa as LocalProfileAssistantWrapper).invalidateWrapper()
}
}
}

View file

@ -0,0 +1,63 @@
package im.angry.openeuicc.core
import net.typeblog.lpac_jni.EuiccInfo2
import net.typeblog.lpac_jni.LocalProfileAssistant
import net.typeblog.lpac_jni.LocalProfileInfo
import net.typeblog.lpac_jni.LocalProfileNotification
import net.typeblog.lpac_jni.ProfileDownloadCallback
class LocalProfileAssistantWrapper(orig: LocalProfileAssistant) :
LocalProfileAssistant {
private var _inner: LocalProfileAssistant? = orig
private val lpa: LocalProfileAssistant
get() {
if (_inner == null) {
throw IllegalStateException("This wrapper has been invalidated")
}
return _inner!!
}
override val valid: Boolean
get() = lpa.valid
override val profiles: List<LocalProfileInfo>
get() = lpa.profiles
override val notifications: List<LocalProfileNotification>
get() = lpa.notifications
override val eID: String
get() = lpa.eID
override val euiccInfo2: EuiccInfo2?
get() = lpa.euiccInfo2
override fun setEs10xMss(mss: Byte) = lpa.setEs10xMss(mss)
override fun enableProfile(iccid: String, refresh: Boolean): Boolean =
lpa.enableProfile(iccid, refresh)
override fun disableProfile(iccid: String, refresh: Boolean): Boolean =
lpa.disableProfile(iccid, refresh)
override fun deleteProfile(iccid: String): Boolean = lpa.deleteProfile(iccid)
override fun downloadProfile(
smdp: String,
matchingId: String?,
imei: String?,
confirmationCode: String?,
callback: ProfileDownloadCallback
): Boolean = lpa.downloadProfile(smdp, matchingId, imei, confirmationCode, callback)
override fun deleteNotification(seqNumber: Long): Boolean = lpa.deleteNotification(seqNumber)
override fun handleNotification(seqNumber: Long): Boolean = lpa.handleNotification(seqNumber)
override fun setNickname(iccid: String, nickname: String): Boolean =
lpa.setNickname(iccid, nickname)
override fun close() = lpa.close()
fun invalidateWrapper() {
_inner = null
}
}

View file

@ -1,13 +1,12 @@
package im.angry.openeuicc.di package im.angry.openeuicc.di
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.ui.EuiccManagementFragment import im.angry.openeuicc.ui.EuiccManagementFragment
import im.angry.openeuicc.ui.NoEuiccPlaceholderFragment import im.angry.openeuicc.ui.NoEuiccPlaceholderFragment
open class DefaultUiComponentFactory : UiComponentFactory { open class DefaultUiComponentFactory : UiComponentFactory {
override fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment = override fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment =
EuiccManagementFragment.newInstance(channel.slotId, channel.portId) EuiccManagementFragment.newInstance(slotId, portId)
override fun createNoEuiccPlaceholderFragment(): Fragment = NoEuiccPlaceholderFragment() override fun createNoEuiccPlaceholderFragment(): Fragment = NoEuiccPlaceholderFragment()
} }

View file

@ -1,10 +1,9 @@
package im.angry.openeuicc.di package im.angry.openeuicc.di
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.ui.EuiccManagementFragment import im.angry.openeuicc.ui.EuiccManagementFragment
interface UiComponentFactory { interface UiComponentFactory {
fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment
fun createNoEuiccPlaceholderFragment(): Fragment fun createNoEuiccPlaceholderFragment(): Fragment
} }

View file

@ -21,6 +21,8 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.last
import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.flow.takeWhile
@ -55,7 +57,14 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
private const val TAG = "EuiccChannelManagerService" private const val TAG = "EuiccChannelManagerService"
private const val CHANNEL_ID = "tasks" private const val CHANNEL_ID = "tasks"
private const val FOREGROUND_ID = 1000 private const val FOREGROUND_ID = 1000
private const val TASK_FAILURE_ID = 1001 private const val TASK_FAILURE_ID = 1000
/**
* Utility function to wait for a foreground task to be done, return its
* error if any, or null on success.
*/
suspend fun Flow<ForegroundTaskState>.waitDone(): Throwable? =
(this.last() as ForegroundTaskState.Done).error
} }
inner class LocalBinder : Binder() { inner class LocalBinder : Binder() {
@ -185,7 +194,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
failureTitle: String, failureTitle: String,
iconRes: Int, iconRes: Int,
task: suspend EuiccChannelManagerService.() -> Unit task: suspend EuiccChannelManagerService.() -> Unit
): Flow<ForegroundTaskState>? { ): Flow<ForegroundTaskState> {
// Atomically set the state to InProgress. If this returns true, we are // Atomically set the state to InProgress. If this returns true, we are
// the only task currently in progress. // the only task currently in progress.
if (!foregroundTaskState.compareAndSet( if (!foregroundTaskState.compareAndSet(
@ -193,7 +202,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
ForegroundTaskState.InProgress(0) ForegroundTaskState.InProgress(0)
) )
) { ) {
return null return flow { emit(ForegroundTaskState.Done(IllegalStateException("There are tasks currently running"))) }
} }
lifecycleScope.launch(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
@ -280,26 +289,27 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
matchingId: String?, matchingId: String?,
confirmationCode: String?, confirmationCode: String?,
imei: String? imei: String?
): Flow<ForegroundTaskState>? = ): Flow<ForegroundTaskState> =
launchForegroundTask( launchForegroundTask(
getString(R.string.task_profile_download), getString(R.string.task_profile_download),
getString(R.string.task_profile_download_failure), getString(R.string.task_profile_download_failure),
R.drawable.ic_task_sim_card_download R.drawable.ic_task_sim_card_download
) { ) {
euiccChannelManager.beginTrackedOperation(slotId, portId) { euiccChannelManager.beginTrackedOperation(slotId, portId) {
val channel = euiccChannelManager.findEuiccChannelByPort(slotId, portId) val res = euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
val res = channel!!.lpa.downloadProfile( channel.lpa.downloadProfile(
smdp, smdp,
matchingId, matchingId,
imei, imei,
confirmationCode, confirmationCode,
object : ProfileDownloadCallback { object : ProfileDownloadCallback {
override fun onStateUpdate(state: ProfileDownloadCallback.DownloadState) { override fun onStateUpdate(state: ProfileDownloadCallback.DownloadState) {
if (state.progress == 0) return if (state.progress == 0) return
foregroundTaskState.value = foregroundTaskState.value =
ForegroundTaskState.InProgress(state.progress) ForegroundTaskState.InProgress(state.progress)
} }
}) })
}
if (!res) { if (!res) {
// TODO: Provide more details on the error // TODO: Provide more details on the error
@ -315,16 +325,18 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
portId: Int, portId: Int,
iccid: String, iccid: String,
name: String name: String
): Flow<ForegroundTaskState>? = ): Flow<ForegroundTaskState> =
launchForegroundTask( launchForegroundTask(
getString(R.string.task_profile_rename), getString(R.string.task_profile_rename),
getString(R.string.task_profile_rename_failure), getString(R.string.task_profile_rename_failure),
R.drawable.ic_task_rename R.drawable.ic_task_rename
) { ) {
val res = euiccChannelManager.findEuiccChannelByPort(slotId, portId)!!.lpa.setNickname( val res = euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
iccid, channel.lpa.setNickname(
name iccid,
) name
)
}
if (!res) { if (!res) {
throw RuntimeException("Profile not renamed") throw RuntimeException("Profile not renamed")
@ -335,17 +347,16 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
slotId: Int, slotId: Int,
portId: Int, portId: Int,
iccid: String iccid: String
): Flow<ForegroundTaskState>? = ): Flow<ForegroundTaskState> =
launchForegroundTask( launchForegroundTask(
getString(R.string.task_profile_delete), getString(R.string.task_profile_delete),
getString(R.string.task_profile_delete_failure), getString(R.string.task_profile_delete_failure),
R.drawable.ic_task_delete R.drawable.ic_task_delete
) { ) {
euiccChannelManager.beginTrackedOperation(slotId, portId) { euiccChannelManager.beginTrackedOperation(slotId, portId) {
euiccChannelManager.findEuiccChannelByPort( euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
slotId, channel.lpa.deleteProfile(iccid)
portId }
)!!.lpa.deleteProfile(iccid)
preferenceRepository.notificationDeleteFlow.first() preferenceRepository.notificationDeleteFlow.first()
} }
@ -359,15 +370,17 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
iccid: String, iccid: String,
enable: Boolean, // Enable or disable the profile indicated in iccid enable: Boolean, // Enable or disable the profile indicated in iccid
reconnectTimeoutMillis: Long = 0 // 0 = do not wait for reconnect, useful for USB readers reconnectTimeoutMillis: Long = 0 // 0 = do not wait for reconnect, useful for USB readers
): Flow<ForegroundTaskState>? = ): Flow<ForegroundTaskState> =
launchForegroundTask( launchForegroundTask(
getString(R.string.task_profile_switch), getString(R.string.task_profile_switch),
getString(R.string.task_profile_switch_failure), getString(R.string.task_profile_switch_failure),
R.drawable.ic_task_switch R.drawable.ic_task_switch
) { ) {
euiccChannelManager.beginTrackedOperation(slotId, portId) { euiccChannelManager.beginTrackedOperation(slotId, portId) {
val channel = euiccChannelManager.findEuiccChannelByPort(slotId, portId)!! val (res, refreshed) = euiccChannelManager.withEuiccChannel(
val (res, refreshed) = slotId,
portId
) { channel ->
if (!channel.lpa.switchProfile(iccid, enable, refresh = true)) { if (!channel.lpa.switchProfile(iccid, enable, refresh = true)) {
// Sometimes, we *can* enable or disable the profile, but we cannot // Sometimes, we *can* enable or disable the profile, but we cannot
// send the refresh command to the modem because the profile somehow // send the refresh command to the modem because the profile somehow
@ -378,6 +391,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
} else { } else {
Pair(true, true) Pair(true, true)
} }
}
if (!res) { if (!res) {
throw RuntimeException("Could not switch profile") throw RuntimeException("Could not switch profile")

View file

@ -3,6 +3,8 @@ package im.angry.openeuicc.ui
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -10,22 +12,32 @@ class DirectProfileDownloadActivity : BaseEuiccAccessActivity(), SlotSelectFragm
override fun onInit() { override fun onInit() {
lifecycleScope.launch { lifecycleScope.launch {
val knownChannels = withContext(Dispatchers.IO) { val knownChannels = withContext(Dispatchers.IO) {
euiccChannelManager.enumerateEuiccChannels() euiccChannelManager.flowEuiccPorts().map { (slotId, portId) ->
euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
Triple(slotId, channel.logicalSlotId, portId)
}
}.toList().sortedBy { it.second }
} }
when { when {
knownChannels.isEmpty() -> { knownChannels.isEmpty() -> {
finish() finish()
} }
knownChannels.hasMultipleChips -> { // Detect multiple eUICC chips
SlotSelectFragment.newInstance(knownChannels.sortedBy { it.logicalSlotId }) knownChannels.distinctBy { it.first }.size > 1 -> {
SlotSelectFragment.newInstance(
knownChannels.map { it.first },
knownChannels.map { it.second },
knownChannels.map { it.third })
.show(supportFragmentManager, SlotSelectFragment.TAG) .show(supportFragmentManager, SlotSelectFragment.TAG)
} }
else -> { else -> {
// If the device has only one eSIM "chip" (but may be mapped to multiple slots), // If the device has only one eSIM "chip" (but may be mapped to multiple slots),
// we can skip the slot selection dialog since there is only one chip to save to. // we can skip the slot selection dialog since there is only one chip to save to.
onSlotSelected(knownChannels[0].slotId, onSlotSelected(
knownChannels[0].portId) knownChannels[0].first,
knownChannels[0].third
)
} }
} }
} }

View file

@ -0,0 +1,202 @@
package im.angry.openeuicc.ui
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import im.angry.openeuicc.common.R
import im.angry.openeuicc.util.*
import kotlinx.coroutines.launch
import net.typeblog.lpac_jni.impl.DEFAULT_PKID_GSMA_RSP2_ROOT_CI1
import net.typeblog.lpac_jni.impl.PKID_GSMA_TEST_CI
class EuiccInfoActivity : BaseEuiccAccessActivity() {
private lateinit var swipeRefresh: SwipeRefreshLayout
private lateinit var infoList: RecyclerView
private var logicalSlotId: Int = -1
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_euicc_info)
setSupportActionBar(requireViewById(R.id.toolbar))
setupToolbarInsets()
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
swipeRefresh = requireViewById(R.id.swipe_refresh)
infoList = requireViewById(R.id.recycler_view)
infoList.layoutManager =
LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
infoList.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
infoList.adapter = EuiccInfoAdapter()
logicalSlotId = intent.getIntExtra("logicalSlotId", 0)
title = getString(
R.string.euicc_info_activity_title,
getString(R.string.channel_name_format, logicalSlotId)
)
swipeRefresh.setOnRefreshListener { refresh() }
setupRootViewInsets(infoList)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
android.R.id.home -> {
finish()
true
}
else -> super.onOptionsItemSelected(item)
}
override fun onInit() {
refresh()
}
private fun refresh() {
swipeRefresh.isRefreshing = true
lifecycleScope.launch {
val unknownStr = getString(R.string.unknown)
val newItems = mutableListOf<Pair<String, String>>()
newItems.add(
Pair(
getString(R.string.euicc_info_access_mode),
euiccChannelManager.withEuiccChannel(logicalSlotId) { channel -> channel.type }
)
)
newItems.add(
Pair(
getString(R.string.euicc_info_removable),
if (euiccChannelManager.withEuiccChannel(logicalSlotId) { channel -> channel.port.card.isRemovable }) {
getString(R.string.yes)
} else {
getString(R.string.no)
}
)
)
newItems.add(
Pair(
getString(R.string.euicc_info_eid),
euiccChannelManager.withEuiccChannel(logicalSlotId) { channel -> channel.lpa.eID }
)
)
val euiccInfo2 = euiccChannelManager.withEuiccChannel(logicalSlotId) { channel ->
channel.lpa.euiccInfo2
}
newItems.add(
Pair(
getString(R.string.euicc_info_firmware_version),
euiccInfo2?.euiccFirmwareVersion ?: unknownStr
)
)
newItems.add(
Pair(
getString(R.string.euicc_info_globalplatform_version),
euiccInfo2?.globalPlatformVersion ?: unknownStr
)
)
newItems.add(
Pair(
getString(R.string.euicc_info_pp_version),
euiccInfo2?.ppVersion ?: unknownStr
)
)
newItems.add(
Pair(
getString(R.string.euicc_info_sas_accreditation_number),
euiccInfo2?.sasAccreditationNumber ?: unknownStr
)
)
newItems.add(
Pair(
getString(R.string.euicc_info_free_nvram),
euiccInfo2?.freeNvram?.let { formatFreeSpace(it) } ?: unknownStr
))
newItems.add(
Pair(
getString(R.string.euicc_info_gsma_prod),
if (euiccInfo2?.euiccCiPKIdListForSigning?.contains(
DEFAULT_PKID_GSMA_RSP2_ROOT_CI1
) == true
) {
getString(R.string.supported)
} else {
getString(R.string.unsupported)
}
)
)
newItems.add(
Pair(
getString(R.string.euicc_info_gsma_test),
if (PKID_GSMA_TEST_CI.any { euiccInfo2?.euiccCiPKIdListForSigning?.contains(it) == true }) {
getString(R.string.supported)
} else {
getString(R.string.unsupported)
}
)
)
(infoList.adapter!! as EuiccInfoAdapter).euiccInfoItems = newItems
swipeRefresh.isRefreshing = false
}
}
inner class EuiccInfoViewHolder(root: View) : ViewHolder(root) {
private val title: TextView = root.requireViewById(R.id.euicc_info_title)
private val content: TextView = root.requireViewById(R.id.euicc_info_content)
fun bind(item: Pair<String, String>) {
title.text = item.first
content.text = item.second
}
}
inner class EuiccInfoAdapter : RecyclerView.Adapter<EuiccInfoViewHolder>() {
var euiccInfoItems: List<Pair<String, String>> = listOf()
@SuppressLint("NotifyDataSetChanged")
set(newVal) {
field = newVal
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EuiccInfoViewHolder {
val root = LayoutInflater.from(parent.context)
.inflate(R.layout.euicc_info_item, parent, false)
return EuiccInfoViewHolder(root)
}
override fun getItemCount(): Int = euiccInfoItems.size
override fun onBindViewHolder(holder: EuiccInfoViewHolder, position: Int) {
holder.bind(euiccInfoItems[position])
}
}
}

View file

@ -6,7 +6,6 @@ import android.content.ClipboardManager
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.text.method.PasswordTransformationMethod import android.text.method.PasswordTransformationMethod
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
@ -31,11 +30,11 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton
import net.typeblog.lpac_jni.LocalProfileInfo import net.typeblog.lpac_jni.LocalProfileInfo
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
import im.angry.openeuicc.service.EuiccChannelManagerService import im.angry.openeuicc.service.EuiccChannelManagerService
import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.last
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -52,6 +51,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
private lateinit var swipeRefresh: SwipeRefreshLayout private lateinit var swipeRefresh: SwipeRefreshLayout
private lateinit var fab: FloatingActionButton private lateinit var fab: FloatingActionButton
private lateinit var profileList: RecyclerView private lateinit var profileList: RecyclerView
private var logicalSlotId: Int = -1
private val adapter = EuiccProfileAdapter() private val adapter = EuiccProfileAdapter()
@ -127,9 +127,21 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
override fun onOptionsItemSelected(item: MenuItem): Boolean = override fun onOptionsItemSelected(item: MenuItem): Boolean =
when (item.itemId) { when (item.itemId) {
R.id.show_notifications -> { R.id.show_notifications -> {
Intent(requireContext(), NotificationsActivity::class.java).apply { if (logicalSlotId != -1) {
putExtra("logicalSlotId", channel.logicalSlotId) Intent(requireContext(), NotificationsActivity::class.java).apply {
startActivity(this) putExtra("logicalSlotId", logicalSlotId)
startActivity(this)
}
}
true
}
R.id.euicc_info -> {
if (logicalSlotId != -1) {
Intent(requireContext(), EuiccInfoActivity::class.java).apply {
putExtra("logicalSlotId", logicalSlotId)
startActivity(this)
}
} }
true true
} }
@ -148,31 +160,36 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
listOf() listOf()
} }
@SuppressLint("NotifyDataSetChanged")
private fun refresh() { private fun refresh() {
if (invalid) return if (invalid) return
swipeRefresh.isRefreshing = true swipeRefresh.isRefreshing = true
lifecycleScope.launch { lifecycleScope.launch {
ensureEuiccChannelManager() doRefresh()
euiccChannelManagerService.waitForForegroundTask() }
}
if (!this@EuiccManagementFragment::disableSafeguardFlow.isInitialized) { @SuppressLint("NotifyDataSetChanged")
disableSafeguardFlow = protected open suspend fun doRefresh() {
preferenceRepository.disableSafeguardFlow.stateIn(lifecycleScope) ensureEuiccChannelManager()
} euiccChannelManagerService.waitForForegroundTask()
val profiles = withContext(Dispatchers.IO) { if (!this@EuiccManagementFragment::disableSafeguardFlow.isInitialized) {
euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId) disableSafeguardFlow =
channel.lpa.profiles.operational preferenceRepository.disableSafeguardFlow.stateIn(lifecycleScope)
} }
withContext(Dispatchers.Main) { val profiles = withEuiccChannel { channel ->
adapter.profiles = profiles logicalSlotId = channel.logicalSlotId
adapter.footerViews = onCreateFooterViews(profileList, profiles) euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId)
adapter.notifyDataSetChanged() channel.lpa.profiles.operational
swipeRefresh.isRefreshing = false }
}
withContext(Dispatchers.Main) {
adapter.profiles = profiles
adapter.footerViews = onCreateFooterViews(profileList, profiles)
adapter.notifyDataSetChanged()
swipeRefresh.isRefreshing = false
} }
} }
@ -192,7 +209,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
ensureEuiccChannelManager() ensureEuiccChannelManager()
euiccChannelManagerService.waitForForegroundTask() euiccChannelManagerService.waitForForegroundTask()
val res = euiccChannelManagerService.launchProfileSwitchTask( val err = euiccChannelManagerService.launchProfileSwitchTask(
slotId, slotId,
portId, portId,
iccid, iccid,
@ -202,14 +219,9 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
} else { } else {
30 * 1000 30 * 1000
} }
)?.last() as? EuiccChannelManagerService.ForegroundTaskState.Done ).waitDone()
if (res == null) { when (err) {
showSwitchFailureText()
return@launch
}
when (res.error) {
null -> {} null -> {}
is EuiccChannelManagerService.SwitchingProfilesRefreshException -> { is EuiccChannelManagerService.SwitchingProfilesRefreshException -> {
// This is only really fatal for internal eSIMs // This is only really fatal for internal eSIMs

View file

@ -23,9 +23,12 @@ import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -44,6 +47,7 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
private var refreshing = false private var refreshing = false
private data class Page( private data class Page(
val logicalSlotId: Int,
val title: String, val title: String,
val createFragment: () -> Fragment val createFragment: () -> Fragment
) )
@ -138,65 +142,83 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
// Prevent concurrent access with any running foreground task // Prevent concurrent access with any running foreground task
euiccChannelManagerService.waitForForegroundTask() euiccChannelManagerService.waitForForegroundTask()
val knownChannels = withContext(Dispatchers.IO) { val (usbDevice, _) = withContext(Dispatchers.IO) {
euiccChannelManager.enumerateEuiccChannels().onEach { euiccChannelManager.tryOpenUsbEuiccChannel()
Log.d(TAG, "slot ${it.slotId} port ${it.portId}") }
val newPages: MutableList<Page> = mutableListOf()
euiccChannelManager.flowEuiccPorts().onEach { (slotId, portId) ->
Log.d(TAG, "slot $slotId port $portId")
euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
if (preferenceRepository.verboseLoggingFlow.first()) { if (preferenceRepository.verboseLoggingFlow.first()) {
Log.d(TAG, it.lpa.eID) Log.d(TAG, channel.lpa.eID)
} }
// Request the system to refresh the list of profiles every time we start // Request the system to refresh the list of profiles every time we start
// Note that this is currently supposed to be no-op when unprivileged, // Note that this is currently supposed to be no-op when unprivileged,
// but it could change in the future // but it could change in the future
euiccChannelManager.notifyEuiccProfilesChanged(it.logicalSlotId) euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId)
newPages.add(
Page(
channel.logicalSlotId,
getString(R.string.channel_name_format, channel.logicalSlotId)
) {
appContainer.uiComponentFactory.createEuiccManagementFragment(
slotId,
portId
)
})
} }
}.collect()
// If USB readers exist, add them at the very last
// We use a wrapper fragment to handle logic specific to USB readers
usbDevice?.let {
newPages.add(
Page(
EuiccChannelManager.USB_CHANNEL_ID,
it.productName ?: getString(R.string.usb)
) { UsbCcidReaderFragment() })
}
viewPager.visibility = View.VISIBLE
if (newPages.size > 1) {
tabs.visibility = View.VISIBLE
} else if (newPages.isEmpty()) {
newPages.add(
Page(
-1,
""
) { appContainer.uiComponentFactory.createNoEuiccPlaceholderFragment() })
} }
val (usbDevice, _) = withContext(Dispatchers.IO) { newPages.sortBy { it.logicalSlotId }
euiccChannelManager.enumerateUsbEuiccChannel()
pages.clear()
pages.addAll(newPages)
loadingProgress.visibility = View.GONE
pagerAdapter.notifyDataSetChanged()
// Reset the adapter so that the current view actually gets cleared
// notifyDataSetChanged() doesn't cause the current view to be removed.
viewPager.adapter = pagerAdapter
if (fromUsbEvent && usbDevice != null) {
// If this refresh was triggered by a USB insertion while active, scroll to that page
viewPager.post {
viewPager.setCurrentItem(pages.size - 1, true)
}
} else {
viewPager.currentItem = 0
} }
withContext(Dispatchers.Main) { if (pages.size > 0) {
loadingProgress.visibility = View.GONE ensureNotificationPermissions()
knownChannels.sortedBy { it.logicalSlotId }.forEach { channel ->
pages.add(Page(
getString(R.string.channel_name_format, channel.logicalSlotId)
) { appContainer.uiComponentFactory.createEuiccManagementFragment(channel) })
}
// If USB readers exist, add them at the very last
// We use a wrapper fragment to handle logic specific to USB readers
usbDevice?.let {
pages.add(Page(it.productName ?: getString(R.string.usb)) { UsbCcidReaderFragment() })
}
viewPager.visibility = View.VISIBLE
if (pages.size > 1) {
tabs.visibility = View.VISIBLE
} else if (pages.isEmpty()) {
pages.add(Page("") { appContainer.uiComponentFactory.createNoEuiccPlaceholderFragment() })
}
pagerAdapter.notifyDataSetChanged()
// Reset the adapter so that the current view actually gets cleared
// notifyDataSetChanged() doesn't cause the current view to be removed.
viewPager.adapter = pagerAdapter
if (fromUsbEvent && usbDevice != null) {
// If this refresh was triggered by a USB insertion while active, scroll to that page
viewPager.post {
viewPager.setCurrentItem(pages.size - 1, true)
}
} else {
viewPager.currentItem = 0
}
if (pages.size > 0) {
ensureNotificationPermissions()
}
refreshing = false
} }
refreshing = false
} }
private fun refresh(fromUsbEvent: Boolean = false) { private fun refresh(fromUsbEvent: Boolean = false) {

View file

@ -20,7 +20,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -33,7 +32,7 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
private lateinit var notificationList: RecyclerView private lateinit var notificationList: RecyclerView
private val notificationAdapter = NotificationAdapter() private val notificationAdapter = NotificationAdapter()
private lateinit var euiccChannel: EuiccChannel private var logicalSlotId = -1
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge() enableEdgeToEdge()
@ -56,7 +55,7 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
notificationList.adapter = notificationAdapter notificationList.adapter = notificationAdapter
registerForContextMenu(notificationList) registerForContextMenu(notificationList)
val logicalSlotId = intent.getIntExtra("logicalSlotId", 0) logicalSlotId = intent.getIntExtra("logicalSlotId", 0)
// This is slightly different from the MainActivity logic // This is slightly different from the MainActivity logic
// due to the length (we don't want to display the full USB product name) // due to the length (we don't want to display the full USB product name)
@ -104,16 +103,8 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
swipeRefresh.isRefreshing = true swipeRefresh.isRefreshing = true
lifecycleScope.launch { lifecycleScope.launch {
if (!this@NotificationsActivity::euiccChannel.isInitialized) { withContext(Dispatchers.IO) {
withContext(Dispatchers.IO) { euiccChannelManagerLoaded.await()
euiccChannelManagerLoaded.await()
euiccChannel = euiccChannelManager.findEuiccChannelBySlotBlocking(
intent.getIntExtra(
"logicalSlotId",
0
)
)!!
}
} }
task() task()
@ -124,13 +115,11 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
private fun refresh() { private fun refresh() {
launchTask { launchTask {
val profiles = withContext(Dispatchers.IO) {
euiccChannel.lpa.profiles
}
notificationAdapter.notifications = notificationAdapter.notifications =
withContext(Dispatchers.IO) { euiccChannelManager.withEuiccChannel(logicalSlotId) { channel ->
euiccChannel.lpa.notifications.map { val profiles = channel.lpa.profiles
channel.lpa.notifications.map {
val profile = profiles.find { p -> p.iccid == it.iccid } val profile = profiles.find { p -> p.iccid == it.iccid }
LocalProfileNotificationWrapper(it, profile?.displayName ?: "???") LocalProfileNotificationWrapper(it, profile?.displayName ?: "???")
} }
@ -205,7 +194,9 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
R.id.notification_process -> { R.id.notification_process -> {
launchTask { launchTask {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
euiccChannel.lpa.handleNotification(notification.inner.seqNumber) euiccChannelManager.withEuiccChannel(logicalSlotId) { channel ->
channel.lpa.handleNotification(notification.inner.seqNumber)
}
} }
refresh() refresh()
@ -215,7 +206,9 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
R.id.notification_delete -> { R.id.notification_delete -> {
launchTask { launchTask {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
euiccChannel.lpa.deleteNotification(notification.inner.seqNumber) euiccChannelManager.withEuiccChannel(logicalSlotId) { channel ->
channel.lpa.deleteNotification(notification.inner.seqNumber)
}
} }
refresh() refresh()

View file

@ -8,8 +8,8 @@ import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -74,7 +74,7 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
slotId, slotId,
portId, portId,
requireArguments().getString("iccid")!! requireArguments().getString("iccid")!!
)!!.onStart { ).onStart {
if (parentFragment is EuiccProfilesChangedListener) { if (parentFragment is EuiccProfilesChangedListener) {
// Trigger a refresh in the parent fragment -- it should wait until // Trigger a refresh in the parent fragment -- it should wait until
// any foreground task is completed before actually doing a refresh // any foreground task is completed before actually doing a refresh
@ -86,7 +86,7 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
} catch (e: IllegalStateException) { } catch (e: IllegalStateException) {
// Ignored // Ignored
} }
}.collect() }.waitDone()
} }
} }
} }

View file

@ -19,9 +19,9 @@ import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions import com.journeyapps.barcodescanner.ScanOptions
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
import im.angry.openeuicc.service.EuiccChannelManagerService import im.angry.openeuicc.service.EuiccChannelManagerService
import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.last
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -159,22 +159,26 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(),
return@launch return@launch
} }
val imei = try { withEuiccChannel { channel ->
telephonyManager.getImei(channel.logicalSlotId) ?: "" val imei = try {
} catch (e: Exception) { telephonyManager.getImei(channel.logicalSlotId) ?: ""
"" } catch (e: Exception) {
} ""
}
// Fetch remaining NVRAM // Fetch remaining NVRAM
val str = channel.lpa.euiccInfo2?.freeNvram?.also { val str = channel.lpa.euiccInfo2?.freeNvram?.also {
freeNvram = it freeNvram = it
}?.let { formatFreeSpace(it) } }?.let { formatFreeSpace(it) }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
profileDownloadFreeSpace.text = getString(R.string.profile_download_free_space, profileDownloadFreeSpace.text = getString(
str ?: getText(R.string.unknown)) R.string.profile_download_free_space,
profileDownloadIMEI.editText!!.text = str ?: getText(R.string.unknown)
Editable.Factory.getInstance().newEditable(imei) )
profileDownloadIMEI.editText!!.text =
Editable.Factory.getInstance().newEditable(imei)
}
} }
} }
} }
@ -215,14 +219,11 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(),
lifecycleScope.launch { lifecycleScope.launch {
ensureEuiccChannelManager() ensureEuiccChannelManager()
euiccChannelManagerService.waitForForegroundTask() euiccChannelManagerService.waitForForegroundTask()
val res = doDownloadProfile(server, code, confirmationCode, imei) val err = doDownloadProfile(server, code, confirmationCode, imei)
if (res == null || res.error != null) { if (err != null) {
Log.d(TAG, "Error downloading profile") Log.d(TAG, "Error downloading profile")
Log.d(TAG, Log.getStackTraceString(err))
if (res?.error != null) {
Log.d(TAG, Log.getStackTraceString(res.error))
}
Toast.makeText(requireContext(), R.string.profile_download_failed, Toast.LENGTH_LONG).show() Toast.makeText(requireContext(), R.string.profile_download_failed, Toast.LENGTH_LONG).show()
} }
@ -246,14 +247,15 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(),
imei: String? imei: String?
) = withContext(Dispatchers.Main) { ) = withContext(Dispatchers.Main) {
// The service is responsible for launching the actual blocking part on the IO context // The service is responsible for launching the actual blocking part on the IO context
val res = euiccChannelManagerService.launchProfileDownloadTask( // On our side, we need the Main context because of the UI updates
euiccChannelManagerService.launchProfileDownloadTask(
slotId, slotId,
portId, portId,
server, server,
code, code,
confirmationCode, confirmationCode,
imei imei
)!!.onEach { ).onEach {
if (it is EuiccChannelManagerService.ForegroundTaskState.InProgress) { if (it is EuiccChannelManagerService.ForegroundTaskState.InProgress) {
progress.progress = it.progress progress.progress = it.progress
progress.isIndeterminate = it.progress == 0 progress.isIndeterminate = it.progress == 0
@ -261,9 +263,7 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(),
progress.progress = 100 progress.progress = 100
progress.isIndeterminate = false progress.isIndeterminate = false
} }
}.last() }.waitDone()
res as? EuiccChannelManagerService.ForegroundTaskState.Done
} }
override fun onDismiss(dialog: DialogInterface) { override fun onDismiss(dialog: DialogInterface) {

View file

@ -11,8 +11,8 @@ import androidx.appcompat.widget.Toolbar
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragmentMarker { class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragmentMarker {
@ -100,7 +100,7 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
portId, portId,
requireArguments().getString("iccid")!!, requireArguments().getString("iccid")!!,
name name
)?.collect() ).waitDone()
if (parentFragment is EuiccProfilesChangedListener) { if (parentFragment is EuiccProfilesChangedListener) {
(parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged() (parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged()

View file

@ -16,12 +16,12 @@ class SlotSelectFragment : BaseMaterialDialogFragment(), OpenEuiccContextMarker
companion object { companion object {
const val TAG = "SlotSelectFragment" const val TAG = "SlotSelectFragment"
fun newInstance(knownChannels: List<EuiccChannel>): SlotSelectFragment { fun newInstance(slotIds: List<Int>, logicalSlotIds: List<Int>, portIds: List<Int>): SlotSelectFragment {
return SlotSelectFragment().apply { return SlotSelectFragment().apply {
arguments = Bundle().apply { arguments = Bundle().apply {
putIntArray("slotIds", knownChannels.map { it.slotId }.toIntArray()) putIntArray("slotIds", slotIds.toIntArray())
putIntArray("logicalSlotIds", knownChannels.map { it.logicalSlotId }.toIntArray()) putIntArray("logicalSlotIds", logicalSlotIds.toIntArray())
putIntArray("portIds", knownChannels.map { it.portId }.toIntArray()) putIntArray("portIds", portIds.toIntArray())
} }
} }
} }

View file

@ -20,7 +20,6 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.commit import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -73,7 +72,6 @@ class UsbCcidReaderFragment : Fragment(), OpenEuiccContextMarker {
private lateinit var loadingProgress: ProgressBar private lateinit var loadingProgress: ProgressBar
private var usbDevice: UsbDevice? = null private var usbDevice: UsbDevice? = null
private var usbChannel: EuiccChannel? = null
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -140,24 +138,26 @@ class UsbCcidReaderFragment : Fragment(), OpenEuiccContextMarker {
permissionButton.visibility = View.GONE permissionButton.visibility = View.GONE
loadingProgress.visibility = View.VISIBLE loadingProgress.visibility = View.VISIBLE
val (device, channel) = withContext(Dispatchers.IO) { val (device, canOpen) = withContext(Dispatchers.IO) {
euiccChannelManager.enumerateUsbEuiccChannel() euiccChannelManager.tryOpenUsbEuiccChannel()
} }
loadingProgress.visibility = View.GONE loadingProgress.visibility = View.GONE
usbDevice = device usbDevice = device
usbChannel = channel
if (device != null && channel == null && !usbManager.hasPermission(device)) { if (device != null && !canOpen && !usbManager.hasPermission(device)) {
text.text = getString(R.string.usb_permission_needed) text.text = getString(R.string.usb_permission_needed)
text.visibility = View.VISIBLE text.visibility = View.VISIBLE
permissionButton.visibility = View.VISIBLE permissionButton.visibility = View.VISIBLE
} else if (device != null && channel != null) { } else if (device != null && canOpen) {
childFragmentManager.commit { childFragmentManager.commit {
replace( replace(
R.id.child_container, R.id.child_container,
appContainer.uiComponentFactory.createEuiccManagementFragment(channel) appContainer.uiComponentFactory.createEuiccManagementFragment(
EuiccChannelManager.USB_CHANNEL_ID,
0
)
) )
} }
} else { } else {

View file

@ -36,9 +36,11 @@ val <T> T.euiccChannelManager: EuiccChannelManager where T: Fragment, T: EuiccCh
get() = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManager get() = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManager
val <T> T.euiccChannelManagerService: EuiccChannelManagerService where T: Fragment, T: EuiccChannelFragmentMarker val <T> T.euiccChannelManagerService: EuiccChannelManagerService where T: Fragment, T: EuiccChannelFragmentMarker
get() = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManagerService get() = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManagerService
val <T> T.channel: EuiccChannel where T: Fragment, T: EuiccChannelFragmentMarker
get() = suspend fun <T, R> T.withEuiccChannel(fn: suspend (EuiccChannel) -> R): R where T : Fragment, T : EuiccChannelFragmentMarker {
euiccChannelManager.findEuiccChannelByPortBlocking(slotId, portId)!! ensureEuiccChannelManager()
return euiccChannelManager.withEuiccChannel(slotId, portId, fn)
}
suspend fun <T> T.ensureEuiccChannelManager() where T: Fragment, T: EuiccChannelFragmentMarker = suspend fun <T> T.ensureEuiccChannelManager() where T: Fragment, T: EuiccChannelFragmentMarker =
(requireActivity() as BaseEuiccAccessActivity).euiccChannelManagerLoaded.await() (requireActivity() as BaseEuiccAccessActivity).euiccChannelManagerLoaded.await()

View file

@ -3,9 +3,6 @@ package im.angry.openeuicc.util
import android.util.Log import android.util.Log
import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.core.EuiccChannelManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import net.typeblog.lpac_jni.LocalProfileAssistant import net.typeblog.lpac_jni.LocalProfileAssistant
import net.typeblog.lpac_jni.LocalProfileInfo import net.typeblog.lpac_jni.LocalProfileInfo
@ -48,16 +45,21 @@ fun LocalProfileAssistant.disableActiveProfile(refresh: Boolean): Boolean =
} ?: true } ?: true
/** /**
* Disable the active profile, return a lambda that reverts this action when called. * Disable the current active profile if any. If refresh is true, also cause a refresh command.
* If refreshOnDisable is true, also cause a eUICC refresh command. Note that refreshing
* will disconnect the eUICC and might need some time before being operational again.
* See EuiccManager.waitForReconnect() * See EuiccManager.waitForReconnect()
*
* Return the iccid of the profile being disabled, or null if no active profile found or failed to
* disable.
*/ */
fun LocalProfileAssistant.disableActiveProfileWithUndo(refreshOnDisable: Boolean): () -> Unit = fun LocalProfileAssistant.disableActiveProfileKeepIccId(refresh: Boolean): String? =
profiles.find { it.isEnabled }?.let { profiles.find { it.isEnabled }?.let {
disableProfile(it.iccid, refreshOnDisable) Log.i(TAG, "Disabling active profile ${it.iccid}")
return { enableProfile(it.iccid) } if (disableProfile(it.iccid, refresh)) {
} ?: { } it.iccid
} else {
null
}
}
/** /**
* Begin a "tracked" operation where notifications may be generated by the eSIM * Begin a "tracked" operation where notifications may be generated by the eSIM
@ -78,60 +80,21 @@ suspend inline fun EuiccChannelManager.beginTrackedOperation(
portId: Int, portId: Int,
op: () -> Boolean op: () -> Boolean
) { ) {
val latestSeq = val latestSeq = withEuiccChannel(slotId, portId) { channel ->
findEuiccChannelByPort(slotId, portId)!!.lpa.notifications.firstOrNull()?.seqNumber channel.lpa.notifications.firstOrNull()?.seqNumber
?: 0 ?: 0
Log.d(TAG, "Latest notification is $latestSeq before operation")
if (op()) {
Log.d(TAG, "Operation has requested notification handling")
try {
// Note that the exact instance of "channel" might have changed here if reconnected;
// so we MUST use the automatic getter for "channel"
findEuiccChannelByPort(
slotId,
portId
)?.lpa?.notifications?.filter { it.seqNumber > latestSeq }?.forEach {
Log.d(TAG, "Handling notification $it")
findEuiccChannelByPort(
slotId,
portId
)?.lpa?.handleNotification(it.seqNumber)
}
} catch (e: Exception) {
// Ignore any error during notification handling
e.printStackTrace()
}
} }
Log.d(TAG, "Operation complete")
}
/**
* Same as beginTrackedOperation but uses blocking primitives.
* TODO: This function needs to be phased out of use.
*/
inline fun EuiccChannelManager.beginTrackedOperationBlocking(
slotId: Int,
portId: Int,
op: () -> Boolean
) {
val latestSeq =
findEuiccChannelByPortBlocking(slotId, portId)!!.lpa.notifications.firstOrNull()?.seqNumber
?: 0
Log.d(TAG, "Latest notification is $latestSeq before operation") Log.d(TAG, "Latest notification is $latestSeq before operation")
if (op()) { if (op()) {
Log.d(TAG, "Operation has requested notification handling") Log.d(TAG, "Operation has requested notification handling")
try { try {
// Note that the exact instance of "channel" might have changed here if reconnected; // Note that the exact instance of "channel" might have changed here if reconnected;
// so we MUST use the automatic getter for "channel" // this is why we need to use two distinct calls to withEuiccChannel()
findEuiccChannelByPortBlocking( withEuiccChannel(slotId, portId) { channel ->
slotId, channel.lpa.notifications.filter { it.seqNumber > latestSeq }.forEach {
portId Log.d(TAG, "Handling notification $it")
)?.lpa?.notifications?.filter { it.seqNumber > latestSeq }?.forEach { channel.lpa.handleNotification(it.seqNumber)
Log.d(TAG, "Handling notification $it") }
findEuiccChannelByPortBlocking(
slotId,
portId
)?.lpa?.handleNotification(it.seqNumber)
} }
} catch (e: Exception) { } catch (e: Exception) {
// Ignore any error during notification handling // Ignore any error during notification handling

View file

@ -45,6 +45,8 @@ fun SEService.getUiccReaderCompat(slotNumber: Int): Reader {
interface UiccCardInfoCompat { interface UiccCardInfoCompat {
val physicalSlotIndex: Int val physicalSlotIndex: Int
val ports: Collection<UiccPortInfoCompat> val ports: Collection<UiccPortInfoCompat>
val isRemovable: Boolean
get() = true // This defaults to removable unless overridden
} }
interface UiccPortInfoCompat { interface UiccPortInfoCompat {

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<include layout="@layout/toolbar_activity" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/toolbar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView
android:id="@+id/euicc_info_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginVertical="12dp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/euicc_info_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginVertical="12dp"
android:maxLines="1"
android:ellipsize="marquee"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/euicc_info_title"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -5,4 +5,9 @@
android:id="@+id/show_notifications" android:id="@+id/show_notifications"
android:title="@string/profile_notifications_show" android:title="@string/profile_notifications_show"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:id="@+id/euicc_info"
android:title="@string/euicc_info"
app:showAsAction="never" />
</menu> </menu>

View file

@ -8,6 +8,7 @@
<string name="channel_name_format">Logical Slot %d</string> <string name="channel_name_format">Logical Slot %d</string>
<string name="usb">USB</string> <string name="usb">USB</string>
<string name="omapi">OpenMobile API (OMAPI)</string>
<string name="enabled">Enabled</string> <string name="enabled">Enabled</string>
<string name="disabled">Disabled</string> <string name="disabled">Disabled</string>
@ -71,6 +72,25 @@
<string name="profile_notification_process">Process</string> <string name="profile_notification_process">Process</string>
<string name="profile_notification_delete">Delete</string> <string name="profile_notification_delete">Delete</string>
<string name="euicc_info">eUICC Info</string>
<string name="euicc_info_activity_title">eUICC Info (%s)</string>
<string name="euicc_info_access_mode">Access Mode</string>
<string name="euicc_info_removable">Removable</string>
<string name="euicc_info_eid">EID</string>
<string name="euicc_info_firmware_version">eUICC OS Version</string>
<string name="euicc_info_globalplatform_version">GlobalPlatform Version</string>
<string name="euicc_info_sas_accreditation_number">SAS Accreditation Number</string>
<string name="euicc_info_pp_version">Protected Profile Version</string>
<string name="euicc_info_free_nvram">Free NVRAM (eSIM profile storage)</string>
<string name="euicc_info_gsma_prod">GSMA Production Certificate</string>
<string name="euicc_info_gsma_test">GSMA Test Certificate</string>
<string name="supported">Supported</string>
<string name="unsupported">Unsupported</string>
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="logs_save">Save</string> <string name="logs_save">Save</string>
<string name="logs_filename_template">Logs at %s</string> <string name="logs_filename_template">Logs at %s</string>

View file

@ -22,9 +22,14 @@
<activity <activity
android:name="im.angry.openeuicc.ui.CompatibilityCheckActivity" android:name="im.angry.openeuicc.ui.CompatibilityCheckActivity"
android:label="@string/compatibility_check" android:exported="false"
android:exported="false" /> android:label="@string/compatibility_check" />
</application> </application>
<queries>
<package android:name="com.android.stk" />
<package android:name="com.android.stk1" />
<package android:name="com.android.stk2" />
</queries>
</manifest> </manifest>

View file

@ -1,9 +1,14 @@
package im.angry.openeuicc.di package im.angry.openeuicc.di
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import im.angry.openeuicc.ui.EuiccManagementFragment
import im.angry.openeuicc.ui.UnprivilegedEuiccManagementFragment
import im.angry.openeuicc.ui.UnprivilegedNoEuiccPlaceholderFragment import im.angry.openeuicc.ui.UnprivilegedNoEuiccPlaceholderFragment
class UnprivilegedUiComponentFactory : DefaultUiComponentFactory() { class UnprivilegedUiComponentFactory : DefaultUiComponentFactory() {
override fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment =
UnprivilegedEuiccManagementFragment.newInstance(slotId, portId)
override fun createNoEuiccPlaceholderFragment(): Fragment = override fun createNoEuiccPlaceholderFragment(): Fragment =
UnprivilegedNoEuiccPlaceholderFragment() UnprivilegedNoEuiccPlaceholderFragment()
} }

View file

@ -0,0 +1,31 @@
package im.angry.openeuicc.ui
import android.view.Menu
import android.view.MenuInflater
import im.angry.easyeuicc.R
import im.angry.openeuicc.util.SIMToolkit
import im.angry.openeuicc.util.newInstanceEuicc
import im.angry.openeuicc.util.slotId
class UnprivilegedEuiccManagementFragment : EuiccManagementFragment() {
companion object {
const val TAG = "UnprivilegedEuiccManagementFragment"
fun newInstance(slotId: Int, portId: Int): EuiccManagementFragment =
newInstanceEuicc(UnprivilegedEuiccManagementFragment::class.java, slotId, portId)
}
private val stk by lazy {
SIMToolkit(requireContext())
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.fragment_sim_toolkit, menu)
menu.findItem(R.id.open_sim_toolkit).apply {
isVisible = stk.isAvailable(slotId)
intent = stk.intent(slotId)
}
}
}

View file

@ -0,0 +1,64 @@
package im.angry.openeuicc.util
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import androidx.annotation.ArrayRes
import im.angry.easyeuicc.R
import im.angry.openeuicc.core.EuiccChannelManager
class SIMToolkit(private val context: Context) {
private val slotSelection = getComponentNames(R.array.sim_toolkit_slot_selection)
private val slots = buildMap {
put(0, getComponentNames(R.array.sim_toolkit_slot_1))
put(1, getComponentNames(R.array.sim_toolkit_slot_2))
}
private val packageNames = buildSet {
addAll(slotSelection.map { it.packageName })
addAll(slots.values.flatten().map { it.packageName })
}
private val activities = packageNames.flatMap(::getActivities).toSet()
private val launchIntent by lazy {
packageNames.firstNotNullOfOrNull(::getLaunchIntent)
}
private fun getLaunchIntent(packageName: String) = try {
val pm = context.packageManager
pm.getLaunchIntentForPackage(packageName)
} catch (_: PackageManager.NameNotFoundException) {
null
}
private fun getActivities(packageName: String) = try {
val pm = context.packageManager
val packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES)
packageInfo.activities!!.filter { it.exported }
.map { ComponentName(it.packageName, it.name) }
} catch (_: PackageManager.NameNotFoundException) {
emptyList()
}
private fun getComponentNames(@ArrayRes id: Int) =
context.resources.getStringArray(id).mapNotNull(ComponentName::unflattenFromString)
fun isAvailable(slotId: Int) = when (slotId) {
-1 -> false
EuiccChannelManager.USB_CHANNEL_ID -> false
else -> intent(slotId) != null
}
fun intent(slotId: Int): Intent? {
val components = slots.getOrDefault(slotId, emptySet()) + slotSelection
val intent = Intent(Intent.ACTION_MAIN, null).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
component = components.find(activities::contains)
addCategory(Intent.CATEGORY_LAUNCHER)
}
return if (intent.component != null) intent else launchIntent
}
}

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/open_sim_toolkit"
android:title="@string/open_sim_toolkit"
android:visible="false"
app:showAsAction="never" />
</menu>

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="Typos">
<string-array name="sim_toolkit_slot_selection">
<item>com.android.stk/.StkMain</item>
<item>com.android.stk/.StkMainHide</item>
<item>com.android.stk/.StkListActivity</item>
<item>com.android.stk/.StkLauncherListActivity</item>
</string-array>
<string-array name="sim_toolkit_slot_1">
<item>com.android.stk/.StkMain1</item>
<item>com.android.stk/.PrimaryStkMain</item>
<item>com.android.stk/.StkLauncherActivity</item>
<item>com.android.stk/.StkLauncherActivity_Chn</item>
<item>com.android.stk/.StkLauncherActivityI</item>
<item>com.android.stk/.OppoStkLauncherActivity1</item>
<item>com.android.stk/.OplusStkLauncherActivity1</item>
<item>com.android.stk/.mtk.StkLauncherActivityI</item>
</string-array>
<string-array name="sim_toolkit_slot_2">
<item>com.android.stk/.StkMain2</item>
<item>com.android.stk/.SecondaryStkMain</item>
<item>com.android.stk/.StkLauncherActivity2</item>
<item>com.android.stk/.StkLauncherActivityII</item>
<item>com.android.stk/.OppoStkLauncherActivity2</item>
<item>com.android.stk/.OplusStkLauncherActivity2</item>
<item>com.android.stk/.mtk.StkLauncherActivityII</item>
<item>com.android.stk1/.StkLauncherActivity</item>
<item>com.android.stk2/.StkLauncherActivity</item>
<item>com.android.stk2/.StkLauncherActivity_Chn</item>
<item>com.android.stk2/.StkLauncherActivity2</item>
</string-array>
</resources>

View file

@ -2,6 +2,7 @@
<string name="app_name" translatable="false">EasyEUICC</string> <string name="app_name" translatable="false">EasyEUICC</string>
<string name="channel_name_format">SIM %d</string> <string name="channel_name_format">SIM %d</string>
<string name="compatibility_check">Compatibility Check</string> <string name="compatibility_check">Compatibility Check</string>
<string name="open_sim_toolkit">Open SIM Toolkit</string>
<!-- Compatibility Check Descriptions --> <!-- Compatibility Check Descriptions -->
<string name="compatibility_check_system_features">System Features</string> <string name="compatibility_check_system_features">System Features</string>
@ -11,11 +12,11 @@
<string name="compatibility_check_omapi_connectivity">OMAPI Connectivity</string> <string name="compatibility_check_omapi_connectivity">OMAPI Connectivity</string>
<string name="compatibility_check_omapi_connectivity_desc">Does your device allow access to Secure Elements on SIM cards via OMAPI?</string> <string name="compatibility_check_omapi_connectivity_desc">Does your device allow access to Secure Elements on SIM cards via OMAPI?</string>
<string name="compatibility_check_omapi_connectivity_fail">Unable to detect Secure Element readers for SIM cards via OMAPI. If you have not inserted a SIM in this device, try inserting one and retry this check.</string> <string name="compatibility_check_omapi_connectivity_fail">Unable to detect Secure Element readers for SIM cards via OMAPI. If you have not inserted a SIM in this device, try inserting one and retry this check.</string>
<string name="compatibility_check_omapi_connectivity_partial_success_sim_number">Successfully detected Secure Element access, but only for the following SIM slots: %s.</string> <string name="compatibility_check_omapi_connectivity_partial_success_sim_number">Successfully detected Secure Element access, but only for the following SIM slots: <b>SIM%s</b>.</string>
<string name="compatibility_check_isdr_channel">ISD-R Channel Access</string> <string name="compatibility_check_isdr_channel">ISD-R Channel Access</string>
<string name="compatibility_check_isdr_channel_desc">Does your device support opening an ISD-R (management) channel to eSIMs via OMAPI?</string> <string name="compatibility_check_isdr_channel_desc">Does your device support opening an ISD-R (management) channel to eSIMs via OMAPI?</string>
<string name="compatibility_check_isdr_channel_desc_unknown">Cannot determine whether ISD-R access through OMAPI is supported. You might want to retry with SIM cards inserted (any SIM card will do) if not already.</string> <string name="compatibility_check_isdr_channel_desc_unknown">Cannot determine whether ISD-R access through OMAPI is supported. You might want to retry with SIM cards inserted (any SIM card will do) if not already.</string>
<string name="compatibility_check_isdr_channel_desc_partial_fail">OMAPI access to ISD-R is only possible on the following SIM slots: %s.</string> <string name="compatibility_check_isdr_channel_desc_partial_fail">OMAPI access to ISD-R is only possible on the following SIM slots: <b>SIM%s</b>.</string>
<string name="compatibility_check_known_broken">Not on the Known Broken List</string> <string name="compatibility_check_known_broken">Not on the Known Broken List</string>
<string name="compatibility_check_known_broken_desc">Making sure your device is not known to have bugs associated with removable eSIMs.</string> <string name="compatibility_check_known_broken_desc">Making sure your device is not known to have bugs associated with removable eSIMs.</string>
<string name="compatibility_check_known_broken_fail">Oops, your device is known to have bugs when accessing removable eSIMs. This does not necessarily mean that it will not work at all, but you will have to proceed with caution.</string> <string name="compatibility_check_known_broken_fail">Oops, your device is known to have bugs when accessing removable eSIMs. This does not necessarily mean that it will not work at all, but you will have to proceed with caution.</string>

View file

@ -3,6 +3,7 @@ package im.angry.openeuicc.core
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import im.angry.openeuicc.OpenEuiccApplication import im.angry.openeuicc.OpenEuiccApplication
import im.angry.openeuicc.R
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import java.lang.IllegalArgumentException import java.lang.IllegalArgumentException
@ -26,7 +27,8 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto
"Trying TelephonyManager for slot ${port.card.physicalSlotIndex} port ${port.portIndex}" "Trying TelephonyManager for slot ${port.card.physicalSlotIndex} port ${port.portIndex}"
) )
try { try {
return EuiccChannel( return EuiccChannelImpl(
context.getString(R.string.telephony_manager),
port, port,
TelephonyManagerApduInterface( TelephonyManagerApduInterface(
port, port,

View file

@ -28,9 +28,9 @@ class PrivilegedEuiccChannelManager(
} }
} }
override fun notifyEuiccProfilesChanged(logicalSlotId: Int) { override suspend fun notifyEuiccProfilesChanged(logicalSlotId: Int) {
appContainer.subscriptionManager.apply { appContainer.subscriptionManager.apply {
findEuiccChannelBySlotBlocking(logicalSlotId)?.let { findEuiccChannelByLogicalSlot(logicalSlotId)?.let {
tryRefreshCachedEuiccInfo(it.cardId) tryRefreshCachedEuiccInfo(it.cardId)
} }
} }

View file

@ -1,10 +1,9 @@
package im.angry.openeuicc.di package im.angry.openeuicc.di
import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.ui.EuiccManagementFragment import im.angry.openeuicc.ui.EuiccManagementFragment
import im.angry.openeuicc.ui.PrivilegedEuiccManagementFragment import im.angry.openeuicc.ui.PrivilegedEuiccManagementFragment
class PrivilegedUiComponentFactory : DefaultUiComponentFactory() { class PrivilegedUiComponentFactory : DefaultUiComponentFactory() {
override fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment = override fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment =
PrivilegedEuiccManagementFragment.newInstance(channel.slotId, channel.portId) PrivilegedEuiccManagementFragment.newInstance(slotId, portId)
} }

View file

@ -9,12 +9,12 @@ import android.telephony.euicc.DownloadableSubscription
import android.telephony.euicc.EuiccInfo import android.telephony.euicc.EuiccInfo
import android.util.Log import android.util.Log
import net.typeblog.lpac_jni.LocalProfileInfo import net.typeblog.lpac_jni.LocalProfileInfo
import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.flow.first import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.lang.IllegalStateException import kotlin.IllegalStateException
class OpenEuiccService : EuiccService(), OpenEuiccContextMarker { class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
companion object { companion object {
@ -37,16 +37,10 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
} }
private data class EuiccChannelManagerContext( private data class EuiccChannelManagerContext(
val euiccChannelManager: EuiccChannelManager val euiccChannelManagerService: EuiccChannelManagerService
) { ) {
fun findChannel(physicalSlotId: Int): EuiccChannel? = val euiccChannelManager
euiccChannelManager.findEuiccChannelByPhysicalSlotBlocking(physicalSlotId) get() = euiccChannelManagerService.euiccChannelManager
fun findChannel(slotId: Int, portId: Int): EuiccChannel? =
euiccChannelManager.findEuiccChannelByPortBlocking(slotId, portId)
fun findAllChannels(physicalSlotId: Int): List<EuiccChannel>? =
euiccChannelManager.findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId)
} }
/** /**
@ -59,7 +53,7 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
* *
* This function cannot be inline because non-local returns may bypass the unbind * This function cannot be inline because non-local returns may bypass the unbind
*/ */
private fun <T> withEuiccChannelManager(fn: EuiccChannelManagerContext.() -> T): T { private fun <T> withEuiccChannelManager(fn: suspend EuiccChannelManagerContext.() -> T): T {
val (binder, unbind) = runBlocking { val (binder, unbind) = runBlocking {
bindServiceSuspended( bindServiceSuspended(
Intent( Intent(
@ -73,23 +67,24 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
throw RuntimeException("Unable to bind to EuiccChannelManagerService; aborting") throw RuntimeException("Unable to bind to EuiccChannelManagerService; aborting")
} }
val ret = val localBinder = binder as EuiccChannelManagerService.LocalBinder
EuiccChannelManagerContext((binder as EuiccChannelManagerService.LocalBinder).service.euiccChannelManager).fn()
val ret = runBlocking {
EuiccChannelManagerContext(localBinder.service).fn()
}
unbind() unbind()
return ret return ret
} }
override fun onGetEid(slotId: Int): String? = withEuiccChannelManager { override fun onGetEid(slotId: Int): String? = withEuiccChannelManager {
findChannel(slotId)?.lpa?.eID val portId = euiccChannelManager.findFirstAvailablePort(slotId)
if (portId < 0) return@withEuiccChannelManager null
euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
channel.lpa.eID
}
} }
// When two eSIM cards are present on one device, the Android settings UI
// gets confused and sets the incorrect slotId for profiles from one of
// the cards. This function helps Detect this case and abort early.
private fun EuiccChannel.profileExists(iccid: String?) =
lpa.profiles.any { it.iccid == iccid }
private fun ensurePortIsMapped(slotId: Int, portId: Int) { private fun ensurePortIsMapped(slotId: Int, portId: Int) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
return return
@ -121,7 +116,11 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
telephonyManager.simSlotMapping = mappings.reversed() telephonyManager.simSlotMapping = mappings.reversed()
} }
private fun <T> retryWithTimeout(timeoutMillis: Int, backoff: Int = 1000, f: () -> T?): T? { private suspend fun <T> retryWithTimeout(
timeoutMillis: Int,
backoff: Int = 1000,
f: suspend () -> T?
): T? {
val startTimeMillis = System.currentTimeMillis() val startTimeMillis = System.currentTimeMillis()
do { do {
try { try {
@ -129,7 +128,7 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
} catch (_: Exception) { } catch (_: Exception) {
// Ignore // Ignore
} finally { } finally {
Thread.sleep(backoff.toLong()) delay(backoff.toLong())
} }
} while (System.currentTimeMillis() - startTimeMillis < timeoutMillis) } while (System.currentTimeMillis() - startTimeMillis < timeoutMillis)
return null return null
@ -177,38 +176,54 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
} }
// TODO: Temporarily enable the slot to access its profiles if it is currently unmapped // TODO: Temporarily enable the slot to access its profiles if it is currently unmapped
val channel = val port = euiccChannelManager.findFirstAvailablePort(slotId)
findChannel(slotId) ?: return@withEuiccChannelManager GetEuiccProfileInfoListResult( if (port == -1) {
return@withEuiccChannelManager GetEuiccProfileInfoListResult(
RESULT_FIRST_USER, RESULT_FIRST_USER,
arrayOf(), arrayOf(),
true true
) )
val profiles = channel.lpa.profiles.operational.map {
EuiccProfileInfo.Builder(it.iccid).apply {
setProfileName(it.name)
setNickname(it.displayName)
setServiceProviderName(it.providerName)
setState(
when (it.state) {
LocalProfileInfo.State.Enabled -> EuiccProfileInfo.PROFILE_STATE_ENABLED
LocalProfileInfo.State.Disabled -> EuiccProfileInfo.PROFILE_STATE_DISABLED
}
)
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
}
)
}.build()
} }
return@withEuiccChannelManager GetEuiccProfileInfoListResult( try {
RESULT_OK, return@withEuiccChannelManager euiccChannelManager.withEuiccChannel(
profiles.toTypedArray(), slotId,
channel.removable port
) ) { channel ->
val profiles = channel.lpa.profiles.operational.map {
EuiccProfileInfo.Builder(it.iccid).apply {
setProfileName(it.name)
setNickname(it.displayName)
setServiceProviderName(it.providerName)
setState(
when (it.state) {
LocalProfileInfo.State.Enabled -> EuiccProfileInfo.PROFILE_STATE_ENABLED
LocalProfileInfo.State.Disabled -> EuiccProfileInfo.PROFILE_STATE_DISABLED
}
)
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
}
)
}.build()
}
GetEuiccProfileInfoListResult(
RESULT_OK,
profiles.toTypedArray(),
channel.port.card.isRemovable
)
}
} catch (e: EuiccChannelManager.EuiccChannelNotFoundException) {
return@withEuiccChannelManager GetEuiccProfileInfoListResult(
RESULT_FIRST_USER,
arrayOf(),
true
)
}
} }
override fun onGetEuiccInfo(slotId: Int): EuiccInfo { override fun onGetEuiccInfo(slotId: Int): EuiccInfo {
@ -219,39 +234,30 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
Log.i(TAG, "onDeleteSubscription slotId=$slotId iccid=$iccid") Log.i(TAG, "onDeleteSubscription slotId=$slotId iccid=$iccid")
if (shouldIgnoreSlot(slotId)) return@withEuiccChannelManager RESULT_FIRST_USER if (shouldIgnoreSlot(slotId)) return@withEuiccChannelManager RESULT_FIRST_USER
try { val ports = euiccChannelManager.findAvailablePorts(slotId)
val channels = if (ports.isEmpty()) return@withEuiccChannelManager RESULT_FIRST_USER
findAllChannels(slotId) ?: return@withEuiccChannelManager RESULT_FIRST_USER
if (!channels[0].profileExists(iccid)) { // Check that the profile has been disabled on all slots
return@withEuiccChannelManager RESULT_FIRST_USER val enabledAnywhere = ports.any { port ->
} euiccChannelManager.withEuiccChannel(slotId, port) { channel ->
// If the profile is enabled by ANY channel (port), we cannot delete it
channels.forEach { channel ->
val profile = channel.lpa.profiles.find { val profile = channel.lpa.profiles.find {
it.iccid == iccid it.iccid == iccid
} ?: return@withEuiccChannelManager RESULT_FIRST_USER } ?: return@withEuiccChannel false
if (profile.state == LocalProfileInfo.State.Enabled) { profile.state == LocalProfileInfo.State.Enabled
// Must disable the profile first
return@withEuiccChannelManager RESULT_FIRST_USER
}
} }
}
euiccChannelManager.beginTrackedOperationBlocking(channels[0].slotId, channels[0].portId) { if (enabledAnywhere) return@withEuiccChannelManager RESULT_FIRST_USER
if (channels[0].lpa.deleteProfile(iccid)) {
return@withEuiccChannelManager RESULT_OK
}
runBlocking { euiccChannelManagerService.waitForForegroundTask()
preferenceRepository.notificationDeleteFlow.first() val success = euiccChannelManagerService.launchProfileDeleteTask(slotId, ports[0], iccid)
} .waitDone() == null
}
return@withEuiccChannelManager RESULT_FIRST_USER return@withEuiccChannelManager if (success) {
} catch (e: Exception) { RESULT_OK
return@withEuiccChannelManager RESULT_FIRST_USER } else {
RESULT_FIRST_USER
} }
} }
@ -274,52 +280,91 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
if (shouldIgnoreSlot(slotId)) return@withEuiccChannelManager RESULT_FIRST_USER if (shouldIgnoreSlot(slotId)) return@withEuiccChannelManager RESULT_FIRST_USER
try { try {
// First, try to find a pair of slotId and portId we can use for the switching operation
// retryWithTimeout is needed here because this function may be called just after // retryWithTimeout is needed here because this function may be called just after
// AOSP has switched slot mappings, in which case the slots may not be ready yet. // AOSP has switched slot mappings, in which case the slots may not be ready yet.
val channel = if (portIndex == -1) { val (foundSlotId, foundPortId) = retryWithTimeout(5000) {
retryWithTimeout(5000) { findChannel(slotId) } if (portIndex == -1) {
} else { // If port is not indicated, we can use any port
retryWithTimeout(5000) { findChannel(slotId, portIndex) } val port = euiccChannelManager.findFirstAvailablePort(slotId).let {
if (it < 0) {
throw IllegalStateException("No mapped port available; may need to try again")
}
it
}
Pair(slotId, port)
} else {
// Else, check until the indicated port is available
euiccChannelManager.withEuiccChannel(slotId, portIndex) { channel ->
if (!channel.valid) {
throw IllegalStateException("Indicated slot / port combination is unavailable; may need to try again")
}
}
Pair(slotId, portIndex)
}
} ?: run { } ?: run {
// Failure case: mapped slots / ports aren't usable per constraints
// If we can't find a usable slot / port already mapped, and we aren't allowed to
// deactivate a SIM, we can only abort
if (!forceDeactivateSim) { if (!forceDeactivateSim) {
// The user must select which SIM to deactivate
return@withEuiccChannelManager RESULT_MUST_DEACTIVATE_SIM return@withEuiccChannelManager RESULT_MUST_DEACTIVATE_SIM
}
// If port ID is not indicated, we just try to map port 0
// This is because in order to get here, we have to have failed findFirstAvailablePort(),
// which means no eUICC port is mapped or connected properly whatsoever.
val foundPortId = if (portIndex == -1) {
0
} else { } else {
try { portIndex
// If we are allowed to deactivate any SIM we like, try mapping the indicated port first
ensurePortIsMapped(slotId, portIndex)
retryWithTimeout(5000) { findChannel(slotId, portIndex) }
} catch (e: Exception) {
// We cannot map the port (or it is already mapped)
// but we can also use any port available on the card
retryWithTimeout(5000) { findChannel(slotId) }
} ?: return@withEuiccChannelManager RESULT_FIRST_USER
} }
}
if (iccid != null && !channel.profileExists(iccid)) { // Now we can try to map an unused port
Log.i(TAG, "onSwitchToSubscriptionWithPort iccid=$iccid not found") try {
return@withEuiccChannelManager RESULT_FIRST_USER ensurePortIsMapped(slotId, foundPortId)
} } catch (_: Exception) {
return@withEuiccChannelManager RESULT_FIRST_USER
}
euiccChannelManager.beginTrackedOperationBlocking(channel.slotId, channel.portId) { // Wait for availability again
if (iccid != null) { retryWithTimeout(5000) {
// Disable any active profile first if present euiccChannelManager.withEuiccChannel(slotId, foundPortId) { channel ->
channel.lpa.disableActiveProfile(false) if (!channel.valid) {
if (!channel.lpa.enableProfile(iccid)) { throw IllegalStateException("Indicated slot / port combination is unavailable; may need to try again")
return@withEuiccChannelManager RESULT_FIRST_USER }
} }
} else { } ?: return@withEuiccChannelManager RESULT_FIRST_USER
if (!channel.lpa.disableActiveProfile(true)) {
return@withEuiccChannelManager RESULT_FIRST_USER
}
}
runBlocking { Pair(slotId, foundPortId)
preferenceRepository.notificationSwitchFlow.first()
}
} }
Log.i(TAG, "Found slotId=$foundSlotId, portId=$foundPortId for switching")
// Now, figure out what they want us to do: disabling a profile, or enabling a new one?
val (foundIccid, enable) = if (iccid == null) {
// iccid == null means disabling
val foundIccid =
euiccChannelManager.withEuiccChannel(foundSlotId, foundPortId) { channel ->
channel.lpa.profiles.find { it.state == LocalProfileInfo.State.Enabled }
}?.iccid ?: return@withEuiccChannelManager RESULT_FIRST_USER
Pair(foundIccid, false)
} else {
Pair(iccid, true)
}
val res = euiccChannelManagerService.launchProfileSwitchTask(
foundSlotId,
foundPortId,
foundIccid,
enable,
30 * 1000
).waitDone()
if (res != null) return@withEuiccChannelManager RESULT_FIRST_USER
return@withEuiccChannelManager RESULT_OK return@withEuiccChannelManager RESULT_OK
} catch (e: Exception) { } catch (e: Exception) {
return@withEuiccChannelManager RESULT_FIRST_USER return@withEuiccChannelManager RESULT_FIRST_USER
@ -335,13 +380,19 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
"onUpdateSubscriptionNickname slotId=$slotId iccid=$iccid nickname=$nickname" "onUpdateSubscriptionNickname slotId=$slotId iccid=$iccid nickname=$nickname"
) )
if (shouldIgnoreSlot(slotId)) return@withEuiccChannelManager RESULT_FIRST_USER if (shouldIgnoreSlot(slotId)) return@withEuiccChannelManager RESULT_FIRST_USER
val channel = findChannel(slotId) ?: return@withEuiccChannelManager RESULT_FIRST_USER val port = euiccChannelManager.findFirstAvailablePort(slotId)
if (!channel.profileExists(iccid)) { if (port < 0) {
return@withEuiccChannelManager RESULT_FIRST_USER return@withEuiccChannelManager RESULT_FIRST_USER
} }
val success = channel.lpa
.setNickname(iccid, nickname!!) euiccChannelManagerService.waitForForegroundTask()
appContainer.subscriptionManager.tryRefreshCachedEuiccInfo(channel.cardId) val success =
(euiccChannelManagerService.launchProfileRenameTask(slotId, port, iccid, nickname!!)
.waitDone()) == null
euiccChannelManager.withEuiccChannel(slotId, port) { channel ->
appContainer.subscriptionManager.tryRefreshCachedEuiccInfo(channel.cardId)
}
return@withEuiccChannelManager if (success) { return@withEuiccChannelManager if (success) {
RESULT_OK RESULT_OK
} else { } else {

View file

@ -6,8 +6,6 @@ import android.widget.Button
import android.widget.PopupMenu import android.widget.PopupMenu
import im.angry.openeuicc.R import im.angry.openeuicc.R
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import net.typeblog.lpac_jni.LocalProfileInfo import net.typeblog.lpac_jni.LocalProfileInfo
class PrivilegedEuiccManagementFragment: EuiccManagementFragment() { class PrivilegedEuiccManagementFragment: EuiccManagementFragment() {
@ -16,14 +14,23 @@ class PrivilegedEuiccManagementFragment: EuiccManagementFragment() {
newInstanceEuicc(PrivilegedEuiccManagementFragment::class.java, slotId, portId) newInstanceEuicc(PrivilegedEuiccManagementFragment::class.java, slotId, portId)
} }
private var isMEP = false
private var isRemovable = false
override suspend fun doRefresh() {
super.doRefresh()
withEuiccChannel { channel ->
isMEP = channel.isMEP
isRemovable = channel.port.card.isRemovable
}
}
override suspend fun onCreateFooterViews( override suspend fun onCreateFooterViews(
parent: ViewGroup, parent: ViewGroup,
profiles: List<LocalProfileInfo> profiles: List<LocalProfileInfo>
): List<View> = ): List<View> =
super.onCreateFooterViews(parent, profiles).let { footers -> super.onCreateFooterViews(parent, profiles).let { footers ->
// isMEP can map to a slow operation (UiccCardInfo.isMultipleEnabledProfilesSupported()) if (isMEP) {
// so let's do it in the IO context
if (withContext(Dispatchers.IO) { channel.isMEP }) {
val view = layoutInflater.inflate(R.layout.footer_mep, parent, false) val view = layoutInflater.inflate(R.layout.footer_mep, parent, false)
view.requireViewById<Button>(R.id.footer_mep_slot_mapping).setOnClickListener { view.requireViewById<Button>(R.id.footer_mep_slot_mapping).setOnClickListener {
(requireActivity() as PrivilegedMainActivity).showSlotMappingFragment() (requireActivity() as PrivilegedMainActivity).showSlotMappingFragment()
@ -36,7 +43,7 @@ class PrivilegedEuiccManagementFragment: EuiccManagementFragment() {
override fun populatePopupWithProfileActions(popup: PopupMenu, profile: LocalProfileInfo) { override fun populatePopupWithProfileActions(popup: PopupMenu, profile: LocalProfileInfo) {
super.populatePopupWithProfileActions(popup, profile) super.populatePopupWithProfileActions(popup, profile)
if (profile.isEnabled && !channel.removable) { if (profile.isEnabled && !isRemovable) {
// Only show the disable option for non-removable eUICCs // Only show the disable option for non-removable eUICCs
// Some devices without internal eUICCs have the "optimization" of ignoring SIM // Some devices without internal eUICCs have the "optimization" of ignoring SIM
// slots without a valid profile. This can lead to "bricking" of external eUICCs // slots without a valid profile. This can lead to "bricking" of external eUICCs

View file

@ -33,7 +33,7 @@ class RealUiccCardInfoCompat(val inner: UiccCardInfo): UiccCardInfoCompat {
val isEuicc: Boolean val isEuicc: Boolean
get() = inner.isEuicc get() = inner.isEuicc
val isRemovable: Boolean override val isRemovable: Boolean
get() = inner.isRemovable get() = inner.isRemovable
val isMultipleEnabledProfilesSupported: Boolean val isMultipleEnabledProfilesSupported: Boolean

View file

@ -5,6 +5,7 @@ import android.telephony.TelephonyManager
import android.telephony.UiccSlotMapping import android.telephony.UiccSlotMapping
import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.core.EuiccChannelManager
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.lang.Exception import java.lang.Exception
@ -15,14 +16,14 @@ val TelephonyManager.dsdsEnabled: Boolean
get() = activeModemCount >= 2 get() = activeModemCount >= 2
fun TelephonyManager.setDsdsEnabled(euiccManager: EuiccChannelManager, enabled: Boolean) { fun TelephonyManager.setDsdsEnabled(euiccManager: EuiccChannelManager, enabled: Boolean) {
val knownChannels = runBlocking {
euiccManager.enumerateEuiccChannels()
}
// Disable all eSIM profiles before performing a DSDS switch (only for internal eSIMs) // Disable all eSIM profiles before performing a DSDS switch (only for internal eSIMs)
knownChannels.forEach { runBlocking {
if (!it.removable) { euiccManager.flowEuiccPorts().onEach { (slotId, portId) ->
it.lpa.disableActiveProfileWithUndo(false) euiccManager.withEuiccChannel(slotId, portId) {
if (!it.port.card.isRemovable) {
it.lpa.disableActiveProfile(false)
}
}
} }
} }
@ -31,7 +32,7 @@ fun TelephonyManager.setDsdsEnabled(euiccManager: EuiccChannelManager, enabled:
// Disable eSIM profiles before switching the slot mapping // Disable eSIM profiles before switching the slot mapping
// This ensures that unmapped eSIM ports never have "ghost" profiles enabled // This ensures that unmapped eSIM ports never have "ghost" profiles enabled
fun TelephonyManager.updateSimSlotMapping( suspend fun TelephonyManager.updateSimSlotMapping(
euiccManager: EuiccChannelManager, newMapping: Collection<UiccSlotMapping>, euiccManager: EuiccChannelManager, newMapping: Collection<UiccSlotMapping>,
currentMapping: Collection<UiccSlotMapping> = simSlotMapping currentMapping: Collection<UiccSlotMapping> = simSlotMapping
) { ) {
@ -42,14 +43,24 @@ fun TelephonyManager.updateSimSlotMapping(
} }
} }
val undo = unmapped.mapNotNull { mapping -> val undo: List<suspend () -> Unit> = unmapped.mapNotNull { mapping ->
euiccManager.findEuiccChannelByPortBlocking(mapping.physicalSlotIndex, mapping.portIndex)?.let { channel -> euiccManager.withEuiccChannel(mapping.physicalSlotIndex, mapping.portIndex) { channel ->
if (!channel.removable) { if (!channel.port.card.isRemovable) {
return@mapNotNull channel.lpa.disableActiveProfileWithUndo(false) channel.lpa.disableActiveProfileKeepIccId(false)
} else { } else {
// Do not do anything for external eUICCs -- we can't really trust them to work properly // Do not do anything for external eUICCs -- we can't really trust them to work properly
// with no profile enabled. // with no profile enabled.
return@mapNotNull null null
}
}?.let { iccid ->
// Generate undo closure because we can't keep reference to `channel` in the closure above
{
euiccManager.withEuiccChannel(
mapping.physicalSlotIndex,
mapping.portIndex
) { channel ->
channel.lpa.enableProfile(iccid)
}
} }
} }
} }
@ -75,9 +86,6 @@ fun SubscriptionManager.tryRefreshCachedEuiccInfo(cardId: Int) {
// Every EuiccChannel we use here should be backed by a RealUiccPortInfoCompat // Every EuiccChannel we use here should be backed by a RealUiccPortInfoCompat
// except when it is from a USB card reader // except when it is from a USB card reader
val EuiccChannel.removable
get() = (port as? RealUiccPortInfoCompat)?.card?.isRemovable ?: true
val EuiccChannel.cardId val EuiccChannel.cardId
get() = (port as? RealUiccPortInfoCompat)?.card?.cardId ?: -1 get() = (port as? RealUiccPortInfoCompat)?.card?.cardId ?: -1

View file

@ -1,6 +1,7 @@
<resources> <resources>
<string name="app_name">OpenEUICC</string> <string name="app_name">OpenEUICC</string>
<string name="no_euicc">No eUICC found on this device.\nOn some devices, you may need to enable dual SIM first in the menu of this app.</string> <string name="no_euicc">No eUICC found on this device.\nOn some devices, you may need to enable dual SIM first in the menu of this app.</string>
<string name="telephony_manager">TelephonyManager (Privileged)</string>
<string name="dsds">Dual SIM</string> <string name="dsds">Dual SIM</string>

View file

@ -7,6 +7,9 @@ import java.security.cert.CertificateFactory
const val DEFAULT_PKID_GSMA_RSP2_ROOT_CI1 = "81370f5125d0b1d408d4c3b232e6d25e795bebfb" const val DEFAULT_PKID_GSMA_RSP2_ROOT_CI1 = "81370f5125d0b1d408d4c3b232e6d25e795bebfb"
val PKID_GSMA_TEST_CI =
arrayOf("34eecf13156518d48d30bdf06853404d115f955d", "2209f61cd9ec5c9c854e787341ff83ecf9776a5b")
private fun getCertificate(keyId: String): Certificate? = private fun getCertificate(keyId: String): Certificate? =
KNOWN_CI_CERTS[keyId]?.toByteArray()?.let { cert -> KNOWN_CI_CERTS[keyId]?.toByteArray()?.let { cert ->
ByteArrayInputStream(cert).use { stream -> ByteArrayInputStream(cert).use { stream ->
@ -35,7 +38,7 @@ internal fun keyIdToKeystore(keyIds: Array<String>): KeyStore {
return ret return ret
} }
// ref: <https://euicc-manual.septs.app/docs/pki/ci/> // ref: <https://euicc-manual.osmocom.org/docs/pki/ci/>
internal val KNOWN_CI_CERTS = hashMapOf( internal val KNOWN_CI_CERTS = hashMapOf(
// GSM Association - RSP2 Root CI1 (CA: DigiCert) // GSM Association - RSP2 Root CI1 (CA: DigiCert)
// Specs: SGP.21 and SGP.22 version 2 and version 3 // Specs: SGP.21 and SGP.22 version 2 and version 3