WIP: feat: Support for removable eSIM with multiple SEs #239

Draft
PeterCxy wants to merge 11 commits from multi-se into master
35 changed files with 595 additions and 236 deletions

View file

@ -20,7 +20,8 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
override suspend fun tryOpenEuiccChannel( override suspend fun tryOpenEuiccChannel(
port: UiccPortInfoCompat, port: UiccPortInfoCompat,
isdrAid: ByteArray isdrAid: ByteArray,
seId: EuiccChannel.SecureElementId,
): EuiccChannel? = try { ): EuiccChannel? = try {
if (port.portIndex != 0) { if (port.portIndex != 0) {
Log.w( Log.w(
@ -45,6 +46,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
context.preferenceRepository.verboseLoggingFlow context.preferenceRepository.verboseLoggingFlow
), ),
isdrAid, isdrAid,
seId,
context.preferenceRepository.verboseLoggingFlow, context.preferenceRepository.verboseLoggingFlow,
context.preferenceRepository.ignoreTLSCertificateFlow, context.preferenceRepository.ignoreTLSCertificateFlow,
context.preferenceRepository.es10xMssFlow, context.preferenceRepository.es10xMssFlow,
@ -60,7 +62,8 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
override fun tryOpenUsbEuiccChannel( override fun tryOpenUsbEuiccChannel(
ccidCtx: UsbCcidContext, ccidCtx: UsbCcidContext,
isdrAid: ByteArray isdrAid: ByteArray,
seId: EuiccChannel.SecureElementId
): EuiccChannel? = try { ): EuiccChannel? = try {
EuiccChannelImpl( EuiccChannelImpl(
context.getString(R.string.channel_type_usb), context.getString(R.string.channel_type_usb),
@ -70,6 +73,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
ccidCtx ccidCtx
), ),
isdrAid, isdrAid,
seId,
context.preferenceRepository.verboseLoggingFlow, context.preferenceRepository.verboseLoggingFlow,
context.preferenceRepository.ignoreTLSCertificateFlow, context.preferenceRepository.ignoreTLSCertificateFlow,
context.preferenceRepository.es10xMssFlow, context.preferenceRepository.es10xMssFlow,

View file

@ -32,7 +32,7 @@ open class DefaultEuiccChannelManager(
private val channelCache = mutableListOf<EuiccChannel>() private val channelCache = mutableListOf<EuiccChannel>()
private var usbChannel: EuiccChannel? = null private var usbChannels = mutableListOf<EuiccChannel>()
private val lock = Mutex() private val lock = Mutex()
@ -51,15 +51,20 @@ open class DefaultEuiccChannelManager(
protected open val uiccCards: Collection<UiccCardInfoCompat> protected open val uiccCards: Collection<UiccCardInfoCompat>
get() = (0..<tm.activeModemCountCompat).map { FakeUiccCardInfoCompat(it) } get() = (0..<tm.activeModemCountCompat).map { FakeUiccCardInfoCompat(it) }
private suspend inline fun tryOpenChannelFirstValidAid(openFn: (ByteArray) -> EuiccChannel?): EuiccChannel? { private suspend inline fun tryOpenChannelWithKnownAids(openFn: (ByteArray, EuiccChannel.SecureElementId) -> EuiccChannel?): List<EuiccChannel> {
val isdrAidList = val isdrAidList =
parseIsdrAidList(appContainer.preferenceRepository.isdrAidListFlow.first()) parseIsdrAidList(appContainer.preferenceRepository.isdrAidListFlow.first())
var seId = 0
return isdrAidList.firstNotNullOfOrNull { return isdrAidList.mapNotNull {
Log.i(TAG, "Opening channel, trying ISDR AID ${it.encodeHex()}") Log.i(
TAG,
"Opening channel, trying ISDR AID ${it.encodeHex()}, this will be seId $seId"
)
openFn(it)?.let { channel -> openFn(it, EuiccChannel.SecureElementId.createFromInt(seId))?.let { channel ->
if (channel.valid) { if (channel.valid) {
seId += 1
channel channel
} else { } else {
channel.close() channel.close()
@ -69,19 +74,18 @@ open class DefaultEuiccChannelManager(
} }
} }
private suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel? { private suspend fun tryOpenEuiccChannel(
port: UiccPortInfoCompat,
seId: EuiccChannel.SecureElementId = EuiccChannel.SecureElementId.DEFAULT
): EuiccChannel? {
lock.withLock { lock.withLock {
if (port.card.physicalSlotIndex == EuiccChannelManager.USB_CHANNEL_ID) { if (port.card.physicalSlotIndex == EuiccChannelManager.USB_CHANNEL_ID) {
return if (usbChannel != null && usbChannel!!.valid) { // We only compare seId because we assume we can only open 1 card from USB
usbChannel return usbChannels.find { it.seId == seId }
} else {
usbChannel = null
null
}
} }
val existing = val existing =
channelCache.find { it.slotId == port.card.physicalSlotIndex && it.portId == port.portIndex } channelCache.find { it.slotId == port.card.physicalSlotIndex && it.portId == port.portIndex && it.seId == seId }
if (existing != null) { if (existing != null) {
if (existing.valid && port.logicalSlotIndex == existing.logicalSlotId) { if (existing.valid && port.logicalSlotIndex == existing.logicalSlotId) {
return existing return existing
@ -96,12 +100,18 @@ open class DefaultEuiccChannelManager(
return null return null
} }
val channel = val channels =
tryOpenChannelFirstValidAid { euiccChannelFactory.tryOpenEuiccChannel(port, it) } tryOpenChannelWithKnownAids { isdrAid, seId ->
euiccChannelFactory.tryOpenEuiccChannel(
port,
isdrAid,
seId
)
}
if (channel != null) { if (channels.isNotEmpty()) {
channelCache.add(channel) channelCache.addAll(channels)
return channel return channels.find { it.seId == seId }
} else { } else {
Log.i( Log.i(
TAG, TAG,
@ -112,10 +122,13 @@ open class DefaultEuiccChannelManager(
} }
} }
protected suspend fun findEuiccChannelByLogicalSlot(logicalSlotId: Int): EuiccChannel? = protected suspend fun findEuiccChannelByLogicalSlot(
logicalSlotId: Int,
seId: EuiccChannel.SecureElementId = EuiccChannel.SecureElementId.DEFAULT
): EuiccChannel? =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
return@withContext usbChannel return@withContext usbChannels.find { it.seId == seId }
} }
for (card in uiccCards) { for (card in uiccCards) {
@ -131,7 +144,7 @@ open class DefaultEuiccChannelManager(
private suspend fun findAllEuiccChannelsByPhysicalSlot(physicalSlotId: Int): List<EuiccChannel>? { private suspend fun findAllEuiccChannelsByPhysicalSlot(physicalSlotId: Int): List<EuiccChannel>? {
if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
return usbChannel?.let { listOf(it) } return usbChannels.ifEmpty { null }
} }
for (card in uiccCards) { for (card in uiccCards) {
@ -142,14 +155,18 @@ open class DefaultEuiccChannelManager(
return null return null
} }
private suspend fun findEuiccChannelByPort(physicalSlotId: Int, portId: Int): EuiccChannel? = private suspend fun findEuiccChannelByPort(
physicalSlotId: Int,
portId: Int,
seId: EuiccChannel.SecureElementId = EuiccChannel.SecureElementId.DEFAULT
): EuiccChannel? =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
return@withContext usbChannel return@withContext usbChannels.find { it.seId == seId }
} }
uiccCards.find { it.physicalSlotIndex == physicalSlotId }?.let { card -> uiccCards.find { it.physicalSlotIndex == physicalSlotId }?.let { card ->
card.ports.find { it.portIndex == portId }?.let { tryOpenEuiccChannel(it) } card.ports.find { it.portIndex == portId }?.let { tryOpenEuiccChannel(it, seId) }
} }
} }
@ -168,15 +185,17 @@ open class DefaultEuiccChannelManager(
return@withContext listOf(0) return@withContext listOf(0)
} }
findAllEuiccChannelsByPhysicalSlot(physicalSlotId)?.map { it.portId } ?: listOf() findAllEuiccChannelsByPhysicalSlot(physicalSlotId)?.map { it.portId }?.toSet()?.toList()
?: listOf()
} }
override suspend fun <R> withEuiccChannel( override suspend fun <R> withEuiccChannel(
physicalSlotId: Int, physicalSlotId: Int,
portId: Int, portId: Int,
seId: EuiccChannel.SecureElementId,
fn: suspend (EuiccChannel) -> R fn: suspend (EuiccChannel) -> R
): R { ): R {
val channel = findEuiccChannelByPort(physicalSlotId, portId) val channel = findEuiccChannelByPort(physicalSlotId, portId, seId)
?: throw EuiccChannelManager.EuiccChannelNotFoundException() ?: throw EuiccChannelManager.EuiccChannelNotFoundException()
val wrapper = EuiccChannelWrapper(channel) val wrapper = EuiccChannelWrapper(channel)
try { try {
@ -190,9 +209,10 @@ open class DefaultEuiccChannelManager(
override suspend fun <R> withEuiccChannel( override suspend fun <R> withEuiccChannel(
logicalSlotId: Int, logicalSlotId: Int,
seId: EuiccChannel.SecureElementId,
fn: suspend (EuiccChannel) -> R fn: suspend (EuiccChannel) -> R
): R { ): R {
val channel = findEuiccChannelByLogicalSlot(logicalSlotId) val channel = findEuiccChannelByLogicalSlot(logicalSlotId, seId)
?: throw EuiccChannelManager.EuiccChannelNotFoundException() ?: throw EuiccChannelManager.EuiccChannelNotFoundException()
val wrapper = EuiccChannelWrapper(channel) val wrapper = EuiccChannelWrapper(channel)
try { try {
@ -206,8 +226,8 @@ open class DefaultEuiccChannelManager(
override suspend fun waitForReconnect(physicalSlotId: Int, portId: Int, timeoutMillis: Long) { override suspend fun waitForReconnect(physicalSlotId: Int, portId: Int, timeoutMillis: Long) {
if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
usbChannel?.close() usbChannels.forEach { it.close() }
usbChannel = null usbChannels.clear()
} else { } else {
// If there is already a valid channel, we close it proactively // If there is already a valid channel, we close it proactively
// Sometimes the current channel can linger on for a bit even after it should have become invalid // Sometimes the current channel can linger on for a bit even after it should have become invalid
@ -223,7 +243,7 @@ open class DefaultEuiccChannelManager(
// tryOpenUsbEuiccChannel() will always try to reopen the channel, even if // tryOpenUsbEuiccChannel() will always try to reopen the channel, even if
// a USB channel already exists // a USB channel already exists
tryOpenUsbEuiccChannel() tryOpenUsbEuiccChannel()
usbChannel!! usbChannels.getOrNull(0)!!
} else { } else {
// tryOpenEuiccChannel() will automatically dispose of invalid channels // tryOpenEuiccChannel() will automatically dispose of invalid channels
// and recreate when needed // and recreate when needed
@ -264,6 +284,20 @@ open class DefaultEuiccChannelManager(
} }
}) })
override fun flowEuiccSecureElements(
slotId: Int,
portId: Int
): Flow<EuiccChannel.SecureElementId> = flow {
// Emit the "default" channel first
// TODO: This function below should really return a list, not just one SE
findEuiccChannelByPort(slotId, portId, seId = EuiccChannel.SecureElementId.DEFAULT)?.let {
emit(EuiccChannel.SecureElementId.DEFAULT)
channelCache.filter { it.slotId == slotId && it.portId == portId && it.seId != EuiccChannel.SecureElementId.DEFAULT }
.forEach { emit(it.seId) }
}
}
override suspend fun tryOpenUsbEuiccChannel(): Pair<UsbDevice?, Boolean> = override suspend fun tryOpenUsbEuiccChannel(): Pair<UsbDevice?, Boolean> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
usbManager.deviceList.values.forEach { device -> usbManager.deviceList.values.forEach { device ->
@ -277,15 +311,17 @@ open class DefaultEuiccChannelManager(
"Found CCID interface on ${device.deviceId}:${device.vendorId}, and has permission; trying to open channel" "Found CCID interface on ${device.deviceId}:${device.vendorId}, and has permission; trying to open channel"
) )
val ccidCtx = UsbCcidContext.createFromUsbDevice(context, device, iface) ?: return@forEach val ccidCtx =
UsbCcidContext.createFromUsbDevice(context, device, iface) ?: return@forEach
try { try {
val channel = tryOpenChannelFirstValidAid { val channels = tryOpenChannelWithKnownAids { isdrAid, seId ->
euiccChannelFactory.tryOpenUsbEuiccChannel(ccidCtx, it) euiccChannelFactory.tryOpenUsbEuiccChannel(ccidCtx, isdrAid, seId)
} }
if (channel != null && channel.lpa.valid) { if (channels.isNotEmpty() && channels[0].valid) {
ccidCtx.allowDisconnect = true ccidCtx.allowDisconnect = true
usbChannel = channel usbChannels.clear()
usbChannels.addAll(channels)
return@withContext Pair(device, true) return@withContext Pair(device, true)
} }
} catch (e: Exception) { } catch (e: Exception) {
@ -309,8 +345,8 @@ open class DefaultEuiccChannelManager(
channel.close() channel.close()
} }
usbChannel?.close() usbChannels.forEach { it.close() }
usbChannel = null usbChannels.clear()
channelCache.clear() channelCache.clear()
euiccChannelFactory.cleanup() euiccChannelFactory.cleanup()
} }

View file

@ -1,5 +1,7 @@
package im.angry.openeuicc.core package im.angry.openeuicc.core
import android.os.Parcel
import android.os.Parcelable
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import net.typeblog.lpac_jni.ApduInterface import net.typeblog.lpac_jni.ApduInterface
import net.typeblog.lpac_jni.LocalProfileAssistant import net.typeblog.lpac_jni.LocalProfileAssistant
@ -13,6 +15,59 @@ interface EuiccChannel {
val logicalSlotId: Int val logicalSlotId: Int
val portId: Int val portId: Int
/**
* A semi-obscure wrapper over the integer ID of a secure element on a card.
*
* Because the ID is arbitrary, this is intended to discourage the use of the
* integer value directly. Additionally, it prevents accidentally calling the
* wrong function in EuiccChannelManager with a ton of integer parameters.
*/
class SecureElementId private constructor(val id: Int) : Parcelable {
companion object {
val DEFAULT = SecureElementId(0)
/**
* Create a SecureElementId from an integer ID. You should not call this directly
* unless you know what you're doing.
*
* This is currently only ever used in the download flow.
*/
fun createFromInt(id: Int): SecureElementId =
SecureElementId(id)
@Suppress("unused")
@JvmField
val CREATOR = object : Parcelable.Creator<SecureElementId> {
override fun createFromParcel(parcel: Parcel): SecureElementId =
createFromInt(parcel.readInt())
override fun newArray(size: Int): Array<SecureElementId?> = arrayOfNulls(size)
}
}
override fun hashCode(): Int =
id.hashCode()
override fun equals(other: Any?): Boolean =
if (other is SecureElementId) {
this.id == other.id
} else {
super.equals(other)
}
override fun describeContents(): Int = id
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(id)
}
}
/**
* Some chips support multiple SEs on one chip. The seId here is intended
* to distinguish channels opened from these different SEs.
*/
val seId: SecureElementId
val lpa: LocalProfileAssistant val lpa: LocalProfileAssistant
val valid: Boolean val valid: Boolean

View file

@ -6,11 +6,12 @@ import im.angry.openeuicc.util.*
// This class is here instead of inside DI because it contains a bit more logic than just // This class is here instead of inside DI because it contains a bit more logic than just
// "dumb" dependency injection. // "dumb" dependency injection.
interface EuiccChannelFactory { interface EuiccChannelFactory {
suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat, isdrAid: ByteArray): EuiccChannel? suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat, isdrAid: ByteArray, seId: EuiccChannel.SecureElementId): EuiccChannel?
fun tryOpenUsbEuiccChannel( fun tryOpenUsbEuiccChannel(
ccidCtx: UsbCcidContext, ccidCtx: UsbCcidContext,
isdrAid: ByteArray isdrAid: ByteArray,
seId: EuiccChannel.SecureElementId
): EuiccChannel? ): EuiccChannel?
/** /**

View file

@ -15,6 +15,7 @@ class EuiccChannelImpl(
override val intrinsicChannelName: String?, override val intrinsicChannelName: String?,
override val apduInterface: ApduInterface, override val apduInterface: ApduInterface,
override val isdrAid: ByteArray, override val isdrAid: ByteArray,
override val seId: EuiccChannel.SecureElementId,
verboseLoggingFlow: Flow<Boolean>, verboseLoggingFlow: Flow<Boolean>,
ignoreTLSCertificateFlow: Flow<Boolean>, ignoreTLSCertificateFlow: Flow<Boolean>,
es10xMssFlow: Flow<Int>, es10xMssFlow: Flow<Int>,

View file

@ -37,6 +37,14 @@ interface EuiccChannelManager {
*/ */
fun flowAllOpenEuiccPorts(): Flow<Pair<Int, Int>> fun flowAllOpenEuiccPorts(): Flow<Pair<Int, Int>>
/**
* Iterate over all the Secure Elements available on one eUICC.
*
* This is going to almost always return only 1 result, except in the case where
* a card has multiple SEs.
*/
fun flowEuiccSecureElements(slotId: Int, portId: Int): Flow<EuiccChannel.SecureElementId>
/** /**
* 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
@ -81,14 +89,16 @@ interface EuiccChannelManager {
suspend fun <R> withEuiccChannel( suspend fun <R> withEuiccChannel(
physicalSlotId: Int, physicalSlotId: Int,
portId: Int, portId: Int,
seId: EuiccChannel.SecureElementId = EuiccChannel.SecureElementId.DEFAULT,
fn: suspend (EuiccChannel) -> R fn: suspend (EuiccChannel) -> R
): R ): R
/** /**
* Same as withEuiccChannel(Int, Int, (EuiccChannel) -> R) but instead uses logical slot ID * Same as withEuiccChannel(Int, Int, SecureElementId, (EuiccChannel) -> R) but instead uses logical slot ID
*/ */
suspend fun <R> withEuiccChannel( suspend fun <R> withEuiccChannel(
logicalSlotId: Int, logicalSlotId: Int,
seId: EuiccChannel.SecureElementId = EuiccChannel.SecureElementId.DEFAULT,
fn: suspend (EuiccChannel) -> R fn: suspend (EuiccChannel) -> R
): R ): R

View file

@ -26,6 +26,8 @@ class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel {
get() = channel.logicalSlotId get() = channel.logicalSlotId
override val portId: Int override val portId: Int
get() = channel.portId get() = channel.portId
override val seId: EuiccChannel.SecureElementId
get() = channel.seId
private val lpaDelegate = lazy { private val lpaDelegate = lazy {
LocalProfileAssistantWrapper(channel.lpa) LocalProfileAssistantWrapper(channel.lpa)
} }

View file

@ -1,5 +1,7 @@
package im.angry.openeuicc.di package im.angry.openeuicc.di
import im.angry.openeuicc.core.EuiccChannel
interface CustomizableTextProvider { interface CustomizableTextProvider {
/** /**
* Explanation string for when no eUICC is found on the device. * Explanation string for when no eUICC is found on the device.
@ -13,8 +15,13 @@ interface CustomizableTextProvider {
val profileSwitchingTimeoutMessage: String val profileSwitchingTimeoutMessage: String
/** /**
* Format the name of a logical slot; internal only -- not intended for * Format the name of a logical slot -- not for USB channels
* other channels such as USB.
*/ */
fun formatInternalChannelName(logicalSlotId: Int): String fun formatNonUsbChannelName(logicalSlotId: Int): String
/**
* Format the name of a logical slot with a SE ID, in case of multi-SE chips; currently
* this is used in the download flow to distinguish between them on the same chip.
*/
fun formatNonUsbChannelNameWithSeId(logicalSlotId: Int, seId: EuiccChannel.SecureElementId): String
} }

View file

@ -2,14 +2,22 @@ package im.angry.openeuicc.di
import android.content.Context import android.content.Context
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
import im.angry.openeuicc.core.EuiccChannel
open class DefaultCustomizableTextProvider(private val context: Context) : CustomizableTextProvider { open class DefaultCustomizableTextProvider(private val context: Context) :
CustomizableTextProvider {
override val noEuiccExplanation: String override val noEuiccExplanation: String
get() = context.getString(R.string.no_euicc) get() = context.getString(R.string.no_euicc)
override val profileSwitchingTimeoutMessage: String override val profileSwitchingTimeoutMessage: String
get() = context.getString(R.string.profile_switch_timeout) get() = context.getString(R.string.profile_switch_timeout)
override fun formatInternalChannelName(logicalSlotId: Int): String = override fun formatNonUsbChannelName(logicalSlotId: Int): String =
context.getString(R.string.channel_name_format, logicalSlotId) context.getString(R.string.channel_name_format, logicalSlotId)
override fun formatNonUsbChannelNameWithSeId(
logicalSlotId: Int,
seId: EuiccChannel.SecureElementId
): String =
context.getString(R.string.channel_name_format_se, logicalSlotId, seId.id)
} }

View file

@ -2,13 +2,18 @@ package im.angry.openeuicc.di
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
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
import im.angry.openeuicc.ui.SettingsFragment import im.angry.openeuicc.ui.SettingsFragment
open class DefaultUiComponentFactory : UiComponentFactory { open class DefaultUiComponentFactory : UiComponentFactory {
override fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment = override fun createEuiccManagementFragment(
EuiccManagementFragment.newInstance(slotId, portId) slotId: Int,
portId: Int,
seId: EuiccChannel.SecureElementId
): EuiccManagementFragment =
EuiccManagementFragment.newInstance(slotId, portId, seId)
override fun createNoEuiccPlaceholderFragment(): Fragment = NoEuiccPlaceholderFragment() override fun createNoEuiccPlaceholderFragment(): Fragment = NoEuiccPlaceholderFragment()

View file

@ -2,10 +2,16 @@ package im.angry.openeuicc.di
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
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(
slotId: Int,
portId: Int,
seId: EuiccChannel.SecureElementId
): EuiccManagementFragment
fun createNoEuiccPlaceholderFragment(): Fragment fun createNoEuiccPlaceholderFragment(): Fragment
fun createSettingsFragment(): Fragment fun createSettingsFragment(): Fragment
} }

View file

@ -12,6 +12,7 @@ import androidx.core.app.NotificationManagerCompat
import androidx.lifecycle.LifecycleService import androidx.lifecycle.LifecycleService
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
@ -380,6 +381,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
fun launchProfileDownloadTask( fun launchProfileDownloadTask(
slotId: Int, slotId: Int,
portId: Int, portId: Int,
seId: EuiccChannel.SecureElementId,
smdp: String, smdp: String,
matchingId: String?, matchingId: String?,
confirmationCode: String?, confirmationCode: String?,
@ -390,8 +392,8 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
getString(R.string.task_profile_download_failure), getString(R.string.task_profile_download_failure),
R.drawable.ic_task_sim_card_download R.drawable.ic_task_sim_card_download
) { ) {
euiccChannelManager.beginTrackedOperation(slotId, portId) { euiccChannelManager.beginTrackedOperation(slotId, portId, seId) {
euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> euiccChannelManager.withEuiccChannel(slotId, portId, seId) { channel ->
channel.lpa.downloadProfile( channel.lpa.downloadProfile(
smdp, smdp,
matchingId, matchingId,
@ -413,6 +415,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
fun launchProfileRenameTask( fun launchProfileRenameTask(
slotId: Int, slotId: Int,
portId: Int, portId: Int,
seId: EuiccChannel.SecureElementId,
iccid: String, iccid: String,
name: String name: String
): ForegroundTaskSubscriberFlow = ): ForegroundTaskSubscriberFlow =
@ -421,7 +424,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
getString(R.string.task_profile_rename_failure), getString(R.string.task_profile_rename_failure),
R.drawable.ic_task_rename R.drawable.ic_task_rename
) { ) {
euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> euiccChannelManager.withEuiccChannel(slotId, portId, seId) { channel ->
channel.lpa.setNickname( channel.lpa.setNickname(
iccid, iccid,
name name
@ -432,6 +435,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
fun launchProfileDeleteTask( fun launchProfileDeleteTask(
slotId: Int, slotId: Int,
portId: Int, portId: Int,
seId: EuiccChannel.SecureElementId,
iccid: String iccid: String
): ForegroundTaskSubscriberFlow = ): ForegroundTaskSubscriberFlow =
launchForegroundTask( launchForegroundTask(
@ -439,8 +443,8 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
getString(R.string.task_profile_delete_failure), getString(R.string.task_profile_delete_failure),
R.drawable.ic_task_delete R.drawable.ic_task_delete
) { ) {
euiccChannelManager.beginTrackedOperation(slotId, portId) { euiccChannelManager.beginTrackedOperation(slotId, portId, seId) {
euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> euiccChannelManager.withEuiccChannel(slotId, portId, seId) { channel ->
channel.lpa.deleteProfile(iccid) channel.lpa.deleteProfile(iccid)
} }
@ -453,6 +457,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
fun launchProfileSwitchTask( fun launchProfileSwitchTask(
slotId: Int, slotId: Int,
portId: Int, portId: Int,
seId: EuiccChannel.SecureElementId,
iccid: String, iccid: String,
enable: Boolean, // Enable or disable the profile indicated in iccid enable: Boolean, // Enable or disable the profile indicated in iccid
reconnectTimeoutMillis: Long = 0 // 0 = do not wait for reconnect reconnectTimeoutMillis: Long = 0 // 0 = do not wait for reconnect
@ -462,9 +467,9 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
getString(R.string.task_profile_switch_failure), getString(R.string.task_profile_switch_failure),
R.drawable.ic_task_switch R.drawable.ic_task_switch
) { ) {
euiccChannelManager.beginTrackedOperation(slotId, portId) { euiccChannelManager.beginTrackedOperation(slotId, portId, seId) {
val (response, refreshed) = val (response, refreshed) =
euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> euiccChannelManager.withEuiccChannel(slotId, portId, seId) { channel ->
val refresh = preferenceRepository.refreshAfterSwitchFlow.first() val refresh = preferenceRepository.refreshAfterSwitchFlow.first()
val response = channel.lpa.switchProfile(iccid, enable, refresh) val response = channel.lpa.switchProfile(iccid, enable, refresh)
if (response || !refresh) { if (response || !refresh) {
@ -510,13 +515,17 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
} }
} }
fun launchMemoryReset(slotId: Int, portId: Int): ForegroundTaskSubscriberFlow = fun launchMemoryReset(
slotId: Int,
portId: Int,
seId: EuiccChannel.SecureElementId
): ForegroundTaskSubscriberFlow =
launchForegroundTask( launchForegroundTask(
getString(R.string.task_euicc_memory_reset), getString(R.string.task_euicc_memory_reset),
getString(R.string.task_euicc_memory_reset_failure), getString(R.string.task_euicc_memory_reset_failure),
R.drawable.ic_euicc_memory_reset R.drawable.ic_euicc_memory_reset
) { ) {
euiccChannelManager.beginTrackedOperation(slotId, portId) { euiccChannelManager.beginTrackedOperation(slotId, portId, seId) {
euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
channel.lpa.euiccMemoryReset() channel.lpa.euiccMemoryReset()
} }

View file

@ -43,6 +43,7 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
private lateinit var infoList: RecyclerView private lateinit var infoList: RecyclerView
private var logicalSlotId: Int = -1 private var logicalSlotId: Int = -1
private var seId: EuiccChannel.SecureElementId = EuiccChannel.SecureElementId.DEFAULT
data class Item( data class Item(
@StringRes @StringRes
@ -67,11 +68,17 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
} }
logicalSlotId = intent.getIntExtra("logicalSlotId", 0) logicalSlotId = intent.getIntExtra("logicalSlotId", 0)
seId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra("seId", EuiccChannel.SecureElementId::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra("seId")!!
} ?: EuiccChannel.SecureElementId.DEFAULT
val channelTitle = if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { val channelTitle = if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
getString(R.string.channel_type_usb) getString(R.string.channel_type_usb)
} else { } else {
appContainer.customizableTextProvider.formatInternalChannelName(logicalSlotId) appContainer.customizableTextProvider.formatNonUsbChannelName(logicalSlotId)
} }
title = getString(R.string.euicc_info_activity_title, channelTitle) title = getString(R.string.euicc_info_activity_title, channelTitle)
@ -99,7 +106,7 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
lifecycleScope.launch { lifecycleScope.launch {
(infoList.adapter!! as EuiccInfoAdapter).euiccInfoItems = (infoList.adapter!! as EuiccInfoAdapter).euiccInfoItems =
euiccChannelManager.withEuiccChannel(logicalSlotId, ::buildEuiccInfoItems) euiccChannelManager.withEuiccChannel(logicalSlotId, fn = ::buildEuiccInfoItems)
swipeRefresh.isRefreshing = false swipeRefresh.isRefreshing = false
} }
@ -107,12 +114,31 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
private fun buildEuiccInfoItems(channel: EuiccChannel) = buildList { private fun buildEuiccInfoItems(channel: EuiccChannel) = buildList {
add(Item(R.string.euicc_info_access_mode, channel.type)) add(Item(R.string.euicc_info_access_mode, channel.type))
add(Item(R.string.euicc_info_removable, formatByBoolean(channel.port.card.isRemovable, YES_NO))) add(
add(Item(R.string.euicc_info_eid, channel.lpa.eID, copiedToastResId = R.string.toast_eid_copied)) Item(
R.string.euicc_info_removable,
formatByBoolean(channel.port.card.isRemovable, YES_NO)
)
)
add(
Item(
R.string.euicc_info_eid,
channel.lpa.eID,
copiedToastResId = R.string.toast_eid_copied
)
)
add(Item(R.string.euicc_info_isdr_aid, channel.isdrAid.encodeHex())) add(Item(R.string.euicc_info_isdr_aid, channel.isdrAid.encodeHex()))
channel.tryParseEuiccVendorInfo()?.let { vendorInfo -> channel.tryParseEuiccVendorInfo()?.let { vendorInfo ->
vendorInfo.skuName?.let { add(Item(R.string.euicc_info_sku, it)) } vendorInfo.skuName?.let { add(Item(R.string.euicc_info_sku, it)) }
vendorInfo.serialNumber?.let { add(Item(R.string.euicc_info_sn, it, copiedToastResId = R.string.toast_sn_copied)) } vendorInfo.serialNumber?.let {
add(
Item(
R.string.euicc_info_sn,
it,
copiedToastResId = R.string.toast_sn_copied
)
)
}
vendorInfo.firmwareVersion?.let { add(Item(R.string.euicc_info_fw_ver, it)) } vendorInfo.firmwareVersion?.let { add(Item(R.string.euicc_info_fw_ver, it)) }
vendorInfo.bootloaderVersion?.let { add(Item(R.string.euicc_info_bl_ver, it)) } vendorInfo.bootloaderVersion?.let { add(Item(R.string.euicc_info_bl_ver, it)) }
} }

View file

@ -31,6 +31,7 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import net.typeblog.lpac_jni.LocalProfileInfo import net.typeblog.lpac_jni.LocalProfileInfo
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.service.EuiccChannelManagerService import im.angry.openeuicc.service.EuiccChannelManagerService
import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
import im.angry.openeuicc.ui.wizard.DownloadWizardActivity import im.angry.openeuicc.ui.wizard.DownloadWizardActivity
@ -49,8 +50,12 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
companion object { companion object {
const val TAG = "EuiccManagementFragment" const val TAG = "EuiccManagementFragment"
fun newInstance(slotId: Int, portId: Int): EuiccManagementFragment = fun newInstance(
newInstanceEuicc(EuiccManagementFragment::class.java, slotId, portId) slotId: Int,
portId: Int,
seId: EuiccChannel.SecureElementId
): EuiccManagementFragment =
newInstanceEuicc(EuiccManagementFragment::class.java, slotId, portId, seId)
} }
private lateinit var swipeRefresh: SwipeRefreshLayout private lateinit var swipeRefresh: SwipeRefreshLayout
@ -148,6 +153,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
R.id.show_notifications -> { R.id.show_notifications -> {
Intent(requireContext(), NotificationsActivity::class.java).apply { Intent(requireContext(), NotificationsActivity::class.java).apply {
putExtra("logicalSlotId", logicalSlotId) putExtra("logicalSlotId", logicalSlotId)
putExtra("seId", seId)
startActivity(this) startActivity(this)
} }
true true
@ -156,13 +162,14 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
R.id.euicc_info -> { R.id.euicc_info -> {
Intent(requireContext(), EuiccInfoActivity::class.java).apply { Intent(requireContext(), EuiccInfoActivity::class.java).apply {
putExtra("logicalSlotId", logicalSlotId) putExtra("logicalSlotId", logicalSlotId)
putExtra("seId", seId)
startActivity(this) startActivity(this)
} }
true true
} }
R.id.euicc_memory_reset -> { R.id.euicc_memory_reset -> {
EuiccMemoryResetFragment.newInstance(slotId, portId, eid) EuiccMemoryResetFragment.newInstance(slotId, portId, seId, eid)
.show(childFragmentManager, EuiccMemoryResetFragment.TAG) .show(childFragmentManager, EuiccMemoryResetFragment.TAG)
true true
} }
@ -241,6 +248,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
val err = euiccChannelManagerService.launchProfileSwitchTask( val err = euiccChannelManagerService.launchProfileSwitchTask(
slotId, slotId,
portId, portId,
seId,
iccid, iccid,
enable, enable,
reconnectTimeoutMillis = 30 * 1000 reconnectTimeoutMillis = 30 * 1000
@ -294,7 +302,10 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
} }
} }
protected open fun populatePopupWithProfileActions(popup: PopupMenu, profile: LocalProfileInfo) { protected open fun populatePopupWithProfileActions(
popup: PopupMenu,
profile: LocalProfileInfo
) {
popup.inflate(R.menu.profile_options) popup.inflate(R.menu.profile_options)
if (profile.isEnabled) { if (profile.isEnabled) {
popup.menu.findItem(R.id.enable).isVisible = false popup.menu.findItem(R.id.enable).isVisible = false
@ -423,20 +434,36 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
enableOrDisableProfile(profile.iccid, true) enableOrDisableProfile(profile.iccid, true)
true true
} }
R.id.disable -> { R.id.disable -> {
enableOrDisableProfile(profile.iccid, false) enableOrDisableProfile(profile.iccid, false)
true true
} }
R.id.rename -> { R.id.rename -> {
ProfileRenameFragment.newInstance(slotId, portId, profile.iccid, profile.displayName) ProfileRenameFragment.newInstance(
slotId,
portId,
seId,
profile.iccid,
profile.displayName
)
.show(childFragmentManager, ProfileRenameFragment.TAG) .show(childFragmentManager, ProfileRenameFragment.TAG)
true true
} }
R.id.delete -> { R.id.delete -> {
ProfileDeleteFragment.newInstance(slotId, portId, profile.iccid, profile.displayName) ProfileDeleteFragment.newInstance(
slotId,
portId,
seId,
profile.iccid,
profile.displayName
)
.show(childFragmentManager, ProfileDeleteFragment.TAG) .show(childFragmentManager, ProfileDeleteFragment.TAG)
true true
} }
else -> false else -> false
} }
} }
@ -448,9 +475,11 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
when (ViewHolder.Type.fromInt(viewType)) { when (ViewHolder.Type.fromInt(viewType)) {
ViewHolder.Type.PROFILE -> { ViewHolder.Type.PROFILE -> {
val view = LayoutInflater.from(parent.context).inflate(R.layout.euicc_profile, parent, false) val view = LayoutInflater.from(parent.context)
.inflate(R.layout.euicc_profile, parent, false)
ProfileViewHolder(view) ProfileViewHolder(view)
} }
ViewHolder.Type.FOOTER -> { ViewHolder.Type.FOOTER -> {
FooterViewHolder() FooterViewHolder()
} }
@ -461,9 +490,11 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
position < profiles.size -> { position < profiles.size -> {
ViewHolder.Type.PROFILE.value ViewHolder.Type.PROFILE.value
} }
position >= profiles.size && position < profiles.size + footerViews.size -> { position >= profiles.size && position < profiles.size + footerViews.size -> {
ViewHolder.Type.FOOTER.value ViewHolder.Type.FOOTER.value
} }
else -> -1 else -> -1
} }
@ -473,6 +504,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
holder.setProfile(profiles[position]) holder.setProfile(profiles[position])
holder.setProfileSequenceNumber(position + 1) holder.setProfileSequenceNumber(position + 1)
} }
is FooterViewHolder -> { is FooterViewHolder -> {
holder.attach(footerViews[position - profiles.size]) holder.attach(footerViews[position - profiles.size])
} }

View file

@ -11,6 +11,7 @@ import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
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.service.EuiccChannelManagerService.Companion.waitDone import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
import im.angry.openeuicc.util.EuiccChannelFragmentMarker import im.angry.openeuicc.util.EuiccChannelFragmentMarker
import im.angry.openeuicc.util.EuiccProfilesChangedListener import im.angry.openeuicc.util.EuiccProfilesChangedListener
@ -19,6 +20,7 @@ import im.angry.openeuicc.util.euiccChannelManagerService
import im.angry.openeuicc.util.newInstanceEuicc import im.angry.openeuicc.util.newInstanceEuicc
import im.angry.openeuicc.util.notifyEuiccProfilesChanged import im.angry.openeuicc.util.notifyEuiccProfilesChanged
import im.angry.openeuicc.util.portId import im.angry.openeuicc.util.portId
import im.angry.openeuicc.util.seId
import im.angry.openeuicc.util.slotId import im.angry.openeuicc.util.slotId
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -29,8 +31,8 @@ class EuiccMemoryResetFragment : DialogFragment(), EuiccChannelFragmentMarker {
private const val FIELD_EID = "eid" private const val FIELD_EID = "eid"
fun newInstance(slotId: Int, portId: Int, eid: String) = fun newInstance(slotId: Int, portId: Int, seId: EuiccChannel.SecureElementId, eid: String) =
newInstanceEuicc(EuiccMemoryResetFragment::class.java, slotId, portId) { newInstanceEuicc(EuiccMemoryResetFragment::class.java, slotId, portId, seId) {
putString(FIELD_EID, eid) putString(FIELD_EID, eid)
} }
} }
@ -103,7 +105,7 @@ class EuiccMemoryResetFragment : DialogFragment(), EuiccChannelFragmentMarker {
ensureEuiccChannelManager() ensureEuiccChannelManager()
euiccChannelManagerService.waitForForegroundTask() euiccChannelManagerService.waitForForegroundTask()
euiccChannelManagerService.launchMemoryReset(slotId, portId) euiccChannelManagerService.launchMemoryReset(slotId, portId, seId)
.onStart { .onStart {
parentFragment?.notifyEuiccProfilesChanged() parentFragment?.notifyEuiccProfilesChanged()

View file

@ -112,10 +112,12 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
startActivity(Intent(this, SettingsActivity::class.java)) startActivity(Intent(this, SettingsActivity::class.java))
true true
} }
R.id.reload -> { R.id.reload -> {
refresh() refresh()
true true
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
@ -154,7 +156,8 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
euiccChannelManager.flowInternalEuiccPorts().onEach { (slotId, portId) -> euiccChannelManager.flowInternalEuiccPorts().onEach { (slotId, portId) ->
Log.d(TAG, "slot $slotId port $portId") Log.d(TAG, "slot $slotId port $portId")
euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> euiccChannelManager.flowEuiccSecureElements(slotId, portId).onEach { seId ->
euiccChannelManager.withEuiccChannel(slotId, portId, seId) { channel ->
if (preferenceRepository.verboseLoggingFlow.first()) { if (preferenceRepository.verboseLoggingFlow.first()) {
Log.d(TAG, channel.lpa.eID) Log.d(TAG, channel.lpa.eID)
} }
@ -164,12 +167,17 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId) euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId)
val channelName = val channelName =
appContainer.customizableTextProvider.formatInternalChannelName(channel.logicalSlotId) appContainer.customizableTextProvider.formatNonUsbChannelName(channel.logicalSlotId)
newPages.add(Page(channel.logicalSlotId, channelName) { newPages.add(Page(channel.logicalSlotId, channelName) {
appContainer.uiComponentFactory.createEuiccManagementFragment(slotId, portId) appContainer.uiComponentFactory.createEuiccManagementFragment(
slotId,
portId,
seId
)
}) })
} }
}.collect() }.collect()
}.collect()
// If USB readers exist, add them at the very last // If USB readers exist, add them at the very last
// We use a wrapper fragment to handle logic specific to USB readers // We use a wrapper fragment to handle logic specific to USB readers

View file

@ -1,6 +1,7 @@
package im.angry.openeuicc.ui package im.angry.openeuicc.ui
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.Html import android.text.Html
import android.view.ContextMenu import android.view.ContextMenu
@ -20,6 +21,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
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
@ -33,6 +35,7 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
private val notificationAdapter = NotificationAdapter() private val notificationAdapter = NotificationAdapter()
private var logicalSlotId = -1 private var logicalSlotId = -1
private var seId = EuiccChannel.SecureElementId.DEFAULT
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge() enableEdgeToEdge()
@ -51,18 +54,29 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
override fun onInit() { override fun onInit() {
notificationList.layoutManager = notificationList.layoutManager =
LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
notificationList.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL)) notificationList.addItemDecoration(
DividerItemDecoration(
this,
LinearLayoutManager.VERTICAL
)
)
notificationList.adapter = notificationAdapter notificationList.adapter = notificationAdapter
registerForContextMenu(notificationList) registerForContextMenu(notificationList)
logicalSlotId = intent.getIntExtra("logicalSlotId", 0) logicalSlotId = intent.getIntExtra("logicalSlotId", 0)
seId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra("seId", EuiccChannel.SecureElementId::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra("seId")!!
} ?: EuiccChannel.SecureElementId.DEFAULT
// This is slightly different from the MainActivity logic // This is slightly different from the MainActivity logic
// due to the length (we don't want to display the full USB product name) // due to the length (we don't want to display the full USB product name)
val channelTitle = if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { val channelTitle = if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
getString(R.string.channel_type_usb) getString(R.string.channel_type_usb)
} else { } else {
appContainer.customizableTextProvider.formatInternalChannelName(logicalSlotId) appContainer.customizableTextProvider.formatNonUsbChannelName(logicalSlotId)
} }
title = getString(R.string.profile_notifications_detailed_format, channelTitle) title = getString(R.string.profile_notifications_detailed_format, channelTitle)
@ -86,6 +100,7 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
finish() finish()
true true
} }
R.id.help -> { R.id.help -> {
AlertDialog.Builder(this, R.style.AlertDialogTheme).apply { AlertDialog.Builder(this, R.style.AlertDialogTheme).apply {
setMessage(R.string.profile_notifications_help) setMessage(R.string.profile_notifications_help)
@ -96,6 +111,7 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
} }
true true
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
@ -170,7 +186,8 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
LocalProfileNotification.Operation.Delete -> R.string.profile_notification_operation_delete LocalProfileNotification.Operation.Delete -> R.string.profile_notification_operation_delete
LocalProfileNotification.Operation.Enable -> R.string.profile_notification_operation_enable LocalProfileNotification.Operation.Enable -> R.string.profile_notification_operation_enable
LocalProfileNotification.Operation.Disable -> R.string.profile_notification_operation_disable LocalProfileNotification.Operation.Disable -> R.string.profile_notification_operation_disable
}) }
)
fun updateNotification(value: LocalProfileNotificationWrapper) { fun updateNotification(value: LocalProfileNotificationWrapper) {
notification = value notification = value
@ -181,10 +198,13 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
value.inner.seqNumber value.inner.seqNumber
) )
profileName.text = Html.fromHtml( profileName.text = Html.fromHtml(
root.context.getString(R.string.profile_notification_name_format, root.context.getString(
R.string.profile_notification_name_format,
operationToLocalizedText(value.inner.profileManagementOperation), operationToLocalizedText(value.inner.profileManagementOperation),
value.profileName, value.inner.iccid), value.profileName, value.inner.iccid
Html.FROM_HTML_MODE_COMPACT) ),
Html.FROM_HTML_MODE_COMPACT
)
} }
override fun onCreateContextMenu( override fun onCreateContextMenu(
@ -213,6 +233,7 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
} }
true true
} }
R.id.notification_delete -> { R.id.notification_delete -> {
launchTask { launchTask {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
@ -225,6 +246,7 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
} }
true true
} }
else -> false else -> false
} }
} }

View file

@ -9,6 +9,7 @@ import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
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.service.EuiccChannelManagerService.Companion.waitDone import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
@ -20,8 +21,8 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
private const val FIELD_ICCID = "iccid" private const val FIELD_ICCID = "iccid"
private const val FIELD_NAME = "name" private const val FIELD_NAME = "name"
fun newInstance(slotId: Int, portId: Int, iccid: String, name: String) = fun newInstance(slotId: Int, portId: Int, seId: EuiccChannel.SecureElementId, iccid: String, name: String) =
newInstanceEuicc(ProfileDeleteFragment::class.java, slotId, portId) { newInstanceEuicc(ProfileDeleteFragment::class.java, slotId, portId, seId) {
putString(FIELD_ICCID, iccid) putString(FIELD_ICCID, iccid)
putString(FIELD_NAME, name) putString(FIELD_NAME, name)
} }
@ -88,7 +89,7 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
requireParentFragment().lifecycleScope.launch { requireParentFragment().lifecycleScope.launch {
ensureEuiccChannelManager() ensureEuiccChannelManager()
euiccChannelManagerService.waitForForegroundTask() euiccChannelManagerService.waitForForegroundTask()
euiccChannelManagerService.launchProfileDeleteTask(slotId, portId, iccid) euiccChannelManagerService.launchProfileDeleteTask(slotId, portId, seId, iccid)
.onStart { .onStart {
parentFragment?.notifyEuiccProfilesChanged() parentFragment?.notifyEuiccProfilesChanged()
runCatching(::dismiss) runCatching(::dismiss)

View file

@ -12,6 +12,7 @@ import androidx.appcompat.widget.Toolbar
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -24,8 +25,8 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
const val TAG = "ProfileRenameFragment" const val TAG = "ProfileRenameFragment"
fun newInstance(slotId: Int, portId: Int, iccid: String, currentName: String) = fun newInstance(slotId: Int, portId: Int, seId: EuiccChannel.SecureElementId, iccid: String, currentName: String) =
newInstanceEuicc(ProfileRenameFragment::class.java, slotId, portId) { newInstanceEuicc(ProfileRenameFragment::class.java, slotId, portId, seId) {
putString(FIELD_ICCID, iccid) putString(FIELD_ICCID, iccid)
putString(FIELD_CURRENT_NAME, currentName) putString(FIELD_CURRENT_NAME, currentName)
} }
@ -105,7 +106,7 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
ensureEuiccChannelManager() ensureEuiccChannelManager()
euiccChannelManagerService.waitForForegroundTask() euiccChannelManagerService.waitForForegroundTask()
val response = euiccChannelManagerService val response = euiccChannelManagerService
.launchProfileRenameTask(slotId, portId, iccid, newName).waitDone() .launchProfileRenameTask(slotId, portId, seId, iccid, newName).waitDone()
when (response) { when (response) {
is LocalProfileAssistant.ProfileNameTooLongException -> { is LocalProfileAssistant.ProfileNameTooLongException -> {

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
@ -156,7 +157,9 @@ class UsbCcidReaderFragment : Fragment(), OpenEuiccContextMarker {
R.id.child_container, R.id.child_container,
appContainer.uiComponentFactory.createEuiccManagementFragment( appContainer.uiComponentFactory.createEuiccManagementFragment(
slotId = EuiccChannelManager.USB_CHANNEL_ID, slotId = EuiccChannelManager.USB_CHANNEL_ID,
portId = 0 portId = 0,
// TODO: What if a USB card has multiple SEs?
seId = EuiccChannel.SecureElementId.DEFAULT
) )
) )
} }

View file

@ -17,6 +17,7 @@ import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
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.ui.BaseEuiccAccessActivity import im.angry.openeuicc.ui.BaseEuiccAccessActivity
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
@ -27,7 +28,7 @@ import net.typeblog.lpac_jni.LocalProfileAssistant
class DownloadWizardActivity : BaseEuiccAccessActivity() { class DownloadWizardActivity : BaseEuiccAccessActivity() {
data class DownloadWizardState( data class DownloadWizardState(
var currentStepFragmentClassName: String?, var currentStepFragmentClassName: String?,
var selectedLogicalSlot: Int, var selectedSyntheticSlotId: Int,
var smdp: String, var smdp: String,
var matchingId: String?, var matchingId: String?,
var confirmationCode: String?, var confirmationCode: String?,
@ -66,7 +67,7 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
state = DownloadWizardState( state = DownloadWizardState(
currentStepFragmentClassName = null, currentStepFragmentClassName = null,
selectedLogicalSlot = intent.getIntExtra("selectedLogicalSlot", 0), selectedSyntheticSlotId = intent.getIntExtra("selectedLogicalSlot", 0),
smdp = "", smdp = "",
matchingId = null, matchingId = null,
confirmationCode = null, confirmationCode = null,
@ -151,7 +152,7 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
outState.putString("currentStepFragmentClassName", state.currentStepFragmentClassName) outState.putString("currentStepFragmentClassName", state.currentStepFragmentClassName)
outState.putInt("selectedLogicalSlot", state.selectedLogicalSlot) outState.putInt("selectedLogicalSlot", state.selectedSyntheticSlotId)
outState.putString("smdp", state.smdp) outState.putString("smdp", state.smdp)
outState.putString("matchingId", state.matchingId) outState.putString("matchingId", state.matchingId)
outState.putString("confirmationCode", state.confirmationCode) outState.putString("confirmationCode", state.confirmationCode)
@ -167,16 +168,20 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
"currentStepFragmentClassName", "currentStepFragmentClassName",
state.currentStepFragmentClassName state.currentStepFragmentClassName
) )
state.selectedLogicalSlot = state.selectedSyntheticSlotId =
savedInstanceState.getInt("selectedLogicalSlot", state.selectedLogicalSlot) savedInstanceState.getInt("selectedSyntheticSlotId", state.selectedSyntheticSlotId)
state.smdp = savedInstanceState.getString("smdp", state.smdp) state.smdp = savedInstanceState.getString("smdp", state.smdp)
state.matchingId = savedInstanceState.getString("matchingId", state.matchingId) state.matchingId = savedInstanceState.getString("matchingId", state.matchingId)
state.imei = savedInstanceState.getString("imei", state.imei) state.imei = savedInstanceState.getString("imei", state.imei)
state.downloadStarted = state.downloadStarted =
savedInstanceState.getBoolean("downloadStarted", state.downloadStarted) savedInstanceState.getBoolean("downloadStarted", state.downloadStarted)
state.downloadTaskID = savedInstanceState.getLong("downloadTaskID", state.downloadTaskID) state.downloadTaskID = savedInstanceState.getLong("downloadTaskID", state.downloadTaskID)
state.confirmationCode = savedInstanceState.getString("confirmationCode", state.confirmationCode) state.confirmationCode =
state.confirmationCodeRequired = savedInstanceState.getBoolean("confirmationCodeRequired", state.confirmationCodeRequired) savedInstanceState.getString("confirmationCode", state.confirmationCode)
state.confirmationCodeRequired = savedInstanceState.getBoolean(
"confirmationCodeRequired",
state.confirmationCodeRequired
)
} }
private fun onPrevPressed() { private fun onPrevPressed() {
@ -200,10 +205,13 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
progressBar.isIndeterminate = true progressBar.isIndeterminate = true
lifecycleScope.launch(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
if (state.selectedLogicalSlot >= 0) { if (state.selectedSyntheticSlotId >= 0) {
try { try {
val (slotId, seId) = DownloadWizardSlotSelectFragment.decodeSyntheticSlotId(
state.selectedSyntheticSlotId
)
// This is run on IO by default // This is run on IO by default
euiccChannelManager.withEuiccChannel(state.selectedLogicalSlot) { channel -> euiccChannelManager.withEuiccChannel(slotId, seId) { channel ->
// Be _very_ sure that the channel we got is valid // Be _very_ sure that the channel we got is valid
if (!channel.valid) throw EuiccChannelManager.EuiccChannelNotFoundException() if (!channel.valid) throw EuiccChannelManager.EuiccChannelNotFoundException()
} }

View file

@ -153,7 +153,12 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep
} else { } else {
euiccChannelManagerService.waitForForegroundTask() euiccChannelManagerService.waitForForegroundTask()
val (slotId, portId) = euiccChannelManager.withEuiccChannel(state.selectedLogicalSlot) { channel -> val (logicalSlotId, seId) = DownloadWizardSlotSelectFragment.decodeSyntheticSlotId(state.selectedSyntheticSlotId)
val (slotId, portId) = euiccChannelManager.withEuiccChannel(
logicalSlotId,
seId
) { channel ->
Pair(channel.slotId, channel.portId) Pair(channel.slotId, channel.portId)
} }
@ -163,6 +168,7 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep
val ret = euiccChannelManagerService.launchProfileDownloadTask( val ret = euiccChannelManagerService.launchProfileDownloadTask(
slotId, slotId,
portId, portId,
seId,
state.smdp, state.smdp,
state.matchingId, state.matchingId,
state.confirmationCode, state.confirmationCode,

View file

@ -14,8 +14,11 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ViewHolder import androidx.recyclerview.widget.RecyclerView.ViewHolder
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.flow.asFlow
import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -24,19 +27,28 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
companion object { companion object {
const val LOW_NVRAM_THRESHOLD = const val LOW_NVRAM_THRESHOLD =
30 * 1024 // < 30 KiB, alert about potential download failure 30 * 1024 // < 30 KiB, alert about potential download failure
fun decodeSyntheticSlotId(id: Int): Pair<Int, EuiccChannel.SecureElementId> =
Pair(id shr 16, EuiccChannel.SecureElementId.createFromInt(id and 0xFF))
} }
private data class SlotInfo( private data class SlotInfo(
val logicalSlotId: Int, val logicalSlotId: Int,
val isRemovable: Boolean, val isRemovable: Boolean,
val hasMultiplePorts: Boolean, val hasMultiplePorts: Boolean,
val hasMultipleSEs: Boolean,
val portId: Int, val portId: Int,
val seId: EuiccChannel.SecureElementId,
val eID: String, val eID: String,
val freeSpace: Int, val freeSpace: Int,
val imei: String, val imei: String,
val enabledProfileName: String?, val enabledProfileName: String?,
val intrinsicChannelName: String?, val intrinsicChannelName: String?,
) ) {
// A synthetic slot ID used to uniquely identify this slot + SE chip in the download process
// We assume we don't have anywhere near 2^16 ports...
val syntheticSlotId: Int = (logicalSlotId shl 16) + seId.id
}
private var loaded = false private var loaded = false
@ -85,7 +97,12 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
recyclerView.adapter = adapter recyclerView.adapter = adapter
recyclerView.layoutManager = recyclerView.layoutManager =
LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false) LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false)
recyclerView.addItemDecoration(DividerItemDecoration(requireContext(), LinearLayoutManager.VERTICAL)) recyclerView.addItemDecoration(
DividerItemDecoration(
requireContext(),
LinearLayoutManager.VERTICAL
)
)
return view return view
} }
@ -97,16 +114,21 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
} }
@SuppressLint("NotifyDataSetChanged", "MissingPermission") @SuppressLint("NotifyDataSetChanged", "MissingPermission")
@OptIn(kotlinx.coroutines.FlowPreview::class)
private suspend fun init() { private suspend fun init() {
ensureEuiccChannelManager() ensureEuiccChannelManager()
showProgressBar(-1) showProgressBar(-1)
val slots = euiccChannelManager.flowAllOpenEuiccPorts().map { (slotId, portId) -> val slots = euiccChannelManager.flowAllOpenEuiccPorts().flatMapConcat { (slotId, portId) ->
euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> val ses = euiccChannelManager.flowEuiccSecureElements(slotId, portId).toList()
ses.asFlow().map { seId ->
euiccChannelManager.withEuiccChannel(slotId, portId, seId) { channel ->
SlotInfo( SlotInfo(
channel.logicalSlotId, channel.logicalSlotId,
channel.port.card.isRemovable, channel.port.card.isRemovable,
channel.port.card.ports.size > 1, channel.port.card.ports.size > 1,
ses.size > 1,
channel.portId, channel.portId,
channel.seId,
channel.lpa.eID, channel.lpa.eID,
channel.lpa.euiccInfo2?.freeNvram ?: 0, channel.lpa.euiccInfo2?.freeNvram ?: 0,
try { try {
@ -118,16 +140,17 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
channel.intrinsicChannelName, channel.intrinsicChannelName,
) )
} }
}.toList().sortedBy { it.logicalSlotId } }
}.toList().sortedBy { it.syntheticSlotId }
adapter.slots = slots adapter.slots = slots
// Ensure we always have a selected slot by default // Ensure we always have a selected slot by default
val selectedIdx = slots.indexOfFirst { it.logicalSlotId == state.selectedLogicalSlot } val selectedIdx = slots.indexOfFirst { it.syntheticSlotId == state.selectedSyntheticSlotId }
adapter.currentSelectedIdx = if (selectedIdx > 0) { adapter.currentSelectedIdx = if (selectedIdx > 0) {
selectedIdx selectedIdx
} else { } else {
if (slots.isNotEmpty()) { if (slots.isNotEmpty()) {
state.selectedLogicalSlot = slots[0].logicalSlotId state.selectedSyntheticSlotId = slots[0].syntheticSlotId
} }
0 0
} }
@ -167,7 +190,8 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
adapter.notifyItemChanged(lastIdx) adapter.notifyItemChanged(lastIdx)
adapter.notifyItemChanged(curIdx) adapter.notifyItemChanged(curIdx)
// Selected index isn't logical slot ID directly, needs a conversion // Selected index isn't logical slot ID directly, needs a conversion
state.selectedLogicalSlot = adapter.slots[adapter.currentSelectedIdx].logicalSlotId state.selectedSyntheticSlotId =
adapter.slots[adapter.currentSelectedIdx].syntheticSlotId
state.imei = adapter.slots[adapter.currentSelectedIdx].imei state.imei = adapter.slots[adapter.currentSelectedIdx].imei
} }
@ -187,11 +211,17 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
title.text = if (item.logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { title.text = if (item.logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
item.intrinsicChannelName ?: root.context.getString(R.string.channel_type_usb) item.intrinsicChannelName ?: root.context.getString(R.string.channel_type_usb)
} else if (item.hasMultipleSEs) {
appContainer.customizableTextProvider.formatNonUsbChannelNameWithSeId(
item.logicalSlotId,
item.seId
)
} else { } else {
appContainer.customizableTextProvider.formatInternalChannelName(item.logicalSlotId) appContainer.customizableTextProvider.formatNonUsbChannelName(item.logicalSlotId)
} }
eID.text = item.eID eID.text = item.eID
activeProfile.text = item.enabledProfileName ?: root.context.getString(R.string.profile_no_enabled_profile) activeProfile.text = item.enabledProfileName
?: root.context.getString(R.string.profile_no_enabled_profile)
freeSpace.text = formatFreeSpace(item.freeSpace) freeSpace.text = formatFreeSpace(item.freeSpace)
checkBox.isChecked = adapter.currentSelectedIdx == idx checkBox.isChecked = adapter.currentSelectedIdx == idx
} }
@ -205,7 +235,8 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
get() = slots[currentSelectedIdx] get() = slots[currentSelectedIdx]
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SlotItemHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SlotItemHolder {
val root = LayoutInflater.from(parent.context).inflate(R.layout.download_slot_item, parent, false) val root = LayoutInflater.from(parent.context)
.inflate(R.layout.download_slot_item, parent, false)
return SlotItemHolder(root) return SlotItemHolder(root)
} }

View file

@ -1,5 +1,6 @@
package im.angry.openeuicc.util package im.angry.openeuicc.util
import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.core.EuiccChannel
@ -9,6 +10,7 @@ import im.angry.openeuicc.ui.BaseEuiccAccessActivity
private const val FIELD_SLOT_ID = "slotId" private const val FIELD_SLOT_ID = "slotId"
private const val FIELD_PORT_ID = "portId" private const val FIELD_PORT_ID = "portId"
private const val FIELD_SE_ID = "seId"
interface EuiccChannelFragmentMarker : OpenEuiccContextMarker interface EuiccChannelFragmentMarker : OpenEuiccContextMarker
@ -17,12 +19,19 @@ private typealias BundleSetter = Bundle.() -> Unit
// We must use extension functions because there is no way to add bounds to the type of "self" // We must use extension functions because there is no way to add bounds to the type of "self"
// in the definition of an interface, so the only way is to limit where the extension functions // in the definition of an interface, so the only way is to limit where the extension functions
// can be applied. // can be applied.
fun <T> newInstanceEuicc(clazz: Class<T>, slotId: Int, portId: Int, addArguments: BundleSetter = {}): T fun <T> newInstanceEuicc(
clazz: Class<T>,
slotId: Int,
portId: Int,
seId: EuiccChannel.SecureElementId,
addArguments: BundleSetter = {}
): T
where T : Fragment, T : EuiccChannelFragmentMarker = where T : Fragment, T : EuiccChannelFragmentMarker =
clazz.getDeclaredConstructor().newInstance().apply { clazz.getDeclaredConstructor().newInstance().apply {
arguments = Bundle() arguments = Bundle()
arguments!!.putInt(FIELD_SLOT_ID, slotId) arguments!!.putInt(FIELD_SLOT_ID, slotId)
arguments!!.putInt(FIELD_PORT_ID, portId) arguments!!.putInt(FIELD_PORT_ID, portId)
arguments!!.putParcelable(FIELD_SE_ID, seId)
arguments!!.addArguments() arguments!!.addArguments()
} }
@ -35,6 +44,18 @@ val <T> T.slotId: Int
val <T> T.portId: Int val <T> T.portId: Int
where T : Fragment, T : EuiccChannelFragmentMarker where T : Fragment, T : EuiccChannelFragmentMarker
get() = requireArguments().getInt(FIELD_PORT_ID) get() = requireArguments().getInt(FIELD_PORT_ID)
val <T> T.seId: EuiccChannel.SecureElementId
where T : Fragment, T : EuiccChannelFragmentMarker
get() =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
requireArguments().getParcelable(
FIELD_SE_ID,
EuiccChannel.SecureElementId::class.java
)!!
} else {
@Suppress("DEPRECATION")
requireArguments().getParcelable(FIELD_SE_ID)!!
}
val <T> T.isUsb: Boolean val <T> T.isUsb: Boolean
where T : Fragment, T : EuiccChannelFragmentMarker where T : Fragment, T : EuiccChannelFragmentMarker
get() = slotId == EuiccChannelManager.USB_CHANNEL_ID get() = slotId == EuiccChannelManager.USB_CHANNEL_ID
@ -54,7 +75,12 @@ val <T> T.euiccChannelManagerService: EuiccChannelManagerService
suspend fun <T, R> T.withEuiccChannel(fn: suspend (EuiccChannel) -> R): R suspend fun <T, R> T.withEuiccChannel(fn: suspend (EuiccChannel) -> R): R
where T : Fragment, T : EuiccChannelFragmentMarker { where T : Fragment, T : EuiccChannelFragmentMarker {
ensureEuiccChannelManager() ensureEuiccChannelManager()
return euiccChannelManager.withEuiccChannel(slotId, portId, fn) return euiccChannelManager.withEuiccChannel(
slotId,
portId,
seId,
fn
)
} }
suspend fun <T> T.ensureEuiccChannelManager() where T : Fragment, T : OpenEuiccContextMarker = suspend fun <T> T.ensureEuiccChannelManager() where T : Fragment, T : OpenEuiccContextMarker =

View file

@ -79,9 +79,10 @@ fun LocalProfileAssistant.disableActiveProfileKeepIccId(refresh: Boolean): Strin
suspend inline fun EuiccChannelManager.beginTrackedOperation( suspend inline fun EuiccChannelManager.beginTrackedOperation(
slotId: Int, slotId: Int,
portId: Int, portId: Int,
seId: EuiccChannel.SecureElementId,
op: () -> Boolean op: () -> Boolean
) { ) {
val latestSeq = withEuiccChannel(slotId, portId) { channel -> val latestSeq = withEuiccChannel(slotId, portId, seId) { channel ->
channel.lpa.notifications.firstOrNull()?.seqNumber channel.lpa.notifications.firstOrNull()?.seqNumber
?: 0 ?: 0
} }
@ -91,7 +92,7 @@ suspend inline fun EuiccChannelManager.beginTrackedOperation(
try { try {
// Note that the exact instance of "channel" might have changed here if reconnected; // Note that the exact instance of "channel" might have changed here if reconnected;
// this is why we need to use two distinct calls to withEuiccChannel() // this is why we need to use two distinct calls to withEuiccChannel()
withEuiccChannel(slotId, portId) { channel -> withEuiccChannel(slotId, portId, seId) { channel ->
channel.lpa.notifications.filter { it.seqNumber > latestSeq }.forEach { channel.lpa.notifications.filter { it.seqNumber > latestSeq }.forEach {
Log.d(TAG, "Handling notification $it") Log.d(TAG, "Handling notification $it")
channel.lpa.handleNotification(it.seqNumber) channel.lpa.handleNotification(it.seqNumber)

View file

@ -9,6 +9,7 @@
<string name="profile_no_enabled_profile">Unknown</string> <string name="profile_no_enabled_profile">Unknown</string>
<string name="channel_name_format">Logical Slot %d</string> <string name="channel_name_format">Logical Slot %d</string>
<string name="channel_name_format_se">Logical Slot %d, SE %d</string>
<string name="channel_type_usb" translatable="false">USB</string> <string name="channel_type_usb" translatable="false">USB</string>
<string name="channel_type_omapi" translatable="false">OpenMobile API (OMAPI)</string> <string name="channel_type_omapi" translatable="false">OpenMobile API (OMAPI)</string>

View file

@ -2,9 +2,16 @@ package im.angry.openeuicc.di
import android.content.Context import android.content.Context
import im.angry.easyeuicc.R import im.angry.easyeuicc.R
import im.angry.openeuicc.core.EuiccChannel
class UnprivilegedCustomizableTextProvider(private val context: Context) : class UnprivilegedCustomizableTextProvider(private val context: Context) :
DefaultCustomizableTextProvider(context) { DefaultCustomizableTextProvider(context) {
override fun formatInternalChannelName(logicalSlotId: Int): String = override fun formatNonUsbChannelName(logicalSlotId: Int): String =
context.getString(R.string.channel_name_format_unpriv, logicalSlotId) context.getString(R.string.channel_name_format_unpriv, logicalSlotId)
override fun formatNonUsbChannelNameWithSeId(
logicalSlotId: Int,
seId: EuiccChannel.SecureElementId
): String =
context.getString(R.string.channel_name_format_unpriv_se, logicalSlotId, seId.id)
} }

View file

@ -1,6 +1,7 @@
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.QuickCompatibilityFragment import im.angry.openeuicc.ui.QuickCompatibilityFragment
import im.angry.openeuicc.ui.UnprivilegedEuiccManagementFragment import im.angry.openeuicc.ui.UnprivilegedEuiccManagementFragment
@ -8,8 +9,12 @@ import im.angry.openeuicc.ui.UnprivilegedNoEuiccPlaceholderFragment
import im.angry.openeuicc.ui.UnprivilegedSettingsFragment import im.angry.openeuicc.ui.UnprivilegedSettingsFragment
open class UnprivilegedUiComponentFactory : DefaultUiComponentFactory() { open class UnprivilegedUiComponentFactory : DefaultUiComponentFactory() {
override fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment = override fun createEuiccManagementFragment(
UnprivilegedEuiccManagementFragment.newInstance(slotId, portId) slotId: Int,
portId: Int,
seId: EuiccChannel.SecureElementId
): EuiccManagementFragment =
UnprivilegedEuiccManagementFragment.newInstance(slotId, portId, seId)
override fun createNoEuiccPlaceholderFragment(): Fragment = override fun createNoEuiccPlaceholderFragment(): Fragment =
UnprivilegedNoEuiccPlaceholderFragment() UnprivilegedNoEuiccPlaceholderFragment()

View file

@ -148,7 +148,7 @@ open class QuickCompatibilityFragment : Fragment(), UnprivilegedEuiccContextMark
if (omapiSlots.isEmpty()) { if (omapiSlots.isEmpty()) {
return CompatibilityResult(Compatibility.NOT_COMPATIBLE) return CompatibilityResult(Compatibility.NOT_COMPATIBLE)
} }
val formatChannelName = appContainer.customizableTextProvider::formatInternalChannelName val formatChannelName = appContainer.customizableTextProvider::formatNonUsbChannelName
return CompatibilityResult( return CompatibilityResult(
Compatibility.COMPATIBLE, Compatibility.COMPATIBLE,
slotsOmapi = omapiSlots.map(formatChannelName), slotsOmapi = omapiSlots.map(formatChannelName),

View file

@ -7,6 +7,7 @@ import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.widget.Toast import android.widget.Toast
import im.angry.easyeuicc.R import im.angry.easyeuicc.R
import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.util.SIMToolkit import im.angry.openeuicc.util.SIMToolkit
import im.angry.openeuicc.util.newInstanceEuicc import im.angry.openeuicc.util.newInstanceEuicc
import im.angry.openeuicc.util.slotId import im.angry.openeuicc.util.slotId
@ -16,8 +17,12 @@ class UnprivilegedEuiccManagementFragment : EuiccManagementFragment() {
companion object { companion object {
const val TAG = "UnprivilegedEuiccManagementFragment" const val TAG = "UnprivilegedEuiccManagementFragment"
fun newInstance(slotId: Int, portId: Int): EuiccManagementFragment = fun newInstance(
newInstanceEuicc(UnprivilegedEuiccManagementFragment::class.java, slotId, portId) slotId: Int,
portId: Int,
seId: EuiccChannel.SecureElementId
): EuiccManagementFragment =
newInstanceEuicc(UnprivilegedEuiccManagementFragment::class.java, slotId, portId, seId)
} }
private val stk by lazy { private val stk by lazy {

View file

@ -1,6 +1,7 @@
<resources> <resources>
<string name="app_name" translatable="false">EasyEUICC</string> <string name="app_name" translatable="false">EasyEUICC</string>
<string name="channel_name_format_unpriv" translatable="false">SIM %d</string> <string name="channel_name_format_unpriv" translatable="false">SIM %d</string>
<string name="channel_name_format_unpriv_se" translatable="false">SIM %d, SE %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> <string name="open_sim_toolkit">Open SIM Toolkit</string>

View file

@ -15,13 +15,14 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto
@Suppress("NAME_SHADOWING") @Suppress("NAME_SHADOWING")
override suspend fun tryOpenEuiccChannel( override suspend fun tryOpenEuiccChannel(
port: UiccPortInfoCompat, port: UiccPortInfoCompat,
isdrAid: ByteArray isdrAid: ByteArray,
seId: EuiccChannel.SecureElementId,
): EuiccChannel? { ): EuiccChannel? {
val port = port as RealUiccPortInfoCompat val port = port as RealUiccPortInfoCompat
if (port.card.isRemovable) { if (port.card.isRemovable) {
// Attempt unprivileged (OMAPI) before TelephonyManager // Attempt unprivileged (OMAPI) before TelephonyManager
// but still try TelephonyManager in case OMAPI is broken // but still try TelephonyManager in case OMAPI is broken
super.tryOpenEuiccChannel(port, isdrAid)?.let { return it } super.tryOpenEuiccChannel(port, isdrAid, seId)?.let { return it }
} }
if (port.card.isEuicc || preferenceRepository.removableTelephonyManagerFlow.first()) { if (port.card.isEuicc || preferenceRepository.removableTelephonyManagerFlow.first()) {
@ -40,6 +41,7 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto
context.preferenceRepository.verboseLoggingFlow context.preferenceRepository.verboseLoggingFlow
), ),
isdrAid, isdrAid,
seId,
context.preferenceRepository.verboseLoggingFlow, context.preferenceRepository.verboseLoggingFlow,
context.preferenceRepository.ignoreTLSCertificateFlow, context.preferenceRepository.ignoreTLSCertificateFlow,
context.preferenceRepository.es10xMssFlow, context.preferenceRepository.es10xMssFlow,
@ -53,6 +55,6 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto
} }
} }
return super.tryOpenEuiccChannel(port, isdrAid) return super.tryOpenEuiccChannel(port, isdrAid, seId)
} }
} }

View file

@ -1,13 +1,18 @@
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.PrivilegedEuiccManagementFragment import im.angry.openeuicc.ui.PrivilegedEuiccManagementFragment
import im.angry.openeuicc.ui.PrivilegedSettingsFragment import im.angry.openeuicc.ui.PrivilegedSettingsFragment
class PrivilegedUiComponentFactory : DefaultUiComponentFactory() { class PrivilegedUiComponentFactory : DefaultUiComponentFactory() {
override fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment = override fun createEuiccManagementFragment(
PrivilegedEuiccManagementFragment.newInstance(slotId, portId) slotId: Int,
portId: Int,
seId: EuiccChannel.SecureElementId
): EuiccManagementFragment =
PrivilegedEuiccManagementFragment.newInstance(slotId, portId, seId)
override fun createSettingsFragment(): Fragment = override fun createSettingsFragment(): Fragment =
PrivilegedSettingsFragment() PrivilegedSettingsFragment()

View file

@ -8,6 +8,7 @@ import android.telephony.UiccSlotMapping
import android.telephony.euicc.DownloadableSubscription import android.telephony.euicc.DownloadableSubscription
import android.telephony.euicc.EuiccInfo import android.telephony.euicc.EuiccInfo
import android.util.Log import android.util.Log
import im.angry.openeuicc.core.EuiccChannel
import net.typeblog.lpac_jni.LocalProfileInfo import net.typeblog.lpac_jni.LocalProfileInfo
import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
@ -165,7 +166,8 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
return GetDefaultDownloadableSubscriptionListResult(RESULT_OK, arrayOf()) return GetDefaultDownloadableSubscriptionListResult(RESULT_OK, arrayOf())
} }
override fun onGetEuiccProfileInfoList(slotId: Int): GetEuiccProfileInfoListResult = withEuiccChannelManager { override fun onGetEuiccProfileInfoList(slotId: Int): GetEuiccProfileInfoListResult =
withEuiccChannelManager {
Log.i(TAG, "onGetEuiccProfileInfoList slotId=$slotId") Log.i(TAG, "onGetEuiccProfileInfoList slotId=$slotId")
if (slotId == -1 || shouldIgnoreSlot(slotId)) { if (slotId == -1 || shouldIgnoreSlot(slotId)) {
Log.i(TAG, "ignoring slot $slotId") Log.i(TAG, "ignoring slot $slotId")
@ -250,7 +252,12 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
if (enabledAnywhere) return@withEuiccChannelManager RESULT_FIRST_USER if (enabledAnywhere) return@withEuiccChannelManager RESULT_FIRST_USER
euiccChannelManagerService.waitForForegroundTask() euiccChannelManagerService.waitForForegroundTask()
val success = euiccChannelManagerService.launchProfileDeleteTask(slotId, ports[0], iccid) val success = euiccChannelManagerService.launchProfileDeleteTask(
slotId,
ports[0],
EuiccChannel.SecureElementId.DEFAULT,
iccid
)
.waitDone() == null .waitDone() == null
return@withEuiccChannelManager if (success) { return@withEuiccChannelManager if (success) {
@ -275,7 +282,10 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
iccid: String?, iccid: String?,
forceDeactivateSim: Boolean forceDeactivateSim: Boolean
): Int = withEuiccChannelManager { ): Int = withEuiccChannelManager {
Log.i(TAG,"onSwitchToSubscriptionWithPort slotId=$slotId portIndex=$portIndex iccid=$iccid forceDeactivateSim=$forceDeactivateSim") Log.i(
TAG,
"onSwitchToSubscriptionWithPort slotId=$slotId portIndex=$portIndex iccid=$iccid forceDeactivateSim=$forceDeactivateSim"
)
if (shouldIgnoreSlot(slotId)) return@withEuiccChannelManager RESULT_FIRST_USER if (shouldIgnoreSlot(slotId)) return@withEuiccChannelManager RESULT_FIRST_USER
try { try {
@ -357,6 +367,7 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
val res = euiccChannelManagerService.launchProfileSwitchTask( val res = euiccChannelManagerService.launchProfileSwitchTask(
foundSlotId, foundSlotId,
foundPortId, foundPortId,
EuiccChannel.SecureElementId.DEFAULT,
foundIccid, foundIccid,
enable, enable,
30 * 1000 30 * 1000
@ -386,7 +397,13 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
euiccChannelManagerService.waitForForegroundTask() euiccChannelManagerService.waitForForegroundTask()
val success = val success =
(euiccChannelManagerService.launchProfileRenameTask(slotId, port, iccid, nickname!!) (euiccChannelManagerService.launchProfileRenameTask(
slotId,
port,
EuiccChannel.SecureElementId.DEFAULT,
iccid,
nickname!!
)
.waitDone()) == null .waitDone()) == null
euiccChannelManager.withEuiccChannel(slotId, port) { channel -> euiccChannelManager.withEuiccChannel(slotId, port) { channel ->

View file

@ -5,13 +5,18 @@ import android.view.ViewGroup
import android.widget.Button import android.widget.Button
import android.widget.PopupMenu import android.widget.PopupMenu
import im.angry.openeuicc.R import im.angry.openeuicc.R
import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import net.typeblog.lpac_jni.LocalProfileInfo import net.typeblog.lpac_jni.LocalProfileInfo
class PrivilegedEuiccManagementFragment : EuiccManagementFragment() { class PrivilegedEuiccManagementFragment : EuiccManagementFragment() {
companion object { companion object {
fun newInstance(slotId: Int, portId: Int): EuiccManagementFragment = fun newInstance(
newInstanceEuicc(PrivilegedEuiccManagementFragment::class.java, slotId, portId) slotId: Int,
portId: Int,
seId: EuiccChannel.SecureElementId
): EuiccManagementFragment =
newInstanceEuicc(PrivilegedEuiccManagementFragment::class.java, slotId, portId, seId)
} }
private var isMEP = false private var isMEP = false