forked from PeterCxy/OpenEUICC
Compare commits
No commits in common. "52c60d443f11a20ded42002d8aac236b1b763d9f" and "1dc500468179918bdea9f9c09e08cc0a21487ae7" have entirely different histories.
52c60d443f
...
1dc5004681
18 changed files with 151 additions and 353 deletions
|
@ -11,8 +11,14 @@ import im.angry.openeuicc.util.*
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.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() {
|
||||
|
|
|
@ -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")
|
||||
|
||||
/**
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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()
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
package im.angry.openeuicc.ui
|
||||
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import im.angry.easyeuicc.R
|
||||
import im.angry.openeuicc.util.SIMToolkit
|
||||
import im.angry.openeuicc.util.isUsb
|
||||
import im.angry.openeuicc.util.newInstanceEuicc
|
||||
import im.angry.openeuicc.util.slotId
|
||||
|
||||
|
||||
class UnprivilegedEuiccManagementFragment : EuiccManagementFragment() {
|
||||
companion object {
|
||||
const val TAG = "UnprivilegedEuiccManagementFragment"
|
||||
|
||||
fun newInstance(slotId: Int, portId: Int): EuiccManagementFragment =
|
||||
newInstanceEuicc(UnprivilegedEuiccManagementFragment::class.java, slotId, portId)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
inflater.inflate(R.menu.fragment_sim_toolkit, menu)
|
||||
menu.findItem(R.id.open_sim_toolkit).isVisible =
|
||||
SIMToolkit.isAvailable(requireContext(), slotId)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean =
|
||||
when (item.itemId) {
|
||||
R.id.open_sim_toolkit -> {
|
||||
val intent = SIMToolkit.intent(requireContext(), slotId)
|
||||
Log.d(TAG, "Opening SIM Toolkit for $slotId slot, intent: $intent")
|
||||
startActivity(intent)
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
package im.angry.openeuicc.util
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager.NameNotFoundException
|
||||
|
||||
object SIMToolkit {
|
||||
private val packageNames = buildSet {
|
||||
addAll(slot1activities.map { it.packageName })
|
||||
addAll(slot2activities.map { it.packageName })
|
||||
}
|
||||
|
||||
private val slot1activities = arrayOf(
|
||||
ComponentName("com.android.stk", "com.android.stk.StkMain1"),
|
||||
)
|
||||
|
||||
private val slot2activities = arrayOf(
|
||||
ComponentName("com.android.stk", "com.android.stk.StkMain2"),
|
||||
)
|
||||
|
||||
private fun getGeneralIntent(context: Context): Intent? {
|
||||
for (packageName in packageNames) {
|
||||
try {
|
||||
return context.packageManager.getLaunchIntentForPackage(packageName)
|
||||
} catch (_: NameNotFoundException) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getComponentName(context: Context, slotId: Int): ComponentName? {
|
||||
val components = when (slotId) {
|
||||
0 -> slot1activities
|
||||
1 -> slot2activities
|
||||
else -> return null
|
||||
}
|
||||
return components.find {
|
||||
try {
|
||||
context.packageManager.getActivityIcon(it)
|
||||
true
|
||||
} catch (_: NameNotFoundException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isAvailable(context: Context, slotId: Int): Boolean {
|
||||
if (getComponentName(context, slotId) != null) return true
|
||||
if (getGeneralIntent(context) != null) return true
|
||||
return false
|
||||
}
|
||||
|
||||
fun intent(context: Context, slotId: Int): Intent? {
|
||||
val intent = Intent(Intent.ACTION_MAIN, null)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
intent.component = getComponentName(context, slotId)
|
||||
intent.addCategory(Intent.CATEGORY_LAUNCHER)
|
||||
if (intent.component == null) return getGeneralIntent(context)
|
||||
return intent
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/open_sim_toolkit"
|
||||
android:title="@string/open_sim_toolkit"
|
||||
android:visible="false"
|
||||
app:showAsAction="never" />
|
||||
</menu>
|
|
@ -2,7 +2,6 @@
|
|||
<string name="app_name" translatable="false">EasyEUICC</string>
|
||||
<string name="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>
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue