Compare commits

..

No commits in common. "52c60d443f11a20ded42002d8aac236b1b763d9f" and "1dc500468179918bdea9f9c09e08cc0a21487ae7" have entirely different histories.

18 changed files with 151 additions and 353 deletions

View file

@ -11,8 +11,14 @@ 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
@ -165,15 +171,6 @@ 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,
@ -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 {
uiccCards.forEach { info ->
info.ports.forEach { port ->
@ -247,20 +251,20 @@ open class DefaultEuiccChannelManager(
}
}.flowOn(Dispatchers.IO)
override suspend fun tryOpenUsbEuiccChannel(): Pair<UsbDevice?, Boolean> =
override suspend fun enumerateUsbEuiccChannel(): Pair<UsbDevice?, EuiccChannel?> =
withContext(Dispatchers.IO) {
usbManager.deviceList.values.forEach { device ->
Log.i(TAG, "Scanning USB device ${device.deviceId}:${device.vendorId}")
val iface = device.getSmartCardInterface() ?: return@forEach
// If we don't have permission, tell UI code that we found a candidate device, but we
// need permission to be able to do anything with it
if (!usbManager.hasPermission(device)) return@withContext Pair(device, false)
if (!usbManager.hasPermission(device)) return@withContext Pair(device, null)
Log.i(TAG, "Found CCID interface on ${device.deviceId}:${device.vendorId}, and has permission; trying to open channel")
try {
val channel = euiccChannelFactory.tryOpenUsbEuiccChannel(device, iface)
if (channel != null && channel.lpa.valid) {
usbChannel = channel
return@withContext Pair(device, true)
return@withContext Pair(device, channel)
}
} catch (e: Exception) {
// Ignored -- skip forward
@ -268,7 +272,7 @@ open class DefaultEuiccChannelManager(
}
Log.i(TAG, "No valid eUICC channel found on USB device ${device.deviceId}:${device.vendorId}")
}
return@withContext Pair(null, false)
return@withContext Pair(null, null)
}
override fun invalidate() {

View file

@ -19,26 +19,21 @@ interface EuiccChannelManager {
}
/**
* Scan all possible _device internal_ sources for EuiccChannels, as a flow, return their physical
* (slotId, portId) and have all scanned channels cached; these channels will remain open
* for the entire lifetime of this EuiccChannelManager object, unless disconnected externally
* or invalidate()'d.
*
* To obtain a temporary reference to a EuiccChannel, use `withEuiccChannel()`.
* Scan all possible _device internal_ sources for EuiccChannels, return them and have all
* scanned channels cached; these channels will remain open for the entire lifetime of
* this EuiccChannelManager object, unless disconnected externally or invalidate()'d
*/
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 will be false.
*
* Returns (usbDevice, canOpen). canOpen is false if either (1) no usb reader is found;
* or (2) usb reader is found, but user interaction is required for access;
* or (3) usb reader is found, but we are unable to open ISD-R.
* to interact with the device, the second return value (EuiccChannel) will be null.
*/
suspend fun tryOpenUsbEuiccChannel(): Pair<UsbDevice?, Boolean>
suspend fun enumerateUsbEuiccChannel(): Pair<UsbDevice?, EuiccChannel?>
/**
* 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?
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,12 +1,13 @@
package im.angry.openeuicc.di
import androidx.fragment.app.Fragment
import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.ui.EuiccManagementFragment
import im.angry.openeuicc.ui.NoEuiccPlaceholderFragment
open class DefaultUiComponentFactory : UiComponentFactory {
override fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment =
EuiccManagementFragment.newInstance(slotId, portId)
override fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment =
EuiccManagementFragment.newInstance(channel.slotId, channel.portId)
override fun createNoEuiccPlaceholderFragment(): Fragment = NoEuiccPlaceholderFragment()
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -48,21 +48,16 @@ fun LocalProfileAssistant.disableActiveProfile(refresh: Boolean): Boolean =
} ?: true
/**
* Disable the current active profile if any. If refresh is true, also cause a refresh command.
* Disable the active profile, return a lambda that reverts this action when called.
* If refreshOnDisable is true, also cause a eUICC refresh command. Note that refreshing
* will disconnect the eUICC and might need some time before being operational again.
* See EuiccManager.waitForReconnect()
*
* Return the iccid of the profile being disabled, or null if no active profile found or failed to
* disable.
*/
fun LocalProfileAssistant.disableActiveProfileKeepIccId(refresh: Boolean): String? =
fun LocalProfileAssistant.disableActiveProfileWithUndo(refreshOnDisable: Boolean): () -> Unit =
profiles.find { it.isEnabled }?.let {
Log.i(TAG, "Disabling active profile ${it.iccid}")
if (disableProfile(it.iccid, refresh)) {
it.iccid
} else {
null
}
}
disableProfile(it.iccid, refreshOnDisable)
return { enableProfile(it.iccid) }
} ?: { }
/**
* Begin a "tracked" operation where notifications may be generated by the eSIM

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,7 +5,6 @@ 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
@ -16,14 +15,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)
runBlocking {
euiccManager.flowEuiccPorts().onEach { (slotId, portId) ->
euiccManager.withEuiccChannel(slotId, portId) {
if (!it.port.card.isRemovable) {
it.lpa.disableActiveProfile(false)
}
}
knownChannels.forEach {
if (!it.port.card.isRemovable) {
it.lpa.disableActiveProfileWithUndo(false)
}
}
@ -32,7 +31,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
suspend fun TelephonyManager.updateSimSlotMapping(
fun TelephonyManager.updateSimSlotMapping(
euiccManager: EuiccChannelManager, newMapping: Collection<UiccSlotMapping>,
currentMapping: Collection<UiccSlotMapping> = simSlotMapping
) {
@ -43,24 +42,14 @@ suspend fun TelephonyManager.updateSimSlotMapping(
}
}
val undo: List<suspend () -> Unit> = unmapped.mapNotNull { mapping ->
euiccManager.withEuiccChannel(mapping.physicalSlotIndex, mapping.portIndex) { channel ->
val undo = unmapped.mapNotNull { mapping ->
euiccManager.findEuiccChannelByPortBlocking(mapping.physicalSlotIndex, mapping.portIndex)?.let { channel ->
if (!channel.port.card.isRemovable) {
channel.lpa.disableActiveProfileKeepIccId(false)
return@mapNotNull channel.lpa.disableActiveProfileWithUndo(false)
} else {
// Do not do anything for external eUICCs -- we can't really trust them to work properly
// with no profile enabled.
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)
}
return@mapNotNull null
}
}
}