diff --git a/app-common/src/main/AndroidManifest.xml b/app-common/src/main/AndroidManifest.xml index 11f16a6..aa301cd 100644 --- a/app-common/src/main/AndroidManifest.xml +++ b/app-common/src/main/AndroidManifest.xml @@ -24,10 +24,6 @@ android:label="@string/profile_download" android:theme="@style/Theme.AppCompat.Translucent" /> - - diff --git a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt index 410cccc..7f3abb0 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt @@ -6,7 +6,6 @@ import android.hardware.usb.UsbInterface import android.hardware.usb.UsbManager import android.se.omapi.SEService import android.util.Log -import im.angry.openeuicc.common.R import im.angry.openeuicc.core.usb.UsbApduInterface import im.angry.openeuicc.core.usb.getIoEndpoints import im.angry.openeuicc.util.* @@ -34,8 +33,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha Log.i(DefaultEuiccChannelManager.TAG, "Trying OMAPI for physical slot ${port.card.physicalSlotIndex}") try { - return EuiccChannelImpl( - context.getString(R.string.omapi), + return EuiccChannel( port, OmapiApduInterface( seService!!, @@ -63,8 +61,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha if (bulkIn == null || bulkOut == null) return null val conn = usbManager.openDevice(usbDevice) ?: return null if (!conn.claimInterface(usbInterface, true)) return null - return EuiccChannelImpl( - context.getString(R.string.usb), + return EuiccChannel( FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)), UsbApduInterface( conn, diff --git a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt index c75af40..55fb53d 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt @@ -10,9 +10,6 @@ import im.angry.openeuicc.di.AppContainer import im.angry.openeuicc.util.* import kotlinx.coroutines.Dispatchers 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.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -91,24 +88,44 @@ open class DefaultEuiccChannelManager( } } - protected suspend fun findEuiccChannelByLogicalSlot(logicalSlotId: Int): EuiccChannel? = - withContext(Dispatchers.IO) { - if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { - return@withContext usbChannel - } + override fun findEuiccChannelBySlotBlocking(logicalSlotId: Int): EuiccChannel? = + runBlocking { + withContext(Dispatchers.IO) { + if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { + return@withContext usbChannel + } - for (card in uiccCards) { - for (port in card.ports) { - if (port.logicalSlotIndex == logicalSlotId) { - return@withContext tryOpenEuiccChannel(port) + for (card in uiccCards) { + for (port in card.ports) { + if (port.logicalSlotIndex == logicalSlotId) { + return@withContext tryOpenEuiccChannel(port) + } } } - } - null + null + } } - private suspend fun findAllEuiccChannelsByPhysicalSlot(physicalSlotId: Int): List? { + override fun findEuiccChannelByPhysicalSlotBlocking(physicalSlotId: Int): 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? { if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { return usbChannel?.let { listOf(it) } } @@ -121,7 +138,12 @@ open class DefaultEuiccChannelManager( return null } - private suspend fun findEuiccChannelByPort(physicalSlotId: Int, portId: Int): EuiccChannel? = + override fun findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId: Int): List? = + runBlocking { + findAllEuiccChannelsByPhysicalSlot(physicalSlotId) + } + + override suspend fun findEuiccChannelByPort(physicalSlotId: Int, portId: Int): EuiccChannel? = withContext(Dispatchers.IO) { if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { return@withContext usbChannel @@ -132,57 +154,11 @@ open class DefaultEuiccChannelManager( } } - override suspend fun findFirstAvailablePort(physicalSlotId: Int): Int = - withContext(Dispatchers.IO) { - if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { - return@withContext 0 - } - - findAllEuiccChannelsByPhysicalSlot(physicalSlotId)?.getOrNull(0)?.portId ?: -1 + override fun findEuiccChannelByPortBlocking(physicalSlotId: Int, portId: Int): EuiccChannel? = + runBlocking { + findEuiccChannelByPort(physicalSlotId, portId) } - override suspend fun findAvailablePorts(physicalSlotId: Int): List = - withContext(Dispatchers.IO) { - if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { - return@withContext listOf(0) - } - - findAllEuiccChannelsByPhysicalSlot(physicalSlotId)?.map { it.portId } ?: listOf() - } - - override suspend fun 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 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) { if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) return @@ -208,35 +184,34 @@ open class DefaultEuiccChannelManager( } } - override fun flowEuiccPorts(): Flow> = flow { - uiccCards.forEach { info -> - info.ports.forEach { port -> - tryOpenEuiccChannel(port)?.also { - Log.d( - TAG, - "Found eUICC on slot ${info.physicalSlotIndex} port ${port.portIndex}" - ) - - emit(Pair(info.physicalSlotIndex, port.portIndex)) + override suspend fun enumerateEuiccChannels(): List = + withContext(Dispatchers.IO) { + uiccCards.flatMap { info -> + info.ports.mapNotNull { port -> + tryOpenEuiccChannel(port)?.also { + Log.d( + TAG, + "Found eUICC on slot ${info.physicalSlotIndex} port ${port.portIndex}" + ) + } } } } - }.flowOn(Dispatchers.IO) - override suspend fun tryOpenUsbEuiccChannel(): Pair = + override suspend fun enumerateUsbEuiccChannel(): Pair = withContext(Dispatchers.IO) { usbManager.deviceList.values.forEach { device -> Log.i(TAG, "Scanning USB device ${device.deviceId}:${device.vendorId}") val iface = device.getSmartCardInterface() ?: return@forEach // If we don't have permission, tell UI code that we found a candidate device, but we // need permission to be able to do anything with it - if (!usbManager.hasPermission(device)) return@withContext Pair(device, false) + if (!usbManager.hasPermission(device)) return@withContext Pair(device, null) Log.i(TAG, "Found CCID interface on ${device.deviceId}:${device.vendorId}, and has permission; trying to open channel") try { val channel = euiccChannelFactory.tryOpenUsbEuiccChannel(device, iface) if (channel != null && channel.lpa.valid) { usbChannel = channel - return@withContext Pair(device, true) + return@withContext Pair(device, channel) } } catch (e: Exception) { // Ignored -- skip forward @@ -244,7 +219,7 @@ open class DefaultEuiccChannelManager( } Log.i(TAG, "No valid eUICC channel found on USB device ${device.deviceId}:${device.vendorId}") } - return@withContext Pair(null, false) + return@withContext Pair(null, null) } override fun invalidate() { diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt index 4ef6808..e106535 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt @@ -1,20 +1,26 @@ 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 -interface EuiccChannel { - val type: String +class EuiccChannel( + val port: UiccPortInfoCompat, + apduInterface: ApduInterface, + verboseLoggingFlow: Flow +) { + val slotId = port.card.physicalSlotIndex // PHYSICAL slot + val logicalSlotId = port.logicalSlotIndex + val portId = port.portIndex - val port: UiccPortInfoCompat - - val slotId: Int // PHYSICAL slot - val logicalSlotId: Int - val portId: Int - - val lpa: LocalProfileAssistant + val lpa: LocalProfileAssistant = + LocalProfileAssistantImpl(apduInterface, HttpInterfaceImpl(verboseLoggingFlow)) val valid: Boolean + get() = lpa.valid - fun close() -} \ No newline at end of file + fun close() = lpa.close() +} diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt deleted file mode 100644 index 9bccbff..0000000 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt +++ /dev/null @@ -1,27 +0,0 @@ -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 -) : 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() -} diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt index c485cd2..b21ccf6 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt @@ -1,7 +1,6 @@ package im.angry.openeuicc.core import android.hardware.usb.UsbDevice -import kotlinx.coroutines.flow.Flow /** * EuiccChannelManager holds references to, and manages the lifecycles of, individual @@ -19,26 +18,19 @@ interface EuiccChannelManager { } /** - * Scan all possible _device internal_ sources for EuiccChannels, as a flow, return their physical - * (slotId, portId) and have all scanned channels cached; these channels will remain open - * for the entire lifetime of this EuiccChannelManager object, unless disconnected externally - * or invalidate()'d. - * - * To obtain a temporary reference to a EuiccChannel, use `withEuiccChannel()`. + * Scan all possible _device internal_ sources for EuiccChannels, return them and have all + * scanned channels cached; these channels will remain open for the entire lifetime of + * this EuiccChannelManager object, unless disconnected externally or invalidate()'d */ - fun flowEuiccPorts(): Flow> + suspend fun enumerateEuiccChannels(): List /** * 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 * as a "port" with id 99. When user interaction is required to obtain permission - * 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. + * to interact with the device, the second return value (EuiccChannel) will be null. */ - suspend fun tryOpenUsbEuiccChannel(): Pair + suspend fun enumerateUsbEuiccChannel(): Pair /** * Wait for a slot + port to reconnect (i.e. become valid again) @@ -48,40 +40,29 @@ interface EuiccChannelManager { suspend fun waitForReconnect(physicalSlotId: Int, portId: Int, timeoutMillis: Long = 1000) /** - * Returns the first mapped & available port ID for a physical slot, or -1 if - * not found. + * Returns the EuiccChannel corresponding to a **logical** slot */ - suspend fun findFirstAvailablePort(physicalSlotId: Int): Int + fun findEuiccChannelBySlotBlocking(logicalSlotId: Int): EuiccChannel? /** - * Returns all mapped & available port IDs for a physical slot. + * Returns the first EuiccChannel corresponding to a **physical** slot + * If the physical slot supports MEP and has multiple ports, it is undefined + * which of the two channels will be returned. */ - suspend fun findAvailablePorts(physicalSlotId: Int): List - - class EuiccChannelNotFoundException: Exception("EuiccChannel not found") + fun findEuiccChannelByPhysicalSlotBlocking(physicalSlotId: Int): EuiccChannel? /** - * Find a EuiccChannel by its slot and port, then run a callback with a reference to it. - * 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 + * Returns all EuiccChannels corresponding to a **physical** slot + * Multiple channels are possible in the case of MEP */ - suspend fun withEuiccChannel( - physicalSlotId: Int, - portId: Int, - fn: suspend (EuiccChannel) -> R - ): R + suspend fun findAllEuiccChannelsByPhysicalSlot(physicalSlotId: Int): List? + fun findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId: Int): List? /** - * Same as withEuiccChannel(Int, Int, (EuiccChannel) -> R) but instead uses logical slot ID + * Returns the EuiccChannel corresponding to a **physical** slot and a port ID */ - suspend fun withEuiccChannel( - logicalSlotId: Int, - fn: suspend (EuiccChannel) -> R - ): R + suspend fun findEuiccChannelByPort(physicalSlotId: Int, portId: Int): EuiccChannel? + fun findEuiccChannelByPortBlocking(physicalSlotId: Int, portId: Int): EuiccChannel? /** * Invalidate all EuiccChannels previously cached by this Manager @@ -93,7 +74,7 @@ interface EuiccChannelManager { * This is only expected to be implemented when the application is privileged * TODO: Remove this from the common interface */ - suspend fun notifyEuiccProfilesChanged(logicalSlotId: Int) { + fun notifyEuiccProfilesChanged(logicalSlotId: Int) { // no-op by default } } \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt deleted file mode 100644 index ab01f22..0000000 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt +++ /dev/null @@ -1,44 +0,0 @@ -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() - } - } -} \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt b/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt deleted file mode 100644 index e6a648a..0000000 --- a/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt +++ /dev/null @@ -1,63 +0,0 @@ -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 - get() = lpa.profiles - override val notifications: List - 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 - } -} \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/di/DefaultUiComponentFactory.kt b/app-common/src/main/java/im/angry/openeuicc/di/DefaultUiComponentFactory.kt index a080017..32550d6 100644 --- a/app-common/src/main/java/im/angry/openeuicc/di/DefaultUiComponentFactory.kt +++ b/app-common/src/main/java/im/angry/openeuicc/di/DefaultUiComponentFactory.kt @@ -1,12 +1,13 @@ package im.angry.openeuicc.di import androidx.fragment.app.Fragment +import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.ui.EuiccManagementFragment import im.angry.openeuicc.ui.NoEuiccPlaceholderFragment open class DefaultUiComponentFactory : UiComponentFactory { - override fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment = - EuiccManagementFragment.newInstance(slotId, portId) + override fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment = + EuiccManagementFragment.newInstance(channel.slotId, channel.portId) override fun createNoEuiccPlaceholderFragment(): Fragment = NoEuiccPlaceholderFragment() } \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/di/UiComponentFactory.kt b/app-common/src/main/java/im/angry/openeuicc/di/UiComponentFactory.kt index eef662c..4e09a70 100644 --- a/app-common/src/main/java/im/angry/openeuicc/di/UiComponentFactory.kt +++ b/app-common/src/main/java/im/angry/openeuicc/di/UiComponentFactory.kt @@ -1,9 +1,10 @@ package im.angry.openeuicc.di import androidx.fragment.app.Fragment +import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.ui.EuiccManagementFragment interface UiComponentFactory { - fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment + fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment fun createNoEuiccPlaceholderFragment(): Fragment } \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt index 9cfd526..8db3bbe 100644 --- a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt +++ b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt @@ -21,8 +21,6 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.last import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.takeWhile @@ -57,14 +55,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { private const val TAG = "EuiccChannelManagerService" private const val CHANNEL_ID = "tasks" private const val FOREGROUND_ID = 1000 - 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.waitDone(): Throwable? = - (this.last() as ForegroundTaskState.Done).error + private const val TASK_FAILURE_ID = 1001 } inner class LocalBinder : Binder() { @@ -194,7 +185,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { failureTitle: String, iconRes: Int, task: suspend EuiccChannelManagerService.() -> Unit - ): Flow { + ): Flow? { // Atomically set the state to InProgress. If this returns true, we are // the only task currently in progress. if (!foregroundTaskState.compareAndSet( @@ -202,7 +193,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { ForegroundTaskState.InProgress(0) ) ) { - return flow { emit(ForegroundTaskState.Done(IllegalStateException("There are tasks currently running"))) } + return null } lifecycleScope.launch(Dispatchers.Main) { @@ -289,27 +280,26 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { matchingId: String?, confirmationCode: String?, imei: String? - ): Flow = + ): Flow? = launchForegroundTask( getString(R.string.task_profile_download), getString(R.string.task_profile_download_failure), R.drawable.ic_task_sim_card_download ) { euiccChannelManager.beginTrackedOperation(slotId, portId) { - val res = euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> - channel.lpa.downloadProfile( - smdp, - matchingId, - imei, - confirmationCode, - object : ProfileDownloadCallback { - override fun onStateUpdate(state: ProfileDownloadCallback.DownloadState) { - if (state.progress == 0) return - foregroundTaskState.value = - ForegroundTaskState.InProgress(state.progress) - } - }) - } + val channel = euiccChannelManager.findEuiccChannelByPort(slotId, portId) + val res = channel!!.lpa.downloadProfile( + smdp, + matchingId, + imei, + confirmationCode, + object : ProfileDownloadCallback { + override fun onStateUpdate(state: ProfileDownloadCallback.DownloadState) { + if (state.progress == 0) return + foregroundTaskState.value = + ForegroundTaskState.InProgress(state.progress) + } + }) if (!res) { // TODO: Provide more details on the error @@ -325,18 +315,16 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { portId: Int, iccid: String, name: String - ): Flow = + ): Flow? = launchForegroundTask( getString(R.string.task_profile_rename), getString(R.string.task_profile_rename_failure), R.drawable.ic_task_rename ) { - val res = euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> - channel.lpa.setNickname( - iccid, - name - ) - } + val res = euiccChannelManager.findEuiccChannelByPort(slotId, portId)!!.lpa.setNickname( + iccid, + name + ) if (!res) { throw RuntimeException("Profile not renamed") @@ -347,16 +335,17 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { slotId: Int, portId: Int, iccid: String - ): Flow = + ): Flow? = launchForegroundTask( getString(R.string.task_profile_delete), getString(R.string.task_profile_delete_failure), R.drawable.ic_task_delete ) { euiccChannelManager.beginTrackedOperation(slotId, portId) { - euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> - channel.lpa.deleteProfile(iccid) - } + euiccChannelManager.findEuiccChannelByPort( + slotId, + portId + )!!.lpa.deleteProfile(iccid) preferenceRepository.notificationDeleteFlow.first() } @@ -370,17 +359,15 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { iccid: String, enable: Boolean, // Enable or disable the profile indicated in iccid reconnectTimeoutMillis: Long = 0 // 0 = do not wait for reconnect, useful for USB readers - ): Flow = + ): Flow? = launchForegroundTask( getString(R.string.task_profile_switch), getString(R.string.task_profile_switch_failure), R.drawable.ic_task_switch ) { euiccChannelManager.beginTrackedOperation(slotId, portId) { - val (res, refreshed) = euiccChannelManager.withEuiccChannel( - slotId, - portId - ) { channel -> + val channel = euiccChannelManager.findEuiccChannelByPort(slotId, portId)!! + val (res, refreshed) = if (!channel.lpa.switchProfile(iccid, enable, refresh = true)) { // Sometimes, we *can* enable or disable the profile, but we cannot // send the refresh command to the modem because the profile somehow @@ -391,7 +378,6 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { } else { Pair(true, true) } - } if (!res) { throw RuntimeException("Could not switch profile") diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/DirectProfileDownloadActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/DirectProfileDownloadActivity.kt index 4baf36b..9e79de6 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/DirectProfileDownloadActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/DirectProfileDownloadActivity.kt @@ -3,8 +3,6 @@ package im.angry.openeuicc.ui import androidx.lifecycle.lifecycleScope import im.angry.openeuicc.util.* import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -12,32 +10,22 @@ class DirectProfileDownloadActivity : BaseEuiccAccessActivity(), SlotSelectFragm override fun onInit() { lifecycleScope.launch { val knownChannels = withContext(Dispatchers.IO) { - euiccChannelManager.flowEuiccPorts().map { (slotId, portId) -> - euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> - Triple(slotId, channel.logicalSlotId, portId) - } - }.toList().sortedBy { it.second } + euiccChannelManager.enumerateEuiccChannels() } when { knownChannels.isEmpty() -> { finish() } - // Detect multiple eUICC chips - knownChannels.distinctBy { it.first }.size > 1 -> { - SlotSelectFragment.newInstance( - knownChannels.map { it.first }, - knownChannels.map { it.second }, - knownChannels.map { it.third }) + knownChannels.hasMultipleChips -> { + SlotSelectFragment.newInstance(knownChannels.sortedBy { it.logicalSlotId }) .show(supportFragmentManager, SlotSelectFragment.TAG) } else -> { // 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. - onSlotSelected( - knownChannels[0].first, - knownChannels[0].third - ) + onSlotSelected(knownChannels[0].slotId, + knownChannels[0].portId) } } } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt deleted file mode 100644 index 8e475df..0000000 --- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt +++ /dev/null @@ -1,202 +0,0 @@ -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>() - - 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) { - title.text = item.first - content.text = item.second - } - } - - inner class EuiccInfoAdapter : RecyclerView.Adapter() { - var euiccInfoItems: List> = 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]) - } - } -} \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt index bf46043..da29123 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt @@ -6,6 +6,7 @@ import android.content.ClipboardManager import android.content.Intent import android.os.Bundle import android.text.method.PasswordTransformationMethod +import android.util.Log import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater @@ -30,11 +31,11 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton import net.typeblog.lpac_jni.LocalProfileInfo import im.angry.openeuicc.common.R import im.angry.openeuicc.service.EuiccChannelManagerService -import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone import im.angry.openeuicc.util.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.last import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -51,7 +52,6 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, private lateinit var swipeRefresh: SwipeRefreshLayout private lateinit var fab: FloatingActionButton private lateinit var profileList: RecyclerView - private var logicalSlotId: Int = -1 private val adapter = EuiccProfileAdapter() @@ -127,21 +127,9 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { R.id.show_notifications -> { - if (logicalSlotId != -1) { - Intent(requireContext(), NotificationsActivity::class.java).apply { - putExtra("logicalSlotId", logicalSlotId) - startActivity(this) - } - } - true - } - - R.id.euicc_info -> { - if (logicalSlotId != -1) { - Intent(requireContext(), EuiccInfoActivity::class.java).apply { - putExtra("logicalSlotId", logicalSlotId) - startActivity(this) - } + Intent(requireContext(), NotificationsActivity::class.java).apply { + putExtra("logicalSlotId", channel.logicalSlotId) + startActivity(this) } true } @@ -160,36 +148,31 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, listOf() } + @SuppressLint("NotifyDataSetChanged") private fun refresh() { if (invalid) return swipeRefresh.isRefreshing = true lifecycleScope.launch { - doRefresh() - } - } + ensureEuiccChannelManager() + euiccChannelManagerService.waitForForegroundTask() - @SuppressLint("NotifyDataSetChanged") - protected open suspend fun doRefresh() { - ensureEuiccChannelManager() - euiccChannelManagerService.waitForForegroundTask() + if (!this@EuiccManagementFragment::disableSafeguardFlow.isInitialized) { + disableSafeguardFlow = + preferenceRepository.disableSafeguardFlow.stateIn(lifecycleScope) + } - if (!this@EuiccManagementFragment::disableSafeguardFlow.isInitialized) { - disableSafeguardFlow = - preferenceRepository.disableSafeguardFlow.stateIn(lifecycleScope) - } + val profiles = withContext(Dispatchers.IO) { + euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId) + channel.lpa.profiles.operational + } - val profiles = withEuiccChannel { channel -> - logicalSlotId = channel.logicalSlotId - euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId) - channel.lpa.profiles.operational - } - - withContext(Dispatchers.Main) { - adapter.profiles = profiles - adapter.footerViews = onCreateFooterViews(profileList, profiles) - adapter.notifyDataSetChanged() - swipeRefresh.isRefreshing = false + withContext(Dispatchers.Main) { + adapter.profiles = profiles + adapter.footerViews = onCreateFooterViews(profileList, profiles) + adapter.notifyDataSetChanged() + swipeRefresh.isRefreshing = false + } } } @@ -209,7 +192,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, ensureEuiccChannelManager() euiccChannelManagerService.waitForForegroundTask() - val err = euiccChannelManagerService.launchProfileSwitchTask( + val res = euiccChannelManagerService.launchProfileSwitchTask( slotId, portId, iccid, @@ -219,9 +202,14 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, } else { 30 * 1000 } - ).waitDone() + )?.last() as? EuiccChannelManagerService.ForegroundTaskState.Done - when (err) { + if (res == null) { + showSwitchFailureText() + return@launch + } + + when (res.error) { null -> {} is EuiccChannelManagerService.SwitchingProfilesRefreshException -> { // This is only really fatal for internal eSIMs diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt index 74f2147..e432f6c 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt @@ -23,12 +23,9 @@ import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator import im.angry.openeuicc.common.R -import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.util.* import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -47,7 +44,6 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { private var refreshing = false private data class Page( - val logicalSlotId: Int, val title: String, val createFragment: () -> Fragment ) @@ -142,83 +138,65 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { // Prevent concurrent access with any running foreground task euiccChannelManagerService.waitForForegroundTask() - val (usbDevice, _) = withContext(Dispatchers.IO) { - euiccChannelManager.tryOpenUsbEuiccChannel() - } - - val newPages: MutableList = mutableListOf() - - euiccChannelManager.flowEuiccPorts().onEach { (slotId, portId) -> - Log.d(TAG, "slot $slotId port $portId") - - euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> + val knownChannels = withContext(Dispatchers.IO) { + euiccChannelManager.enumerateEuiccChannels().onEach { + Log.d(TAG, "slot ${it.slotId} port ${it.portId}") if (preferenceRepository.verboseLoggingFlow.first()) { - Log.d(TAG, channel.lpa.eID) + Log.d(TAG, it.lpa.eID) } // 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, // but it could change in the future - euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId) - - newPages.add( - Page( - channel.logicalSlotId, - getString(R.string.channel_name_format, channel.logicalSlotId) - ) { - appContainer.uiComponentFactory.createEuiccManagementFragment( - slotId, - portId - ) - }) + euiccChannelManager.notifyEuiccProfilesChanged(it.logicalSlotId) } - }.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() }) } - newPages.sortBy { it.logicalSlotId } + val (usbDevice, _) = withContext(Dispatchers.IO) { + euiccChannelManager.enumerateUsbEuiccChannel() + } - pages.clear() - pages.addAll(newPages) + withContext(Dispatchers.Main) { + loadingProgress.visibility = View.GONE - 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) + knownChannels.sortedBy { it.logicalSlotId }.forEach { channel -> + pages.add(Page( + getString(R.string.channel_name_format, channel.logicalSlotId) + ) { appContainer.uiComponentFactory.createEuiccManagementFragment(channel) }) } - } else { - viewPager.currentItem = 0 - } - if (pages.size > 0) { - ensureNotificationPermissions() - } + // 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 - refreshing = false + 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 + } } private fun refresh(fromUsbEvent: Boolean = false) { diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt index 31b7d7e..884e223 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt @@ -20,6 +20,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import im.angry.openeuicc.common.R +import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.util.* import kotlinx.coroutines.Dispatchers @@ -32,7 +33,7 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker { private lateinit var notificationList: RecyclerView private val notificationAdapter = NotificationAdapter() - private var logicalSlotId = -1 + private lateinit var euiccChannel: EuiccChannel override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() @@ -55,7 +56,7 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker { notificationList.adapter = notificationAdapter registerForContextMenu(notificationList) - logicalSlotId = intent.getIntExtra("logicalSlotId", 0) + val logicalSlotId = intent.getIntExtra("logicalSlotId", 0) // This is slightly different from the MainActivity logic // due to the length (we don't want to display the full USB product name) @@ -103,8 +104,16 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker { swipeRefresh.isRefreshing = true lifecycleScope.launch { - withContext(Dispatchers.IO) { - euiccChannelManagerLoaded.await() + if (!this@NotificationsActivity::euiccChannel.isInitialized) { + withContext(Dispatchers.IO) { + euiccChannelManagerLoaded.await() + euiccChannel = euiccChannelManager.findEuiccChannelBySlotBlocking( + intent.getIntExtra( + "logicalSlotId", + 0 + ) + )!! + } } task() @@ -115,11 +124,13 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker { private fun refresh() { launchTask { - notificationAdapter.notifications = - euiccChannelManager.withEuiccChannel(logicalSlotId) { channel -> - val profiles = channel.lpa.profiles + val profiles = withContext(Dispatchers.IO) { + euiccChannel.lpa.profiles + } - channel.lpa.notifications.map { + notificationAdapter.notifications = + withContext(Dispatchers.IO) { + euiccChannel.lpa.notifications.map { val profile = profiles.find { p -> p.iccid == it.iccid } LocalProfileNotificationWrapper(it, profile?.displayName ?: "???") } @@ -194,9 +205,7 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker { R.id.notification_process -> { launchTask { withContext(Dispatchers.IO) { - euiccChannelManager.withEuiccChannel(logicalSlotId) { channel -> - channel.lpa.handleNotification(notification.inner.seqNumber) - } + euiccChannel.lpa.handleNotification(notification.inner.seqNumber) } refresh() @@ -206,9 +215,7 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker { R.id.notification_delete -> { launchTask { withContext(Dispatchers.IO) { - euiccChannelManager.withEuiccChannel(logicalSlotId) { channel -> - channel.lpa.deleteNotification(notification.inner.seqNumber) - } + euiccChannel.lpa.deleteNotification(notification.inner.seqNumber) } refresh() diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt index 181aeee..901f263 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt @@ -8,8 +8,8 @@ import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment import androidx.lifecycle.lifecycleScope import im.angry.openeuicc.common.R -import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone import im.angry.openeuicc.util.* +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch @@ -74,7 +74,7 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker { slotId, portId, requireArguments().getString("iccid")!! - ).onStart { + )!!.onStart { if (parentFragment is EuiccProfilesChangedListener) { // Trigger a refresh in the parent fragment -- it should wait until // any foreground task is completed before actually doing a refresh @@ -86,7 +86,7 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker { } catch (e: IllegalStateException) { // Ignored } - }.waitDone() + }.collect() } } } \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt index 69ad1d0..843c9e5 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt @@ -19,9 +19,9 @@ import com.journeyapps.barcodescanner.ScanContract import com.journeyapps.barcodescanner.ScanOptions import im.angry.openeuicc.common.R import im.angry.openeuicc.service.EuiccChannelManagerService -import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone import im.angry.openeuicc.util.* import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.last import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -159,26 +159,22 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(), return@launch } - withEuiccChannel { channel -> - val imei = try { - telephonyManager.getImei(channel.logicalSlotId) ?: "" - } catch (e: Exception) { - "" - } + val imei = try { + telephonyManager.getImei(channel.logicalSlotId) ?: "" + } catch (e: Exception) { + "" + } - // Fetch remaining NVRAM - val str = channel.lpa.euiccInfo2?.freeNvram?.also { - freeNvram = it - }?.let { formatFreeSpace(it) } + // Fetch remaining NVRAM + val str = channel.lpa.euiccInfo2?.freeNvram?.also { + freeNvram = it + }?.let { formatFreeSpace(it) } - withContext(Dispatchers.Main) { - profileDownloadFreeSpace.text = getString( - R.string.profile_download_free_space, - str ?: getText(R.string.unknown) - ) - profileDownloadIMEI.editText!!.text = - Editable.Factory.getInstance().newEditable(imei) - } + withContext(Dispatchers.Main) { + profileDownloadFreeSpace.text = getString(R.string.profile_download_free_space, + str ?: getText(R.string.unknown)) + profileDownloadIMEI.editText!!.text = + Editable.Factory.getInstance().newEditable(imei) } } } @@ -219,11 +215,14 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(), lifecycleScope.launch { ensureEuiccChannelManager() euiccChannelManagerService.waitForForegroundTask() - val err = doDownloadProfile(server, code, confirmationCode, imei) + val res = doDownloadProfile(server, code, confirmationCode, imei) - if (err != null) { + if (res == null || res.error != null) { 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() } @@ -247,15 +246,14 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(), imei: String? ) = withContext(Dispatchers.Main) { // The service is responsible for launching the actual blocking part on the IO context - // On our side, we need the Main context because of the UI updates - euiccChannelManagerService.launchProfileDownloadTask( + val res = euiccChannelManagerService.launchProfileDownloadTask( slotId, portId, server, code, confirmationCode, imei - ).onEach { + )!!.onEach { if (it is EuiccChannelManagerService.ForegroundTaskState.InProgress) { progress.progress = it.progress progress.isIndeterminate = it.progress == 0 @@ -263,7 +261,9 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(), progress.progress = 100 progress.isIndeterminate = false } - }.waitDone() + }.last() + + res as? EuiccChannelManagerService.ForegroundTaskState.Done } override fun onDismiss(dialog: DialogInterface) { diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt index 8582278..278ea43 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt @@ -11,8 +11,8 @@ import androidx.appcompat.widget.Toolbar import androidx.lifecycle.lifecycleScope import com.google.android.material.textfield.TextInputLayout import im.angry.openeuicc.common.R -import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone import im.angry.openeuicc.util.* +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragmentMarker { @@ -100,7 +100,7 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment portId, requireArguments().getString("iccid")!!, name - ).waitDone() + )?.collect() if (parentFragment is EuiccProfilesChangedListener) { (parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged() diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/SlotSelectFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/SlotSelectFragment.kt index 2c4fe3c..d1239c4 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/SlotSelectFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/SlotSelectFragment.kt @@ -16,12 +16,12 @@ class SlotSelectFragment : BaseMaterialDialogFragment(), OpenEuiccContextMarker companion object { const val TAG = "SlotSelectFragment" - fun newInstance(slotIds: List, logicalSlotIds: List, portIds: List): SlotSelectFragment { + fun newInstance(knownChannels: List): SlotSelectFragment { return SlotSelectFragment().apply { arguments = Bundle().apply { - putIntArray("slotIds", slotIds.toIntArray()) - putIntArray("logicalSlotIds", logicalSlotIds.toIntArray()) - putIntArray("portIds", portIds.toIntArray()) + putIntArray("slotIds", knownChannels.map { it.slotId }.toIntArray()) + putIntArray("logicalSlotIds", knownChannels.map { it.logicalSlotId }.toIntArray()) + putIntArray("portIds", knownChannels.map { it.portId }.toIntArray()) } } } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/UsbCcidReaderFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/UsbCcidReaderFragment.kt index d104582..3988b09 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/UsbCcidReaderFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/UsbCcidReaderFragment.kt @@ -20,6 +20,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.commit import androidx.lifecycle.lifecycleScope import im.angry.openeuicc.common.R +import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.util.* import kotlinx.coroutines.Dispatchers @@ -72,6 +73,7 @@ class UsbCcidReaderFragment : Fragment(), OpenEuiccContextMarker { private lateinit var loadingProgress: ProgressBar private var usbDevice: UsbDevice? = null + private var usbChannel: EuiccChannel? = null override fun onCreateView( inflater: LayoutInflater, @@ -138,26 +140,24 @@ class UsbCcidReaderFragment : Fragment(), OpenEuiccContextMarker { permissionButton.visibility = View.GONE loadingProgress.visibility = View.VISIBLE - val (device, canOpen) = withContext(Dispatchers.IO) { - euiccChannelManager.tryOpenUsbEuiccChannel() + val (device, channel) = withContext(Dispatchers.IO) { + euiccChannelManager.enumerateUsbEuiccChannel() } loadingProgress.visibility = View.GONE usbDevice = device + usbChannel = channel - if (device != null && !canOpen && !usbManager.hasPermission(device)) { + if (device != null && channel == null && !usbManager.hasPermission(device)) { text.text = getString(R.string.usb_permission_needed) text.visibility = View.VISIBLE permissionButton.visibility = View.VISIBLE - } else if (device != null && canOpen) { + } else if (device != null && channel != null) { childFragmentManager.commit { replace( R.id.child_container, - appContainer.uiComponentFactory.createEuiccManagementFragment( - EuiccChannelManager.USB_CHANNEL_ID, - 0 - ) + appContainer.uiComponentFactory.createEuiccManagementFragment(channel) ) } } else { diff --git a/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt index f0cf193..e92be40 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt @@ -36,11 +36,9 @@ val T.euiccChannelManager: EuiccChannelManager where T: Fragment, T: EuiccCh get() = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManager val T.euiccChannelManagerService: EuiccChannelManagerService where T: Fragment, T: EuiccChannelFragmentMarker get() = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManagerService - -suspend fun T.withEuiccChannel(fn: suspend (EuiccChannel) -> R): R where T : Fragment, T : EuiccChannelFragmentMarker { - ensureEuiccChannelManager() - return euiccChannelManager.withEuiccChannel(slotId, portId, fn) -} +val T.channel: EuiccChannel where T: Fragment, T: EuiccChannelFragmentMarker + get() = + euiccChannelManager.findEuiccChannelByPortBlocking(slotId, portId)!! suspend fun T.ensureEuiccChannelManager() where T: Fragment, T: EuiccChannelFragmentMarker = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManagerLoaded.await() diff --git a/app-common/src/main/java/im/angry/openeuicc/util/LPAUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/LPAUtils.kt index 0fe44c1..e7a3322 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/LPAUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/LPAUtils.kt @@ -3,6 +3,9 @@ package im.angry.openeuicc.util import android.util.Log import im.angry.openeuicc.core.EuiccChannel 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.LocalProfileInfo @@ -45,21 +48,16 @@ fun LocalProfileAssistant.disableActiveProfile(refresh: Boolean): Boolean = } ?: true /** - * Disable the current active profile if any. If refresh is true, also cause a refresh command. + * Disable the active profile, return a lambda that reverts this action when called. + * 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() - * - * Return the iccid of the profile being disabled, or null if no active profile found or failed to - * disable. */ -fun LocalProfileAssistant.disableActiveProfileKeepIccId(refresh: Boolean): String? = +fun LocalProfileAssistant.disableActiveProfileWithUndo(refreshOnDisable: Boolean): () -> Unit = profiles.find { it.isEnabled }?.let { - Log.i(TAG, "Disabling active profile ${it.iccid}") - if (disableProfile(it.iccid, refresh)) { - it.iccid - } else { - null - } - } + disableProfile(it.iccid, refreshOnDisable) + return { enableProfile(it.iccid) } + } ?: { } /** * Begin a "tracked" operation where notifications may be generated by the eSIM @@ -80,21 +78,60 @@ suspend inline fun EuiccChannelManager.beginTrackedOperation( portId: Int, op: () -> Boolean ) { - val latestSeq = withEuiccChannel(slotId, portId) { channel -> - channel.lpa.notifications.firstOrNull()?.seqNumber + val latestSeq = + findEuiccChannelByPort(slotId, portId)!!.lpa.notifications.firstOrNull()?.seqNumber ?: 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; - // this is why we need to use two distinct calls to withEuiccChannel() - withEuiccChannel(slotId, portId) { channel -> - channel.lpa.notifications.filter { it.seqNumber > latestSeq }.forEach { - Log.d(TAG, "Handling notification $it") - channel.lpa.handleNotification(it.seqNumber) - } + // 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") + 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" + findEuiccChannelByPortBlocking( + slotId, + portId + )?.lpa?.notifications?.filter { it.seqNumber > latestSeq }?.forEach { + Log.d(TAG, "Handling notification $it") + findEuiccChannelByPortBlocking( + slotId, + portId + )?.lpa?.handleNotification(it.seqNumber) } } catch (e: Exception) { // Ignore any error during notification handling diff --git a/app-common/src/main/java/im/angry/openeuicc/util/TelephonyCompat.kt b/app-common/src/main/java/im/angry/openeuicc/util/TelephonyCompat.kt index b831f01..5c7d217 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/TelephonyCompat.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/TelephonyCompat.kt @@ -45,8 +45,6 @@ fun SEService.getUiccReaderCompat(slotNumber: Int): Reader { interface UiccCardInfoCompat { val physicalSlotIndex: Int val ports: Collection - val isRemovable: Boolean - get() = true // This defaults to removable unless overridden } interface UiccPortInfoCompat { diff --git a/app-common/src/main/res/layout/activity_euicc_info.xml b/app-common/src/main/res/layout/activity_euicc_info.xml deleted file mode 100644 index 8a8b001..0000000 --- a/app-common/src/main/res/layout/activity_euicc_info.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app-common/src/main/res/layout/euicc_info_item.xml b/app-common/src/main/res/layout/euicc_info_item.xml deleted file mode 100644 index 39d15a6..0000000 --- a/app-common/src/main/res/layout/euicc_info_item.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app-common/src/main/res/menu/fragment_euicc.xml b/app-common/src/main/res/menu/fragment_euicc.xml index b54eaf1..9acb640 100644 --- a/app-common/src/main/res/menu/fragment_euicc.xml +++ b/app-common/src/main/res/menu/fragment_euicc.xml @@ -5,9 +5,4 @@ android:id="@+id/show_notifications" android:title="@string/profile_notifications_show" app:showAsAction="never" /> - - \ No newline at end of file diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index cc8f220..0278ffa 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -8,7 +8,6 @@ Logical Slot %d USB - OpenMobile API (OMAPI) Enabled Disabled @@ -72,25 +71,6 @@ Process Delete - eUICC Info - eUICC Info (%s) - Access Mode - Removable - EID - eUICC OS Version - GlobalPlatform Version - SAS Accreditation Number - Protected Profile Version - Free NVRAM (eSIM profile storage) - GSMA Production Certificate - GSMA Test Certificate - - Supported - Unsupported - - Yes - No - Save Logs at %s diff --git a/app-unpriv/src/main/AndroidManifest.xml b/app-unpriv/src/main/AndroidManifest.xml index 8760fb9..e72b112 100644 --- a/app-unpriv/src/main/AndroidManifest.xml +++ b/app-unpriv/src/main/AndroidManifest.xml @@ -22,14 +22,9 @@ + android:label="@string/compatibility_check" + android:exported="false" /> - - - - - \ No newline at end of file diff --git a/app-unpriv/src/main/java/im/angry/openeuicc/di/UnprivilegedUiComponentFactory.kt b/app-unpriv/src/main/java/im/angry/openeuicc/di/UnprivilegedUiComponentFactory.kt index 50e5581..f117038 100644 --- a/app-unpriv/src/main/java/im/angry/openeuicc/di/UnprivilegedUiComponentFactory.kt +++ b/app-unpriv/src/main/java/im/angry/openeuicc/di/UnprivilegedUiComponentFactory.kt @@ -1,14 +1,9 @@ package im.angry.openeuicc.di import androidx.fragment.app.Fragment -import im.angry.openeuicc.ui.EuiccManagementFragment -import im.angry.openeuicc.ui.UnprivilegedEuiccManagementFragment import im.angry.openeuicc.ui.UnprivilegedNoEuiccPlaceholderFragment class UnprivilegedUiComponentFactory : DefaultUiComponentFactory() { - override fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment = - UnprivilegedEuiccManagementFragment.newInstance(slotId, portId) - override fun createNoEuiccPlaceholderFragment(): Fragment = UnprivilegedNoEuiccPlaceholderFragment() } \ No newline at end of file diff --git a/app-unpriv/src/main/java/im/angry/openeuicc/ui/UnprivilegedEuiccManagementFragment.kt b/app-unpriv/src/main/java/im/angry/openeuicc/ui/UnprivilegedEuiccManagementFragment.kt deleted file mode 100644 index fad03fd..0000000 --- a/app-unpriv/src/main/java/im/angry/openeuicc/ui/UnprivilegedEuiccManagementFragment.kt +++ /dev/null @@ -1,31 +0,0 @@ -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) - } - } -} \ No newline at end of file diff --git a/app-unpriv/src/main/java/im/angry/openeuicc/util/SIMToolkit.kt b/app-unpriv/src/main/java/im/angry/openeuicc/util/SIMToolkit.kt deleted file mode 100644 index 148e932..0000000 --- a/app-unpriv/src/main/java/im/angry/openeuicc/util/SIMToolkit.kt +++ /dev/null @@ -1,64 +0,0 @@ -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 - } -} diff --git a/app-unpriv/src/main/res/menu/fragment_sim_toolkit.xml b/app-unpriv/src/main/res/menu/fragment_sim_toolkit.xml deleted file mode 100644 index 610b3a1..0000000 --- a/app-unpriv/src/main/res/menu/fragment_sim_toolkit.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/app-unpriv/src/main/res/values/sim_toolkit.xml b/app-unpriv/src/main/res/values/sim_toolkit.xml deleted file mode 100644 index 1f16271..0000000 --- a/app-unpriv/src/main/res/values/sim_toolkit.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - com.android.stk/.StkMain - com.android.stk/.StkMainHide - com.android.stk/.StkListActivity - com.android.stk/.StkLauncherListActivity - - - com.android.stk/.StkMain1 - com.android.stk/.PrimaryStkMain - com.android.stk/.StkLauncherActivity - com.android.stk/.StkLauncherActivity_Chn - com.android.stk/.StkLauncherActivityI - com.android.stk/.OppoStkLauncherActivity1 - com.android.stk/.OplusStkLauncherActivity1 - com.android.stk/.mtk.StkLauncherActivityI - - - com.android.stk/.StkMain2 - com.android.stk/.SecondaryStkMain - com.android.stk/.StkLauncherActivity2 - com.android.stk/.StkLauncherActivityII - com.android.stk/.OppoStkLauncherActivity2 - com.android.stk/.OplusStkLauncherActivity2 - com.android.stk/.mtk.StkLauncherActivityII - com.android.stk1/.StkLauncherActivity - com.android.stk2/.StkLauncherActivity - com.android.stk2/.StkLauncherActivity_Chn - com.android.stk2/.StkLauncherActivity2 - - \ No newline at end of file diff --git a/app-unpriv/src/main/res/values/strings.xml b/app-unpriv/src/main/res/values/strings.xml index fb7dc94..124bedf 100644 --- a/app-unpriv/src/main/res/values/strings.xml +++ b/app-unpriv/src/main/res/values/strings.xml @@ -2,7 +2,6 @@ EasyEUICC SIM %d Compatibility Check - Open SIM Toolkit System Features @@ -12,11 +11,11 @@ OMAPI Connectivity Does your device allow access to Secure Elements on SIM cards via OMAPI? 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. - Successfully detected Secure Element access, but only for the following SIM slots: SIM%s. + Successfully detected Secure Element access, but only for the following SIM slots: %s. ISD-R Channel Access Does your device support opening an ISD-R (management) channel to eSIMs via OMAPI? 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. - OMAPI access to ISD-R is only possible on the following SIM slots: SIM%s. + OMAPI access to ISD-R is only possible on the following SIM slots: %s. Not on the Known Broken List Making sure your device is not known to have bugs associated with removable eSIMs. 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. diff --git a/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt b/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt index c90e450..ce57fb8 100644 --- a/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt +++ b/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt @@ -3,7 +3,6 @@ package im.angry.openeuicc.core import android.content.Context import android.util.Log import im.angry.openeuicc.OpenEuiccApplication -import im.angry.openeuicc.R import im.angry.openeuicc.util.* import java.lang.IllegalArgumentException @@ -27,8 +26,7 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto "Trying TelephonyManager for slot ${port.card.physicalSlotIndex} port ${port.portIndex}" ) try { - return EuiccChannelImpl( - context.getString(R.string.telephony_manager), + return EuiccChannel( port, TelephonyManagerApduInterface( port, diff --git a/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelManager.kt b/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelManager.kt index aaf5490..923bbab 100644 --- a/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelManager.kt +++ b/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelManager.kt @@ -28,9 +28,9 @@ class PrivilegedEuiccChannelManager( } } - override suspend fun notifyEuiccProfilesChanged(logicalSlotId: Int) { + override fun notifyEuiccProfilesChanged(logicalSlotId: Int) { appContainer.subscriptionManager.apply { - findEuiccChannelByLogicalSlot(logicalSlotId)?.let { + findEuiccChannelBySlotBlocking(logicalSlotId)?.let { tryRefreshCachedEuiccInfo(it.cardId) } } diff --git a/app/src/main/java/im/angry/openeuicc/di/PrivilegedUiComponentFactory.kt b/app/src/main/java/im/angry/openeuicc/di/PrivilegedUiComponentFactory.kt index 701e57d..d3c5cdb 100644 --- a/app/src/main/java/im/angry/openeuicc/di/PrivilegedUiComponentFactory.kt +++ b/app/src/main/java/im/angry/openeuicc/di/PrivilegedUiComponentFactory.kt @@ -1,9 +1,10 @@ package im.angry.openeuicc.di +import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.ui.EuiccManagementFragment import im.angry.openeuicc.ui.PrivilegedEuiccManagementFragment class PrivilegedUiComponentFactory : DefaultUiComponentFactory() { - override fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment = - PrivilegedEuiccManagementFragment.newInstance(slotId, portId) + override fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment = + PrivilegedEuiccManagementFragment.newInstance(channel.slotId, channel.portId) } \ No newline at end of file diff --git a/app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt b/app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt index aeda3e4..572b02b 100644 --- a/app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt +++ b/app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt @@ -9,12 +9,12 @@ import android.telephony.euicc.DownloadableSubscription import android.telephony.euicc.EuiccInfo import android.util.Log import net.typeblog.lpac_jni.LocalProfileInfo +import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.core.EuiccChannelManager -import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone import im.angry.openeuicc.util.* -import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking -import kotlin.IllegalStateException +import java.lang.IllegalStateException class OpenEuiccService : EuiccService(), OpenEuiccContextMarker { companion object { @@ -37,10 +37,16 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker { } private data class EuiccChannelManagerContext( - val euiccChannelManagerService: EuiccChannelManagerService + val euiccChannelManager: EuiccChannelManager ) { - val euiccChannelManager - get() = euiccChannelManagerService.euiccChannelManager + fun findChannel(physicalSlotId: Int): EuiccChannel? = + euiccChannelManager.findEuiccChannelByPhysicalSlotBlocking(physicalSlotId) + + fun findChannel(slotId: Int, portId: Int): EuiccChannel? = + euiccChannelManager.findEuiccChannelByPortBlocking(slotId, portId) + + fun findAllChannels(physicalSlotId: Int): List? = + euiccChannelManager.findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId) } /** @@ -53,7 +59,7 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker { * * This function cannot be inline because non-local returns may bypass the unbind */ - private fun withEuiccChannelManager(fn: suspend EuiccChannelManagerContext.() -> T): T { + private fun withEuiccChannelManager(fn: EuiccChannelManagerContext.() -> T): T { val (binder, unbind) = runBlocking { bindServiceSuspended( Intent( @@ -67,24 +73,23 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker { throw RuntimeException("Unable to bind to EuiccChannelManagerService; aborting") } - val localBinder = binder as EuiccChannelManagerService.LocalBinder - - val ret = runBlocking { - EuiccChannelManagerContext(localBinder.service).fn() - } + val ret = + EuiccChannelManagerContext((binder as EuiccChannelManagerService.LocalBinder).service.euiccChannelManager).fn() unbind() return ret } override fun onGetEid(slotId: Int): String? = withEuiccChannelManager { - val portId = euiccChannelManager.findFirstAvailablePort(slotId) - if (portId < 0) return@withEuiccChannelManager null - euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> - channel.lpa.eID - } + findChannel(slotId)?.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) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { return @@ -116,11 +121,7 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker { telephonyManager.simSlotMapping = mappings.reversed() } - private suspend fun retryWithTimeout( - timeoutMillis: Int, - backoff: Int = 1000, - f: suspend () -> T? - ): T? { + private fun retryWithTimeout(timeoutMillis: Int, backoff: Int = 1000, f: () -> T?): T? { val startTimeMillis = System.currentTimeMillis() do { try { @@ -128,7 +129,7 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker { } catch (_: Exception) { // Ignore } finally { - delay(backoff.toLong()) + Thread.sleep(backoff.toLong()) } } while (System.currentTimeMillis() - startTimeMillis < timeoutMillis) return null @@ -176,54 +177,38 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker { } // TODO: Temporarily enable the slot to access its profiles if it is currently unmapped - val port = euiccChannelManager.findFirstAvailablePort(slotId) - if (port == -1) { - return@withEuiccChannelManager GetEuiccProfileInfoListResult( + val channel = + findChannel(slotId) ?: return@withEuiccChannelManager GetEuiccProfileInfoListResult( RESULT_FIRST_USER, arrayOf(), true ) - } - - try { - return@withEuiccChannelManager euiccChannelManager.withEuiccChannel( - slotId, - 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 + 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 + } ) - } - } catch (e: EuiccChannelManager.EuiccChannelNotFoundException) { - return@withEuiccChannelManager GetEuiccProfileInfoListResult( - RESULT_FIRST_USER, - arrayOf(), - true - ) + 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( + RESULT_OK, + profiles.toTypedArray(), + channel.removable + ) } override fun onGetEuiccInfo(slotId: Int): EuiccInfo { @@ -234,30 +219,39 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker { Log.i(TAG, "onDeleteSubscription slotId=$slotId iccid=$iccid") if (shouldIgnoreSlot(slotId)) return@withEuiccChannelManager RESULT_FIRST_USER - val ports = euiccChannelManager.findAvailablePorts(slotId) - if (ports.isEmpty()) return@withEuiccChannelManager RESULT_FIRST_USER + try { + val channels = + findAllChannels(slotId) ?: return@withEuiccChannelManager RESULT_FIRST_USER - // Check that the profile has been disabled on all slots - val enabledAnywhere = ports.any { port -> - euiccChannelManager.withEuiccChannel(slotId, port) { channel -> + if (!channels[0].profileExists(iccid)) { + return@withEuiccChannelManager RESULT_FIRST_USER + } + + // If the profile is enabled by ANY channel (port), we cannot delete it + channels.forEach { channel -> val profile = channel.lpa.profiles.find { it.iccid == iccid - } ?: return@withEuiccChannel false + } ?: return@withEuiccChannelManager RESULT_FIRST_USER - profile.state == LocalProfileInfo.State.Enabled + if (profile.state == LocalProfileInfo.State.Enabled) { + // Must disable the profile first + return@withEuiccChannelManager RESULT_FIRST_USER + } } - } - if (enabledAnywhere) return@withEuiccChannelManager RESULT_FIRST_USER + euiccChannelManager.beginTrackedOperationBlocking(channels[0].slotId, channels[0].portId) { + if (channels[0].lpa.deleteProfile(iccid)) { + return@withEuiccChannelManager RESULT_OK + } - euiccChannelManagerService.waitForForegroundTask() - val success = euiccChannelManagerService.launchProfileDeleteTask(slotId, ports[0], iccid) - .waitDone() == null + runBlocking { + preferenceRepository.notificationDeleteFlow.first() + } + } - return@withEuiccChannelManager if (success) { - RESULT_OK - } else { - RESULT_FIRST_USER + return@withEuiccChannelManager RESULT_FIRST_USER + } catch (e: Exception) { + return@withEuiccChannelManager RESULT_FIRST_USER } } @@ -280,90 +274,51 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker { if (shouldIgnoreSlot(slotId)) return@withEuiccChannelManager RESULT_FIRST_USER 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 // AOSP has switched slot mappings, in which case the slots may not be ready yet. - val (foundSlotId, foundPortId) = retryWithTimeout(5000) { - if (portIndex == -1) { - // If port is not indicated, we can use any port - 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 { - // 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) { - 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 { - portIndex - } - - // Now we can try to map an unused port - try { - ensurePortIsMapped(slotId, foundPortId) - } catch (_: Exception) { - return@withEuiccChannelManager RESULT_FIRST_USER - } - - // Wait for availability again - retryWithTimeout(5000) { - euiccChannelManager.withEuiccChannel(slotId, foundPortId) { channel -> - if (!channel.valid) { - throw IllegalStateException("Indicated slot / port combination is unavailable; may need to try again") - } - } - } ?: return@withEuiccChannelManager RESULT_FIRST_USER - - Pair(slotId, foundPortId) - } - - 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) + val channel = if (portIndex == -1) { + retryWithTimeout(5000) { findChannel(slotId) } } else { - Pair(iccid, true) + retryWithTimeout(5000) { findChannel(slotId, portIndex) } + } ?: run { + if (!forceDeactivateSim) { + // The user must select which SIM to deactivate + return@withEuiccChannelManager RESULT_MUST_DEACTIVATE_SIM + } else { + try { + // 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 + } } - val res = euiccChannelManagerService.launchProfileSwitchTask( - foundSlotId, - foundPortId, - foundIccid, - enable, - 30 * 1000 - ).waitDone() + if (iccid != null && !channel.profileExists(iccid)) { + Log.i(TAG, "onSwitchToSubscriptionWithPort iccid=$iccid not found") + return@withEuiccChannelManager RESULT_FIRST_USER + } - if (res != null) return@withEuiccChannelManager RESULT_FIRST_USER + euiccChannelManager.beginTrackedOperationBlocking(channel.slotId, channel.portId) { + if (iccid != null) { + // Disable any active profile first if present + channel.lpa.disableActiveProfile(false) + if (!channel.lpa.enableProfile(iccid)) { + return@withEuiccChannelManager RESULT_FIRST_USER + } + } else { + if (!channel.lpa.disableActiveProfile(true)) { + return@withEuiccChannelManager RESULT_FIRST_USER + } + } + + runBlocking { + preferenceRepository.notificationSwitchFlow.first() + } + } return@withEuiccChannelManager RESULT_OK } catch (e: Exception) { @@ -380,19 +335,13 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker { "onUpdateSubscriptionNickname slotId=$slotId iccid=$iccid nickname=$nickname" ) if (shouldIgnoreSlot(slotId)) return@withEuiccChannelManager RESULT_FIRST_USER - val port = euiccChannelManager.findFirstAvailablePort(slotId) - if (port < 0) { + val channel = findChannel(slotId) ?: return@withEuiccChannelManager RESULT_FIRST_USER + if (!channel.profileExists(iccid)) { return@withEuiccChannelManager RESULT_FIRST_USER } - - euiccChannelManagerService.waitForForegroundTask() - val success = - (euiccChannelManagerService.launchProfileRenameTask(slotId, port, iccid, nickname!!) - .waitDone()) == null - - euiccChannelManager.withEuiccChannel(slotId, port) { channel -> - appContainer.subscriptionManager.tryRefreshCachedEuiccInfo(channel.cardId) - } + val success = channel.lpa + .setNickname(iccid, nickname!!) + appContainer.subscriptionManager.tryRefreshCachedEuiccInfo(channel.cardId) return@withEuiccChannelManager if (success) { RESULT_OK } else { diff --git a/app/src/main/java/im/angry/openeuicc/ui/PrivilegedEuiccManagementFragment.kt b/app/src/main/java/im/angry/openeuicc/ui/PrivilegedEuiccManagementFragment.kt index 688ae6c..7d055f4 100644 --- a/app/src/main/java/im/angry/openeuicc/ui/PrivilegedEuiccManagementFragment.kt +++ b/app/src/main/java/im/angry/openeuicc/ui/PrivilegedEuiccManagementFragment.kt @@ -6,6 +6,8 @@ import android.widget.Button import android.widget.PopupMenu import im.angry.openeuicc.R import im.angry.openeuicc.util.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import net.typeblog.lpac_jni.LocalProfileInfo class PrivilegedEuiccManagementFragment: EuiccManagementFragment() { @@ -14,23 +16,14 @@ class PrivilegedEuiccManagementFragment: EuiccManagementFragment() { 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( parent: ViewGroup, profiles: List ): List = super.onCreateFooterViews(parent, profiles).let { footers -> - if (isMEP) { + // isMEP can map to a slow operation (UiccCardInfo.isMultipleEnabledProfilesSupported()) + // 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) view.requireViewById