forked from PeterCxy/OpenEUICC
Compare commits
10 commits
1dc5004681
...
52c60d443f
Author | SHA1 | Date | |
---|---|---|---|
52c60d443f | |||
22ec3e3baf | |||
32f5e3f71a | |||
04debd62d5 | |||
0ef435956c | |||
573dce56a6 | |||
272ab953e0 | |||
6257a03058 | |||
5e5210ae2d | |||
87eb497f40 |
18 changed files with 355 additions and 153 deletions
|
@ -11,14 +11,8 @@ 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.collect
|
|
||||||
import kotlinx.coroutines.flow.count
|
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.mapNotNull
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import kotlinx.coroutines.flow.toList
|
|
||||||
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
|
||||||
|
@ -171,6 +165,15 @@ open class DefaultEuiccChannelManager(
|
||||||
findEuiccChannelByPort(physicalSlotId, portId)
|
findEuiccChannelByPort(physicalSlotId, portId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 suspend fun <R> withEuiccChannel(
|
override suspend fun <R> withEuiccChannel(
|
||||||
physicalSlotId: Int,
|
physicalSlotId: Int,
|
||||||
portId: Int,
|
portId: Int,
|
||||||
|
@ -229,13 +232,6 @@ open class DefaultEuiccChannelManager(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun enumerateEuiccChannels(): List<EuiccChannel> =
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
flowEuiccPorts().mapNotNull { (slotId, portId) ->
|
|
||||||
findEuiccChannelByPort(slotId, portId)
|
|
||||||
}.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun flowEuiccPorts(): Flow<Pair<Int, Int>> = flow {
|
override fun flowEuiccPorts(): Flow<Pair<Int, Int>> = flow {
|
||||||
uiccCards.forEach { info ->
|
uiccCards.forEach { info ->
|
||||||
info.ports.forEach { port ->
|
info.ports.forEach { port ->
|
||||||
|
@ -251,20 +247,20 @@ open class DefaultEuiccChannelManager(
|
||||||
}
|
}
|
||||||
}.flowOn(Dispatchers.IO)
|
}.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
|
||||||
|
@ -272,7 +268,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() {
|
||||||
|
|
|
@ -19,21 +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>>
|
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)
|
||||||
|
@ -67,6 +72,12 @@ interface EuiccChannelManager {
|
||||||
suspend fun findEuiccChannelByPort(physicalSlotId: Int, portId: Int): EuiccChannel?
|
suspend fun findEuiccChannelByPort(physicalSlotId: Int, portId: Int): EuiccChannel?
|
||||||
fun findEuiccChannelByPortBlocking(physicalSlotId: Int, portId: Int): EuiccChannel?
|
fun findEuiccChannelByPortBlocking(physicalSlotId: Int, portId: Int): EuiccChannel?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the first mapped & available port ID for a physical slot, or -1 if
|
||||||
|
* not found.
|
||||||
|
*/
|
||||||
|
suspend fun findFirstAvailablePort(physicalSlotId: Int): Int
|
||||||
|
|
||||||
class EuiccChannelNotFoundException: Exception("EuiccChannel not found")
|
class EuiccChannelNotFoundException: Exception("EuiccChannel not found")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
|
@ -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
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,45 +142,64 @@ 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)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val (usbDevice, _) = withContext(Dispatchers.IO) {
|
newPages.add(
|
||||||
euiccChannelManager.enumerateUsbEuiccChannel()
|
Page(
|
||||||
}
|
channel.logicalSlotId,
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
loadingProgress.visibility = View.GONE
|
|
||||||
|
|
||||||
knownChannels.sortedBy { it.logicalSlotId }.forEach { channel ->
|
|
||||||
pages.add(Page(
|
|
||||||
getString(R.string.channel_name_format, channel.logicalSlotId)
|
getString(R.string.channel_name_format, channel.logicalSlotId)
|
||||||
) { appContainer.uiComponentFactory.createEuiccManagementFragment(channel) })
|
) {
|
||||||
|
appContainer.uiComponentFactory.createEuiccManagementFragment(
|
||||||
|
slotId,
|
||||||
|
portId
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
}.collect()
|
||||||
|
|
||||||
// If USB readers exist, add them at the very last
|
// If USB readers exist, add them at the very last
|
||||||
// We use a wrapper fragment to handle logic specific to USB readers
|
// We use a wrapper fragment to handle logic specific to USB readers
|
||||||
usbDevice?.let {
|
usbDevice?.let {
|
||||||
pages.add(Page(it.productName ?: getString(R.string.usb)) { UsbCcidReaderFragment() })
|
newPages.add(
|
||||||
|
Page(
|
||||||
|
EuiccChannelManager.USB_CHANNEL_ID,
|
||||||
|
it.productName ?: getString(R.string.usb)
|
||||||
|
) { UsbCcidReaderFragment() })
|
||||||
}
|
}
|
||||||
viewPager.visibility = View.VISIBLE
|
viewPager.visibility = View.VISIBLE
|
||||||
|
|
||||||
if (pages.size > 1) {
|
if (newPages.size > 1) {
|
||||||
tabs.visibility = View.VISIBLE
|
tabs.visibility = View.VISIBLE
|
||||||
} else if (pages.isEmpty()) {
|
} else if (newPages.isEmpty()) {
|
||||||
pages.add(Page("") { appContainer.uiComponentFactory.createNoEuiccPlaceholderFragment() })
|
newPages.add(
|
||||||
|
Page(
|
||||||
|
-1,
|
||||||
|
""
|
||||||
|
) { appContainer.uiComponentFactory.createNoEuiccPlaceholderFragment() })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newPages.sortBy { it.logicalSlotId }
|
||||||
|
|
||||||
|
pages.clear()
|
||||||
|
pages.addAll(newPages)
|
||||||
|
|
||||||
|
loadingProgress.visibility = View.GONE
|
||||||
pagerAdapter.notifyDataSetChanged()
|
pagerAdapter.notifyDataSetChanged()
|
||||||
// Reset the adapter so that the current view actually gets cleared
|
// Reset the adapter so that the current view actually gets cleared
|
||||||
// notifyDataSetChanged() doesn't cause the current view to be removed.
|
// notifyDataSetChanged() doesn't cause the current view to be removed.
|
||||||
|
@ -197,7 +220,6 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
||||||
|
|
||||||
refreshing = false
|
refreshing = false
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun refresh(fromUsbEvent: Boolean = false) {
|
private fun refresh(fromUsbEvent: Boolean = false) {
|
||||||
if (refreshing) return
|
if (refreshing) return
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -48,16 +48,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
|
||||||
|
|
|
@ -22,9 +22,12 @@
|
||||||
|
|
||||||
<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" />
|
||||||
|
</queries>
|
||||||
</manifest>
|
</manifest>
|
|
@ -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()
|
||||||
}
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package im.angry.openeuicc.ui
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import im.angry.easyeuicc.R
|
||||||
|
import im.angry.openeuicc.util.SIMToolkit
|
||||||
|
import im.angry.openeuicc.util.isUsb
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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).isVisible =
|
||||||
|
SIMToolkit.isAvailable(requireContext(), slotId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean =
|
||||||
|
when (item.itemId) {
|
||||||
|
R.id.open_sim_toolkit -> {
|
||||||
|
val intent = SIMToolkit.intent(requireContext(), slotId)
|
||||||
|
Log.d(TAG, "Opening SIM Toolkit for $slotId slot, intent: $intent")
|
||||||
|
startActivity(intent)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
package im.angry.openeuicc.util
|
||||||
|
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager.NameNotFoundException
|
||||||
|
|
||||||
|
object SIMToolkit {
|
||||||
|
private val packageNames = buildSet {
|
||||||
|
addAll(slot1activities.map { it.packageName })
|
||||||
|
addAll(slot2activities.map { it.packageName })
|
||||||
|
}
|
||||||
|
|
||||||
|
private val slot1activities = arrayOf(
|
||||||
|
ComponentName("com.android.stk", "com.android.stk.StkMain1"),
|
||||||
|
)
|
||||||
|
|
||||||
|
private val slot2activities = arrayOf(
|
||||||
|
ComponentName("com.android.stk", "com.android.stk.StkMain2"),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun getGeneralIntent(context: Context): Intent? {
|
||||||
|
for (packageName in packageNames) {
|
||||||
|
try {
|
||||||
|
return context.packageManager.getLaunchIntentForPackage(packageName)
|
||||||
|
} catch (_: NameNotFoundException) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getComponentName(context: Context, slotId: Int): ComponentName? {
|
||||||
|
val components = when (slotId) {
|
||||||
|
0 -> slot1activities
|
||||||
|
1 -> slot2activities
|
||||||
|
else -> return null
|
||||||
|
}
|
||||||
|
return components.find {
|
||||||
|
try {
|
||||||
|
context.packageManager.getActivityIcon(it)
|
||||||
|
true
|
||||||
|
} catch (_: NameNotFoundException) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isAvailable(context: Context, slotId: Int): Boolean {
|
||||||
|
if (getComponentName(context, slotId) != null) return true
|
||||||
|
if (getGeneralIntent(context) != null) return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun intent(context: Context, slotId: Int): Intent? {
|
||||||
|
val intent = Intent(Intent.ACTION_MAIN, null)
|
||||||
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
intent.component = getComponentName(context, slotId)
|
||||||
|
intent.addCategory(Intent.CATEGORY_LAUNCHER)
|
||||||
|
if (intent.component == null) return getGeneralIntent(context)
|
||||||
|
return intent
|
||||||
|
}
|
||||||
|
}
|
9
app-unpriv/src/main/res/menu/fragment_sim_toolkit.xml
Normal file
9
app-unpriv/src/main/res/menu/fragment_sim_toolkit.xml
Normal 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>
|
|
@ -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>
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
|
@ -13,6 +13,7 @@ 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.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.last
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import java.lang.IllegalStateException
|
import java.lang.IllegalStateException
|
||||||
|
|
||||||
|
@ -37,8 +38,11 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class EuiccChannelManagerContext(
|
private data class EuiccChannelManagerContext(
|
||||||
val euiccChannelManager: EuiccChannelManager
|
val euiccChannelManagerService: EuiccChannelManagerService
|
||||||
) {
|
) {
|
||||||
|
val euiccChannelManager
|
||||||
|
get() = euiccChannelManagerService.euiccChannelManager
|
||||||
|
|
||||||
fun findChannel(physicalSlotId: Int): EuiccChannel? =
|
fun findChannel(physicalSlotId: Int): EuiccChannel? =
|
||||||
euiccChannelManager.findEuiccChannelByPhysicalSlotBlocking(physicalSlotId)
|
euiccChannelManager.findEuiccChannelByPhysicalSlotBlocking(physicalSlotId)
|
||||||
|
|
||||||
|
@ -59,7 +63,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,8 +77,11 @@ 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
|
||||||
|
@ -177,12 +184,20 @@ 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
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return@withEuiccChannelManager euiccChannelManager.withEuiccChannel(
|
||||||
|
slotId,
|
||||||
|
port
|
||||||
|
) { channel ->
|
||||||
val profiles = channel.lpa.profiles.operational.map {
|
val profiles = channel.lpa.profiles.operational.map {
|
||||||
EuiccProfileInfo.Builder(it.iccid).apply {
|
EuiccProfileInfo.Builder(it.iccid).apply {
|
||||||
setProfileName(it.name)
|
setProfileName(it.name)
|
||||||
|
@ -204,12 +219,20 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
|
||||||
}.build()
|
}.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
return@withEuiccChannelManager GetEuiccProfileInfoListResult(
|
GetEuiccProfileInfoListResult(
|
||||||
RESULT_OK,
|
RESULT_OK,
|
||||||
profiles.toTypedArray(),
|
profiles.toTypedArray(),
|
||||||
channel.port.card.isRemovable
|
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 {
|
||||||
return EuiccInfo("Unknown") // TODO: Can we actually implement this?
|
return EuiccInfo("Unknown") // TODO: Can we actually implement this?
|
||||||
|
@ -335,13 +358,17 @@ 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
|
val success =
|
||||||
.setNickname(iccid, nickname!!)
|
(euiccChannelManagerService.launchProfileRenameTask(slotId, port, iccid, nickname!!)
|
||||||
|
?.last() as? EuiccChannelManagerService.ForegroundTaskState.Done)?.error == null
|
||||||
|
|
||||||
|
euiccChannelManager.withEuiccChannel(slotId, port) { channel ->
|
||||||
appContainer.subscriptionManager.tryRefreshCachedEuiccInfo(channel.cardId)
|
appContainer.subscriptionManager.tryRefreshCachedEuiccInfo(channel.cardId)
|
||||||
|
}
|
||||||
return@withEuiccChannelManager if (success) {
|
return@withEuiccChannelManager if (success) {
|
||||||
RESULT_OK
|
RESULT_OK
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -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 {
|
||||||
|
euiccManager.flowEuiccPorts().onEach { (slotId, portId) ->
|
||||||
|
euiccManager.withEuiccChannel(slotId, portId) {
|
||||||
if (!it.port.card.isRemovable) {
|
if (!it.port.card.isRemovable) {
|
||||||
it.lpa.disableActiveProfileWithUndo(false)
|
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.port.card.isRemovable) {
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue