Compare commits

..

10 commits

18 changed files with 355 additions and 153 deletions

View file

@ -11,14 +11,8 @@ import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.count
import kotlinx.coroutines.flow.flow
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.sync.Mutex
import kotlinx.coroutines.sync.withLock
@ -171,6 +165,15 @@ open class DefaultEuiccChannelManager(
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(
physicalSlotId: 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 {
uiccCards.forEach { info ->
info.ports.forEach { port ->
@ -251,20 +247,20 @@ open class DefaultEuiccChannelManager(
}
}.flowOn(Dispatchers.IO)
override suspend fun enumerateUsbEuiccChannel(): Pair<UsbDevice?, EuiccChannel?> =
override suspend fun tryOpenUsbEuiccChannel(): Pair<UsbDevice?, Boolean> =
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, 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")
try {
val channel = euiccChannelFactory.tryOpenUsbEuiccChannel(device, iface)
if (channel != null && channel.lpa.valid) {
usbChannel = channel
return@withContext Pair(device, channel)
return@withContext Pair(device, true)
}
} catch (e: Exception) {
// 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}")
}
return@withContext Pair(null, null)
return@withContext Pair(null, false)
}
override fun invalidate() {

View file

@ -19,21 +19,26 @@ interface EuiccChannelManager {
}
/**
* 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
* 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()`.
*/
suspend fun enumerateEuiccChannels(): List<EuiccChannel>
fun flowEuiccPorts(): Flow<Pair<Int, Int>>
/**
* 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 (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)
@ -67,6 +72,12 @@ interface EuiccChannelManager {
suspend fun findEuiccChannelByPort(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")
/**

View file

@ -1,13 +1,12 @@
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(channel: EuiccChannel): EuiccManagementFragment =
EuiccManagementFragment.newInstance(channel.slotId, channel.portId)
override fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment =
EuiccManagementFragment.newInstance(slotId, portId)
override fun createNoEuiccPlaceholderFragment(): Fragment = NoEuiccPlaceholderFragment()
}

View file

@ -1,10 +1,9 @@
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(channel: EuiccChannel): EuiccManagementFragment
fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment
fun createNoEuiccPlaceholderFragment(): Fragment
}

View file

@ -3,6 +3,8 @@ 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
@ -10,22 +12,32 @@ class DirectProfileDownloadActivity : BaseEuiccAccessActivity(), SlotSelectFragm
override fun onInit() {
lifecycleScope.launch {
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 {
knownChannels.isEmpty() -> {
finish()
}
knownChannels.hasMultipleChips -> {
SlotSelectFragment.newInstance(knownChannels.sortedBy { it.logicalSlotId })
// Detect multiple eUICC chips
knownChannels.distinctBy { it.first }.size > 1 -> {
SlotSelectFragment.newInstance(
knownChannels.map { it.first },
knownChannels.map { it.second },
knownChannels.map { it.third })
.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].slotId,
knownChannels[0].portId)
onSlotSelected(
knownChannels[0].first,
knownChannels[0].third
)
}
}
}

View file

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

View file

@ -16,12 +16,12 @@ class SlotSelectFragment : BaseMaterialDialogFragment(), OpenEuiccContextMarker
companion object {
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 {
arguments = Bundle().apply {
putIntArray("slotIds", knownChannels.map { it.slotId }.toIntArray())
putIntArray("logicalSlotIds", knownChannels.map { it.logicalSlotId }.toIntArray())
putIntArray("portIds", knownChannels.map { it.portId }.toIntArray())
putIntArray("slotIds", slotIds.toIntArray())
putIntArray("logicalSlotIds", logicalSlotIds.toIntArray())
putIntArray("portIds", portIds.toIntArray())
}
}
}

View file

@ -20,7 +20,6 @@ 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
@ -73,7 +72,6 @@ class UsbCcidReaderFragment : Fragment(), OpenEuiccContextMarker {
private lateinit var loadingProgress: ProgressBar
private var usbDevice: UsbDevice? = null
private var usbChannel: EuiccChannel? = null
override fun onCreateView(
inflater: LayoutInflater,
@ -140,24 +138,26 @@ class UsbCcidReaderFragment : Fragment(), OpenEuiccContextMarker {
permissionButton.visibility = View.GONE
loadingProgress.visibility = View.VISIBLE
val (device, channel) = withContext(Dispatchers.IO) {
euiccChannelManager.enumerateUsbEuiccChannel()
val (device, canOpen) = withContext(Dispatchers.IO) {
euiccChannelManager.tryOpenUsbEuiccChannel()
}
loadingProgress.visibility = View.GONE
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.visibility = View.VISIBLE
permissionButton.visibility = View.VISIBLE
} else if (device != null && channel != null) {
} else if (device != null && canOpen) {
childFragmentManager.commit {
replace(
R.id.child_container,
appContainer.uiComponentFactory.createEuiccManagementFragment(channel)
appContainer.uiComponentFactory.createEuiccManagementFragment(
EuiccChannelManager.USB_CHANNEL_ID,
0
)
)
}
} else {

View file

@ -48,16 +48,21 @@ fun LocalProfileAssistant.disableActiveProfile(refresh: Boolean): Boolean =
} ?: true
/**
* 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.
* Disable the current active profile if any. If refresh is true, also cause a refresh command.
* 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 {
disableProfile(it.iccid, refreshOnDisable)
return { enableProfile(it.iccid) }
} ?: { }
Log.i(TAG, "Disabling active profile ${it.iccid}")
if (disableProfile(it.iccid, refresh)) {
it.iccid
} else {
null
}
}
/**
* Begin a "tracked" operation where notifications may be generated by the eSIM

View file

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

View file

@ -1,9 +1,14 @@
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()
}

View file

@ -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)
}
}

View file

@ -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
}
}

View file

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

View file

@ -2,6 +2,7 @@
<string name="app_name" translatable="false">EasyEUICC</string>
<string name="channel_name_format">SIM %d</string>
<string name="compatibility_check">Compatibility Check</string>
<string name="open_sim_toolkit">Open SIM Toolkit</string>
<!-- Compatibility Check Descriptions -->
<string name="compatibility_check_system_features">System Features</string>

View file

@ -1,10 +1,9 @@
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(channel: EuiccChannel): EuiccManagementFragment =
PrivilegedEuiccManagementFragment.newInstance(channel.slotId, channel.portId)
override fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment =
PrivilegedEuiccManagementFragment.newInstance(slotId, portId)
}

View file

@ -13,6 +13,7 @@ import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.util.*
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.last
import kotlinx.coroutines.runBlocking
import java.lang.IllegalStateException
@ -37,8 +38,11 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
}
private data class EuiccChannelManagerContext(
val euiccChannelManager: EuiccChannelManager
val euiccChannelManagerService: EuiccChannelManagerService
) {
val euiccChannelManager
get() = euiccChannelManagerService.euiccChannelManager
fun findChannel(physicalSlotId: Int): EuiccChannel? =
euiccChannelManager.findEuiccChannelByPhysicalSlotBlocking(physicalSlotId)
@ -59,7 +63,7 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
*
* 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 {
bindServiceSuspended(
Intent(
@ -73,8 +77,11 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
throw RuntimeException("Unable to bind to EuiccChannelManagerService; aborting")
}
val ret =
EuiccChannelManagerContext((binder as EuiccChannelManagerService.LocalBinder).service.euiccChannelManager).fn()
val localBinder = binder as EuiccChannelManagerService.LocalBinder
val ret = runBlocking {
EuiccChannelManagerContext(localBinder.service).fn()
}
unbind()
return ret
@ -177,38 +184,54 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
}
// TODO: Temporarily enable the slot to access its profiles if it is currently unmapped
val channel =
findChannel(slotId) ?: return@withEuiccChannelManager GetEuiccProfileInfoListResult(
val port = euiccChannelManager.findFirstAvailablePort(slotId)
if (port == -1) {
return@withEuiccChannelManager GetEuiccProfileInfoListResult(
RESULT_FIRST_USER,
arrayOf(),
true
)
val profiles = channel.lpa.profiles.operational.map {
EuiccProfileInfo.Builder(it.iccid).apply {
setProfileName(it.name)
setNickname(it.displayName)
setServiceProviderName(it.providerName)
setState(
when (it.state) {
LocalProfileInfo.State.Enabled -> EuiccProfileInfo.PROFILE_STATE_ENABLED
LocalProfileInfo.State.Disabled -> EuiccProfileInfo.PROFILE_STATE_DISABLED
}
)
setProfileClass(
when (it.profileClass) {
LocalProfileInfo.Clazz.Testing -> EuiccProfileInfo.PROFILE_CLASS_TESTING
LocalProfileInfo.Clazz.Provisioning -> EuiccProfileInfo.PROFILE_CLASS_PROVISIONING
LocalProfileInfo.Clazz.Operational -> EuiccProfileInfo.PROFILE_CLASS_OPERATIONAL
}
)
}.build()
}
return@withEuiccChannelManager GetEuiccProfileInfoListResult(
RESULT_OK,
profiles.toTypedArray(),
channel.port.card.isRemovable
)
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
)
}
} catch (e: EuiccChannelManager.EuiccChannelNotFoundException) {
return@withEuiccChannelManager GetEuiccProfileInfoListResult(
RESULT_FIRST_USER,
arrayOf(),
true
)
}
}
override fun onGetEuiccInfo(slotId: Int): EuiccInfo {
@ -335,13 +358,17 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
"onUpdateSubscriptionNickname slotId=$slotId iccid=$iccid nickname=$nickname"
)
if (shouldIgnoreSlot(slotId)) return@withEuiccChannelManager RESULT_FIRST_USER
val channel = findChannel(slotId) ?: return@withEuiccChannelManager RESULT_FIRST_USER
if (!channel.profileExists(iccid)) {
val port = euiccChannelManager.findFirstAvailablePort(slotId)
if (port < 0) {
return@withEuiccChannelManager RESULT_FIRST_USER
}
val success = channel.lpa
.setNickname(iccid, nickname!!)
appContainer.subscriptionManager.tryRefreshCachedEuiccInfo(channel.cardId)
val success =
(euiccChannelManagerService.launchProfileRenameTask(slotId, port, iccid, nickname!!)
?.last() as? EuiccChannelManagerService.ForegroundTaskState.Done)?.error == null
euiccChannelManager.withEuiccChannel(slotId, port) { channel ->
appContainer.subscriptionManager.tryRefreshCachedEuiccInfo(channel.cardId)
}
return@withEuiccChannelManager if (success) {
RESULT_OK
} else {

View file

@ -5,6 +5,7 @@ import android.telephony.TelephonyManager
import android.telephony.UiccSlotMapping
import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.core.EuiccChannelManager
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import java.lang.Exception
@ -15,14 +16,14 @@ val TelephonyManager.dsdsEnabled: Boolean
get() = activeModemCount >= 2
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)
knownChannels.forEach {
if (!it.port.card.isRemovable) {
it.lpa.disableActiveProfileWithUndo(false)
runBlocking {
euiccManager.flowEuiccPorts().onEach { (slotId, portId) ->
euiccManager.withEuiccChannel(slotId, portId) {
if (!it.port.card.isRemovable) {
it.lpa.disableActiveProfile(false)
}
}
}
}
@ -31,7 +32,7 @@ fun TelephonyManager.setDsdsEnabled(euiccManager: EuiccChannelManager, enabled:
// Disable eSIM profiles before switching the slot mapping
// This ensures that unmapped eSIM ports never have "ghost" profiles enabled
fun TelephonyManager.updateSimSlotMapping(
suspend fun TelephonyManager.updateSimSlotMapping(
euiccManager: EuiccChannelManager, newMapping: Collection<UiccSlotMapping>,
currentMapping: Collection<UiccSlotMapping> = simSlotMapping
) {
@ -42,14 +43,24 @@ fun TelephonyManager.updateSimSlotMapping(
}
}
val undo = unmapped.mapNotNull { mapping ->
euiccManager.findEuiccChannelByPortBlocking(mapping.physicalSlotIndex, mapping.portIndex)?.let { channel ->
val undo: List<suspend () -> Unit> = unmapped.mapNotNull { mapping ->
euiccManager.withEuiccChannel(mapping.physicalSlotIndex, mapping.portIndex) { channel ->
if (!channel.port.card.isRemovable) {
return@mapNotNull channel.lpa.disableActiveProfileWithUndo(false)
channel.lpa.disableActiveProfileKeepIccId(false)
} else {
// Do not do anything for external eUICCs -- we can't really trust them to work properly
// 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)
}
}
}
}