forked from PeterCxy/OpenEUICC
Compare commits
No commits in common. "52c60d443f11a20ded42002d8aac236b1b763d9f" and "1dc500468179918bdea9f9c09e08cc0a21487ae7" have entirely different histories.
52c60d443f
...
1dc5004681
18 changed files with 151 additions and 353 deletions
|
@ -11,8 +11,14 @@ 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
|
||||||
|
@ -165,15 +171,6 @@ 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,
|
||||||
|
@ -232,6 +229,13 @@ 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 ->
|
||||||
|
@ -247,20 +251,20 @@ open class DefaultEuiccChannelManager(
|
||||||
}
|
}
|
||||||
}.flowOn(Dispatchers.IO)
|
}.flowOn(Dispatchers.IO)
|
||||||
|
|
||||||
override suspend fun tryOpenUsbEuiccChannel(): Pair<UsbDevice?, Boolean> =
|
override suspend fun enumerateUsbEuiccChannel(): Pair<UsbDevice?, EuiccChannel?> =
|
||||||
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, 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")
|
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, true)
|
return@withContext Pair(device, channel)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Ignored -- skip forward
|
// Ignored -- skip forward
|
||||||
|
@ -268,7 +272,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, false)
|
return@withContext Pair(null, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun invalidate() {
|
override fun invalidate() {
|
||||||
|
|
|
@ -19,26 +19,21 @@ interface EuiccChannelManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scan all possible _device internal_ sources for EuiccChannels, as a flow, return their physical
|
* Scan all possible _device internal_ sources for EuiccChannels, return them and have all
|
||||||
* (slotId, portId) and have all scanned channels cached; these channels will remain open
|
* scanned channels cached; these channels will remain open for the entire lifetime of
|
||||||
* for the entire lifetime of this EuiccChannelManager object, unless disconnected externally
|
* this EuiccChannelManager object, unless disconnected externally or invalidate()'d
|
||||||
* 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 will be false.
|
* to interact with the device, the second return value (EuiccChannel) will be null.
|
||||||
*
|
|
||||||
* 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 tryOpenUsbEuiccChannel(): Pair<UsbDevice?, Boolean>
|
suspend fun enumerateUsbEuiccChannel(): Pair<UsbDevice?, EuiccChannel?>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wait for a slot + port to reconnect (i.e. become valid again)
|
* Wait for a slot + port to reconnect (i.e. become valid again)
|
||||||
|
@ -72,12 +67,6 @@ 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,12 +1,13 @@
|
||||||
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(slotId: Int, portId: Int): EuiccManagementFragment =
|
override fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment =
|
||||||
EuiccManagementFragment.newInstance(slotId, portId)
|
EuiccManagementFragment.newInstance(channel.slotId, channel.portId)
|
||||||
|
|
||||||
override fun createNoEuiccPlaceholderFragment(): Fragment = NoEuiccPlaceholderFragment()
|
override fun createNoEuiccPlaceholderFragment(): Fragment = NoEuiccPlaceholderFragment()
|
||||||
}
|
}
|
|
@ -1,9 +1,10 @@
|
||||||
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(slotId: Int, portId: Int): EuiccManagementFragment
|
fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment
|
||||||
fun createNoEuiccPlaceholderFragment(): Fragment
|
fun createNoEuiccPlaceholderFragment(): Fragment
|
||||||
}
|
}
|
|
@ -3,8 +3,6 @@ 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
|
||||||
|
|
||||||
|
@ -12,32 +10,22 @@ 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.flowEuiccPorts().map { (slotId, portId) ->
|
euiccChannelManager.enumerateEuiccChannels()
|
||||||
euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
|
|
||||||
Triple(slotId, channel.logicalSlotId, portId)
|
|
||||||
}
|
|
||||||
}.toList().sortedBy { it.second }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
when {
|
when {
|
||||||
knownChannels.isEmpty() -> {
|
knownChannels.isEmpty() -> {
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
// Detect multiple eUICC chips
|
knownChannels.hasMultipleChips -> {
|
||||||
knownChannels.distinctBy { it.first }.size > 1 -> {
|
SlotSelectFragment.newInstance(knownChannels.sortedBy { it.logicalSlotId })
|
||||||
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(
|
onSlotSelected(knownChannels[0].slotId,
|
||||||
knownChannels[0].first,
|
knownChannels[0].portId)
|
||||||
knownChannels[0].third
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,12 +23,9 @@ 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
|
||||||
|
|
||||||
|
@ -47,7 +44,6 @@ 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
|
||||||
)
|
)
|
||||||
|
@ -142,83 +138,65 @@ 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 (usbDevice, _) = withContext(Dispatchers.IO) {
|
val knownChannels = withContext(Dispatchers.IO) {
|
||||||
euiccChannelManager.tryOpenUsbEuiccChannel()
|
euiccChannelManager.enumerateEuiccChannels().onEach {
|
||||||
}
|
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, channel.lpa.eID)
|
Log.d(TAG, it.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(channel.logicalSlotId)
|
euiccChannelManager.notifyEuiccProfilesChanged(it.logicalSlotId)
|
||||||
|
|
||||||
newPages.add(
|
|
||||||
Page(
|
|
||||||
channel.logicalSlotId,
|
|
||||||
getString(R.string.channel_name_format, channel.logicalSlotId)
|
|
||||||
) {
|
|
||||||
appContainer.uiComponentFactory.createEuiccManagementFragment(
|
|
||||||
slotId,
|
|
||||||
portId
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}.collect()
|
|
||||||
|
|
||||||
// If USB readers exist, add them at the very last
|
|
||||||
// We use a wrapper fragment to handle logic specific to USB readers
|
|
||||||
usbDevice?.let {
|
|
||||||
newPages.add(
|
|
||||||
Page(
|
|
||||||
EuiccChannelManager.USB_CHANNEL_ID,
|
|
||||||
it.productName ?: getString(R.string.usb)
|
|
||||||
) { UsbCcidReaderFragment() })
|
|
||||||
}
|
|
||||||
viewPager.visibility = View.VISIBLE
|
|
||||||
|
|
||||||
if (newPages.size > 1) {
|
|
||||||
tabs.visibility = View.VISIBLE
|
|
||||||
} else if (newPages.isEmpty()) {
|
|
||||||
newPages.add(
|
|
||||||
Page(
|
|
||||||
-1,
|
|
||||||
""
|
|
||||||
) { appContainer.uiComponentFactory.createNoEuiccPlaceholderFragment() })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
newPages.sortBy { it.logicalSlotId }
|
val (usbDevice, _) = withContext(Dispatchers.IO) {
|
||||||
|
euiccChannelManager.enumerateUsbEuiccChannel()
|
||||||
|
}
|
||||||
|
|
||||||
pages.clear()
|
withContext(Dispatchers.Main) {
|
||||||
pages.addAll(newPages)
|
loadingProgress.visibility = View.GONE
|
||||||
|
|
||||||
loadingProgress.visibility = View.GONE
|
knownChannels.sortedBy { it.logicalSlotId }.forEach { channel ->
|
||||||
pagerAdapter.notifyDataSetChanged()
|
pages.add(Page(
|
||||||
// Reset the adapter so that the current view actually gets cleared
|
getString(R.string.channel_name_format, channel.logicalSlotId)
|
||||||
// notifyDataSetChanged() doesn't cause the current view to be removed.
|
) { appContainer.uiComponentFactory.createEuiccManagementFragment(channel) })
|
||||||
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) {
|
// If USB readers exist, add them at the very last
|
||||||
ensureNotificationPermissions()
|
// 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) {
|
private fun refresh(fromUsbEvent: Boolean = false) {
|
||||||
|
|
|
@ -16,12 +16,12 @@ class SlotSelectFragment : BaseMaterialDialogFragment(), OpenEuiccContextMarker
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "SlotSelectFragment"
|
const val TAG = "SlotSelectFragment"
|
||||||
|
|
||||||
fun newInstance(slotIds: List<Int>, logicalSlotIds: List<Int>, portIds: List<Int>): SlotSelectFragment {
|
fun newInstance(knownChannels: List<EuiccChannel>): SlotSelectFragment {
|
||||||
return SlotSelectFragment().apply {
|
return SlotSelectFragment().apply {
|
||||||
arguments = Bundle().apply {
|
arguments = Bundle().apply {
|
||||||
putIntArray("slotIds", slotIds.toIntArray())
|
putIntArray("slotIds", knownChannels.map { it.slotId }.toIntArray())
|
||||||
putIntArray("logicalSlotIds", logicalSlotIds.toIntArray())
|
putIntArray("logicalSlotIds", knownChannels.map { it.logicalSlotId }.toIntArray())
|
||||||
putIntArray("portIds", portIds.toIntArray())
|
putIntArray("portIds", knownChannels.map { it.portId }.toIntArray())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ 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
|
||||||
|
@ -72,6 +73,7 @@ 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,
|
||||||
|
@ -138,26 +140,24 @@ class UsbCcidReaderFragment : Fragment(), OpenEuiccContextMarker {
|
||||||
permissionButton.visibility = View.GONE
|
permissionButton.visibility = View.GONE
|
||||||
loadingProgress.visibility = View.VISIBLE
|
loadingProgress.visibility = View.VISIBLE
|
||||||
|
|
||||||
val (device, canOpen) = withContext(Dispatchers.IO) {
|
val (device, channel) = withContext(Dispatchers.IO) {
|
||||||
euiccChannelManager.tryOpenUsbEuiccChannel()
|
euiccChannelManager.enumerateUsbEuiccChannel()
|
||||||
}
|
}
|
||||||
|
|
||||||
loadingProgress.visibility = View.GONE
|
loadingProgress.visibility = View.GONE
|
||||||
|
|
||||||
usbDevice = device
|
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.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 && canOpen) {
|
} else if (device != null && channel != null) {
|
||||||
childFragmentManager.commit {
|
childFragmentManager.commit {
|
||||||
replace(
|
replace(
|
||||||
R.id.child_container,
|
R.id.child_container,
|
||||||
appContainer.uiComponentFactory.createEuiccManagementFragment(
|
appContainer.uiComponentFactory.createEuiccManagementFragment(channel)
|
||||||
EuiccChannelManager.USB_CHANNEL_ID,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -48,21 +48,16 @@ fun LocalProfileAssistant.disableActiveProfile(refresh: Boolean): Boolean =
|
||||||
} ?: true
|
} ?: 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()
|
* 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 {
|
profiles.find { it.isEnabled }?.let {
|
||||||
Log.i(TAG, "Disabling active profile ${it.iccid}")
|
disableProfile(it.iccid, refreshOnDisable)
|
||||||
if (disableProfile(it.iccid, refresh)) {
|
return { enableProfile(it.iccid) }
|
||||||
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,12 +22,9 @@
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="im.angry.openeuicc.ui.CompatibilityCheckActivity"
|
android:name="im.angry.openeuicc.ui.CompatibilityCheckActivity"
|
||||||
android:exported="false"
|
android:label="@string/compatibility_check"
|
||||||
android:label="@string/compatibility_check" />
|
android:exported="false" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
<queries>
|
|
||||||
<package android:name="com.android.stk" />
|
|
||||||
</queries>
|
|
||||||
</manifest>
|
</manifest>
|
|
@ -1,14 +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.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()
|
||||||
}
|
}
|
|
@ -1,40 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,63 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
<?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,7 +2,6 @@
|
||||||
<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,9 +1,10 @@
|
||||||
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(slotId: Int, portId: Int): EuiccManagementFragment =
|
override fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment =
|
||||||
PrivilegedEuiccManagementFragment.newInstance(slotId, portId)
|
PrivilegedEuiccManagementFragment.newInstance(channel.slotId, channel.portId)
|
||||||
}
|
}
|
|
@ -13,7 +13,6 @@ 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
|
||||||
|
|
||||||
|
@ -38,11 +37,8 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class EuiccChannelManagerContext(
|
private data class EuiccChannelManagerContext(
|
||||||
val euiccChannelManagerService: EuiccChannelManagerService
|
val euiccChannelManager: EuiccChannelManager
|
||||||
) {
|
) {
|
||||||
val euiccChannelManager
|
|
||||||
get() = euiccChannelManagerService.euiccChannelManager
|
|
||||||
|
|
||||||
fun findChannel(physicalSlotId: Int): EuiccChannel? =
|
fun findChannel(physicalSlotId: Int): EuiccChannel? =
|
||||||
euiccChannelManager.findEuiccChannelByPhysicalSlotBlocking(physicalSlotId)
|
euiccChannelManager.findEuiccChannelByPhysicalSlotBlocking(physicalSlotId)
|
||||||
|
|
||||||
|
@ -63,7 +59,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: suspend EuiccChannelManagerContext.() -> T): T {
|
private fun <T> withEuiccChannelManager(fn: EuiccChannelManagerContext.() -> T): T {
|
||||||
val (binder, unbind) = runBlocking {
|
val (binder, unbind) = runBlocking {
|
||||||
bindServiceSuspended(
|
bindServiceSuspended(
|
||||||
Intent(
|
Intent(
|
||||||
|
@ -77,11 +73,8 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
|
||||||
throw RuntimeException("Unable to bind to EuiccChannelManagerService; aborting")
|
throw RuntimeException("Unable to bind to EuiccChannelManagerService; aborting")
|
||||||
}
|
}
|
||||||
|
|
||||||
val localBinder = binder as EuiccChannelManagerService.LocalBinder
|
val ret =
|
||||||
|
EuiccChannelManagerContext((binder as EuiccChannelManagerService.LocalBinder).service.euiccChannelManager).fn()
|
||||||
val ret = runBlocking {
|
|
||||||
EuiccChannelManagerContext(localBinder.service).fn()
|
|
||||||
}
|
|
||||||
|
|
||||||
unbind()
|
unbind()
|
||||||
return ret
|
return ret
|
||||||
|
@ -184,54 +177,38 @@ 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 port = euiccChannelManager.findFirstAvailablePort(slotId)
|
val channel =
|
||||||
if (port == -1) {
|
findChannel(slotId) ?: return@withEuiccChannelManager GetEuiccProfileInfoListResult(
|
||||||
return@withEuiccChannelManager GetEuiccProfileInfoListResult(
|
|
||||||
RESULT_FIRST_USER,
|
RESULT_FIRST_USER,
|
||||||
arrayOf(),
|
arrayOf(),
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
}
|
val profiles = channel.lpa.profiles.operational.map {
|
||||||
|
EuiccProfileInfo.Builder(it.iccid).apply {
|
||||||
try {
|
setProfileName(it.name)
|
||||||
return@withEuiccChannelManager euiccChannelManager.withEuiccChannel(
|
setNickname(it.displayName)
|
||||||
slotId,
|
setServiceProviderName(it.providerName)
|
||||||
port
|
setState(
|
||||||
) { channel ->
|
when (it.state) {
|
||||||
val profiles = channel.lpa.profiles.operational.map {
|
LocalProfileInfo.State.Enabled -> EuiccProfileInfo.PROFILE_STATE_ENABLED
|
||||||
EuiccProfileInfo.Builder(it.iccid).apply {
|
LocalProfileInfo.State.Disabled -> EuiccProfileInfo.PROFILE_STATE_DISABLED
|
||||||
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
|
|
||||||
)
|
)
|
||||||
}
|
setProfileClass(
|
||||||
} catch (e: EuiccChannelManager.EuiccChannelNotFoundException) {
|
when (it.profileClass) {
|
||||||
return@withEuiccChannelManager GetEuiccProfileInfoListResult(
|
LocalProfileInfo.Clazz.Testing -> EuiccProfileInfo.PROFILE_CLASS_TESTING
|
||||||
RESULT_FIRST_USER,
|
LocalProfileInfo.Clazz.Provisioning -> EuiccProfileInfo.PROFILE_CLASS_PROVISIONING
|
||||||
arrayOf(),
|
LocalProfileInfo.Clazz.Operational -> EuiccProfileInfo.PROFILE_CLASS_OPERATIONAL
|
||||||
true
|
}
|
||||||
)
|
)
|
||||||
|
}.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return@withEuiccChannelManager GetEuiccProfileInfoListResult(
|
||||||
|
RESULT_OK,
|
||||||
|
profiles.toTypedArray(),
|
||||||
|
channel.port.card.isRemovable
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onGetEuiccInfo(slotId: Int): EuiccInfo {
|
override fun onGetEuiccInfo(slotId: Int): EuiccInfo {
|
||||||
|
@ -358,17 +335,13 @@ 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 port = euiccChannelManager.findFirstAvailablePort(slotId)
|
val channel = findChannel(slotId) ?: return@withEuiccChannelManager RESULT_FIRST_USER
|
||||||
if (port < 0) {
|
if (!channel.profileExists(iccid)) {
|
||||||
return@withEuiccChannelManager RESULT_FIRST_USER
|
return@withEuiccChannelManager RESULT_FIRST_USER
|
||||||
}
|
}
|
||||||
val success =
|
val success = channel.lpa
|
||||||
(euiccChannelManagerService.launchProfileRenameTask(slotId, port, iccid, nickname!!)
|
.setNickname(iccid, nickname!!)
|
||||||
?.last() as? EuiccChannelManagerService.ForegroundTaskState.Done)?.error == null
|
appContainer.subscriptionManager.tryRefreshCachedEuiccInfo(channel.cardId)
|
||||||
|
|
||||||
euiccChannelManager.withEuiccChannel(slotId, port) { channel ->
|
|
||||||
appContainer.subscriptionManager.tryRefreshCachedEuiccInfo(channel.cardId)
|
|
||||||
}
|
|
||||||
return@withEuiccChannelManager if (success) {
|
return@withEuiccChannelManager if (success) {
|
||||||
RESULT_OK
|
RESULT_OK
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -5,7 +5,6 @@ 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
|
||||||
|
|
||||||
|
@ -16,14 +15,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)
|
||||||
runBlocking {
|
knownChannels.forEach {
|
||||||
euiccManager.flowEuiccPorts().onEach { (slotId, portId) ->
|
if (!it.port.card.isRemovable) {
|
||||||
euiccManager.withEuiccChannel(slotId, portId) {
|
it.lpa.disableActiveProfileWithUndo(false)
|
||||||
if (!it.port.card.isRemovable) {
|
|
||||||
it.lpa.disableActiveProfile(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,7 +31,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
|
||||||
suspend fun TelephonyManager.updateSimSlotMapping(
|
fun TelephonyManager.updateSimSlotMapping(
|
||||||
euiccManager: EuiccChannelManager, newMapping: Collection<UiccSlotMapping>,
|
euiccManager: EuiccChannelManager, newMapping: Collection<UiccSlotMapping>,
|
||||||
currentMapping: Collection<UiccSlotMapping> = simSlotMapping
|
currentMapping: Collection<UiccSlotMapping> = simSlotMapping
|
||||||
) {
|
) {
|
||||||
|
@ -43,24 +42,14 @@ suspend fun TelephonyManager.updateSimSlotMapping(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val undo: List<suspend () -> Unit> = unmapped.mapNotNull { mapping ->
|
val undo = unmapped.mapNotNull { mapping ->
|
||||||
euiccManager.withEuiccChannel(mapping.physicalSlotIndex, mapping.portIndex) { channel ->
|
euiccManager.findEuiccChannelByPortBlocking(mapping.physicalSlotIndex, mapping.portIndex)?.let { channel ->
|
||||||
if (!channel.port.card.isRemovable) {
|
if (!channel.port.card.isRemovable) {
|
||||||
channel.lpa.disableActiveProfileKeepIccId(false)
|
return@mapNotNull channel.lpa.disableActiveProfileWithUndo(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.
|
||||||
null
|
return@mapNotNull 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