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.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() {

View file

@ -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")
/** /**

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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="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>

View file

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

View file

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

View file

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