WIP: feat: Support for removable eSIM with multiple SEs #239
35 changed files with 595 additions and 236 deletions
|
@ -20,7 +20,8 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
|
|||
|
||||
override suspend fun tryOpenEuiccChannel(
|
||||
port: UiccPortInfoCompat,
|
||||
isdrAid: ByteArray
|
||||
isdrAid: ByteArray,
|
||||
seId: EuiccChannel.SecureElementId,
|
||||
): EuiccChannel? = try {
|
||||
if (port.portIndex != 0) {
|
||||
Log.w(
|
||||
|
@ -45,6 +46,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
|
|||
context.preferenceRepository.verboseLoggingFlow
|
||||
),
|
||||
isdrAid,
|
||||
seId,
|
||||
context.preferenceRepository.verboseLoggingFlow,
|
||||
context.preferenceRepository.ignoreTLSCertificateFlow,
|
||||
context.preferenceRepository.es10xMssFlow,
|
||||
|
@ -60,7 +62,8 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
|
|||
|
||||
override fun tryOpenUsbEuiccChannel(
|
||||
ccidCtx: UsbCcidContext,
|
||||
isdrAid: ByteArray
|
||||
isdrAid: ByteArray,
|
||||
seId: EuiccChannel.SecureElementId
|
||||
): EuiccChannel? = try {
|
||||
EuiccChannelImpl(
|
||||
context.getString(R.string.channel_type_usb),
|
||||
|
@ -70,6 +73,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
|
|||
ccidCtx
|
||||
),
|
||||
isdrAid,
|
||||
seId,
|
||||
context.preferenceRepository.verboseLoggingFlow,
|
||||
context.preferenceRepository.ignoreTLSCertificateFlow,
|
||||
context.preferenceRepository.es10xMssFlow,
|
||||
|
|
|
@ -32,7 +32,7 @@ open class DefaultEuiccChannelManager(
|
|||
|
||||
private val channelCache = mutableListOf<EuiccChannel>()
|
||||
|
||||
private var usbChannel: EuiccChannel? = null
|
||||
private var usbChannels = mutableListOf<EuiccChannel>()
|
||||
|
||||
private val lock = Mutex()
|
||||
|
||||
|
@ -51,15 +51,20 @@ open class DefaultEuiccChannelManager(
|
|||
protected open val uiccCards: Collection<UiccCardInfoCompat>
|
||||
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 =
|
||||
parseIsdrAidList(appContainer.preferenceRepository.isdrAidListFlow.first())
|
||||
var seId = 0
|
||||
|
||||
return isdrAidList.firstNotNullOfOrNull {
|
||||
Log.i(TAG, "Opening channel, trying ISDR AID ${it.encodeHex()}")
|
||||
return isdrAidList.mapNotNull {
|
||||
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) {
|
||||
seId += 1
|
||||
channel
|
||||
} else {
|
||||
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 {
|
||||
if (port.card.physicalSlotIndex == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
return if (usbChannel != null && usbChannel!!.valid) {
|
||||
usbChannel
|
||||
} else {
|
||||
usbChannel = null
|
||||
null
|
||||
}
|
||||
// We only compare seId because we assume we can only open 1 card from USB
|
||||
return usbChannels.find { it.seId == seId }
|
||||
}
|
||||
|
||||
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.valid && port.logicalSlotIndex == existing.logicalSlotId) {
|
||||
return existing
|
||||
|
@ -96,12 +100,18 @@ open class DefaultEuiccChannelManager(
|
|||
return null
|
||||
}
|
||||
|
||||
val channel =
|
||||
tryOpenChannelFirstValidAid { euiccChannelFactory.tryOpenEuiccChannel(port, it) }
|
||||
val channels =
|
||||
tryOpenChannelWithKnownAids { isdrAid, seId ->
|
||||
euiccChannelFactory.tryOpenEuiccChannel(
|
||||
port,
|
||||
isdrAid,
|
||||
seId
|
||||
)
|
||||
}
|
||||
|
||||
if (channel != null) {
|
||||
channelCache.add(channel)
|
||||
return channel
|
||||
if (channels.isNotEmpty()) {
|
||||
channelCache.addAll(channels)
|
||||
return channels.find { it.seId == seId }
|
||||
} else {
|
||||
Log.i(
|
||||
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) {
|
||||
if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
return@withContext usbChannel
|
||||
return@withContext usbChannels.find { it.seId == seId }
|
||||
}
|
||||
|
||||
for (card in uiccCards) {
|
||||
|
@ -131,7 +144,7 @@ open class DefaultEuiccChannelManager(
|
|||
|
||||
private suspend fun findAllEuiccChannelsByPhysicalSlot(physicalSlotId: Int): List<EuiccChannel>? {
|
||||
if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
return usbChannel?.let { listOf(it) }
|
||||
return usbChannels.ifEmpty { null }
|
||||
}
|
||||
|
||||
for (card in uiccCards) {
|
||||
|
@ -142,14 +155,18 @@ open class DefaultEuiccChannelManager(
|
|||
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) {
|
||||
if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
return@withContext usbChannel
|
||||
return@withContext usbChannels.find { it.seId == seId }
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
findAllEuiccChannelsByPhysicalSlot(physicalSlotId)?.map { it.portId } ?: listOf()
|
||||
findAllEuiccChannelsByPhysicalSlot(physicalSlotId)?.map { it.portId }?.toSet()?.toList()
|
||||
?: listOf()
|
||||
}
|
||||
|
||||
override suspend fun <R> withEuiccChannel(
|
||||
physicalSlotId: Int,
|
||||
portId: Int,
|
||||
seId: EuiccChannel.SecureElementId,
|
||||
fn: suspend (EuiccChannel) -> R
|
||||
): R {
|
||||
val channel = findEuiccChannelByPort(physicalSlotId, portId)
|
||||
val channel = findEuiccChannelByPort(physicalSlotId, portId, seId)
|
||||
?: throw EuiccChannelManager.EuiccChannelNotFoundException()
|
||||
val wrapper = EuiccChannelWrapper(channel)
|
||||
try {
|
||||
|
@ -190,9 +209,10 @@ open class DefaultEuiccChannelManager(
|
|||
|
||||
override suspend fun <R> withEuiccChannel(
|
||||
logicalSlotId: Int,
|
||||
seId: EuiccChannel.SecureElementId,
|
||||
fn: suspend (EuiccChannel) -> R
|
||||
): R {
|
||||
val channel = findEuiccChannelByLogicalSlot(logicalSlotId)
|
||||
val channel = findEuiccChannelByLogicalSlot(logicalSlotId, seId)
|
||||
?: throw EuiccChannelManager.EuiccChannelNotFoundException()
|
||||
val wrapper = EuiccChannelWrapper(channel)
|
||||
try {
|
||||
|
@ -206,8 +226,8 @@ open class DefaultEuiccChannelManager(
|
|||
|
||||
override suspend fun waitForReconnect(physicalSlotId: Int, portId: Int, timeoutMillis: Long) {
|
||||
if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
usbChannel?.close()
|
||||
usbChannel = null
|
||||
usbChannels.forEach { it.close() }
|
||||
usbChannels.clear()
|
||||
} else {
|
||||
// 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
|
||||
|
@ -223,7 +243,7 @@ open class DefaultEuiccChannelManager(
|
|||
// tryOpenUsbEuiccChannel() will always try to reopen the channel, even if
|
||||
// a USB channel already exists
|
||||
tryOpenUsbEuiccChannel()
|
||||
usbChannel!!
|
||||
usbChannels.getOrNull(0)!!
|
||||
} else {
|
||||
// tryOpenEuiccChannel() will automatically dispose of invalid channels
|
||||
// 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> =
|
||||
withContext(Dispatchers.IO) {
|
||||
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"
|
||||
)
|
||||
|
||||
val ccidCtx = UsbCcidContext.createFromUsbDevice(context, device, iface) ?: return@forEach
|
||||
val ccidCtx =
|
||||
UsbCcidContext.createFromUsbDevice(context, device, iface) ?: return@forEach
|
||||
|
||||
try {
|
||||
val channel = tryOpenChannelFirstValidAid {
|
||||
euiccChannelFactory.tryOpenUsbEuiccChannel(ccidCtx, it)
|
||||
val channels = tryOpenChannelWithKnownAids { isdrAid, seId ->
|
||||
euiccChannelFactory.tryOpenUsbEuiccChannel(ccidCtx, isdrAid, seId)
|
||||
}
|
||||
if (channel != null && channel.lpa.valid) {
|
||||
if (channels.isNotEmpty() && channels[0].valid) {
|
||||
ccidCtx.allowDisconnect = true
|
||||
usbChannel = channel
|
||||
usbChannels.clear()
|
||||
usbChannels.addAll(channels)
|
||||
return@withContext Pair(device, true)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
@ -309,8 +345,8 @@ open class DefaultEuiccChannelManager(
|
|||
channel.close()
|
||||
}
|
||||
|
||||
usbChannel?.close()
|
||||
usbChannel = null
|
||||
usbChannels.forEach { it.close() }
|
||||
usbChannels.clear()
|
||||
channelCache.clear()
|
||||
euiccChannelFactory.cleanup()
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package im.angry.openeuicc.core
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import im.angry.openeuicc.util.*
|
||||
import net.typeblog.lpac_jni.ApduInterface
|
||||
import net.typeblog.lpac_jni.LocalProfileAssistant
|
||||
|
@ -13,6 +15,59 @@ interface EuiccChannel {
|
|||
val logicalSlotId: 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 valid: Boolean
|
||||
|
|
|
@ -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
|
||||
// "dumb" dependency injection.
|
||||
interface EuiccChannelFactory {
|
||||
suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat, isdrAid: ByteArray): EuiccChannel?
|
||||
suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat, isdrAid: ByteArray, seId: EuiccChannel.SecureElementId): EuiccChannel?
|
||||
|
||||
fun tryOpenUsbEuiccChannel(
|
||||
ccidCtx: UsbCcidContext,
|
||||
isdrAid: ByteArray
|
||||
isdrAid: ByteArray,
|
||||
seId: EuiccChannel.SecureElementId
|
||||
): EuiccChannel?
|
||||
|
||||
/**
|
||||
|
|
|
@ -15,6 +15,7 @@ class EuiccChannelImpl(
|
|||
override val intrinsicChannelName: String?,
|
||||
override val apduInterface: ApduInterface,
|
||||
override val isdrAid: ByteArray,
|
||||
override val seId: EuiccChannel.SecureElementId,
|
||||
verboseLoggingFlow: Flow<Boolean>,
|
||||
ignoreTLSCertificateFlow: Flow<Boolean>,
|
||||
es10xMssFlow: Flow<Int>,
|
||||
|
|
|
@ -37,6 +37,14 @@ interface EuiccChannelManager {
|
|||
*/
|
||||
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.
|
||||
* 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(
|
||||
physicalSlotId: Int,
|
||||
portId: Int,
|
||||
seId: EuiccChannel.SecureElementId = EuiccChannel.SecureElementId.DEFAULT,
|
||||
fn: suspend (EuiccChannel) -> 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(
|
||||
logicalSlotId: Int,
|
||||
seId: EuiccChannel.SecureElementId = EuiccChannel.SecureElementId.DEFAULT,
|
||||
fn: suspend (EuiccChannel) -> R
|
||||
): R
|
||||
|
||||
|
|
|
@ -26,6 +26,8 @@ class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel {
|
|||
get() = channel.logicalSlotId
|
||||
override val portId: Int
|
||||
get() = channel.portId
|
||||
override val seId: EuiccChannel.SecureElementId
|
||||
get() = channel.seId
|
||||
private val lpaDelegate = lazy {
|
||||
LocalProfileAssistantWrapper(channel.lpa)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package im.angry.openeuicc.di
|
||||
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
|
||||
interface CustomizableTextProvider {
|
||||
/**
|
||||
* Explanation string for when no eUICC is found on the device.
|
||||
|
@ -13,8 +15,13 @@ interface CustomizableTextProvider {
|
|||
val profileSwitchingTimeoutMessage: String
|
||||
|
||||
/**
|
||||
* Format the name of a logical slot; internal only -- not intended for
|
||||
* other channels such as USB.
|
||||
* Format the name of a logical slot -- not for USB channels
|
||||
*/
|
||||
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
|
||||
}
|
|
@ -2,14 +2,22 @@ package im.angry.openeuicc.di
|
|||
|
||||
import android.content.Context
|
||||
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
|
||||
get() = context.getString(R.string.no_euicc)
|
||||
|
||||
override val profileSwitchingTimeoutMessage: String
|
||||
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)
|
||||
|
||||
override fun formatNonUsbChannelNameWithSeId(
|
||||
logicalSlotId: Int,
|
||||
seId: EuiccChannel.SecureElementId
|
||||
): String =
|
||||
context.getString(R.string.channel_name_format_se, logicalSlotId, seId.id)
|
||||
}
|
|
@ -2,13 +2,18 @@ package im.angry.openeuicc.di
|
|||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
import im.angry.openeuicc.ui.EuiccManagementFragment
|
||||
import im.angry.openeuicc.ui.NoEuiccPlaceholderFragment
|
||||
import im.angry.openeuicc.ui.SettingsFragment
|
||||
|
||||
open class DefaultUiComponentFactory : UiComponentFactory {
|
||||
override fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment =
|
||||
EuiccManagementFragment.newInstance(slotId, portId)
|
||||
override fun createEuiccManagementFragment(
|
||||
slotId: Int,
|
||||
portId: Int,
|
||||
seId: EuiccChannel.SecureElementId
|
||||
): EuiccManagementFragment =
|
||||
EuiccManagementFragment.newInstance(slotId, portId, seId)
|
||||
|
||||
override fun createNoEuiccPlaceholderFragment(): Fragment = NoEuiccPlaceholderFragment()
|
||||
|
||||
|
|
|
@ -2,10 +2,16 @@ package im.angry.openeuicc.di
|
|||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
import im.angry.openeuicc.ui.EuiccManagementFragment
|
||||
|
||||
interface UiComponentFactory {
|
||||
fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment
|
||||
fun createEuiccManagementFragment(
|
||||
slotId: Int,
|
||||
portId: Int,
|
||||
seId: EuiccChannel.SecureElementId
|
||||
): EuiccManagementFragment
|
||||
|
||||
fun createNoEuiccPlaceholderFragment(): Fragment
|
||||
fun createSettingsFragment(): Fragment
|
||||
}
|
|
@ -12,6 +12,7 @@ import androidx.core.app.NotificationManagerCompat
|
|||
import androidx.lifecycle.LifecycleService
|
||||
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
|
||||
|
@ -380,6 +381,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
fun launchProfileDownloadTask(
|
||||
slotId: Int,
|
||||
portId: Int,
|
||||
seId: EuiccChannel.SecureElementId,
|
||||
smdp: String,
|
||||
matchingId: String?,
|
||||
confirmationCode: String?,
|
||||
|
@ -390,8 +392,8 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
getString(R.string.task_profile_download_failure),
|
||||
R.drawable.ic_task_sim_card_download
|
||||
) {
|
||||
euiccChannelManager.beginTrackedOperation(slotId, portId) {
|
||||
euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
|
||||
euiccChannelManager.beginTrackedOperation(slotId, portId, seId) {
|
||||
euiccChannelManager.withEuiccChannel(slotId, portId, seId) { channel ->
|
||||
channel.lpa.downloadProfile(
|
||||
smdp,
|
||||
matchingId,
|
||||
|
@ -413,6 +415,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
fun launchProfileRenameTask(
|
||||
slotId: Int,
|
||||
portId: Int,
|
||||
seId: EuiccChannel.SecureElementId,
|
||||
iccid: String,
|
||||
name: String
|
||||
): ForegroundTaskSubscriberFlow =
|
||||
|
@ -421,7 +424,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
getString(R.string.task_profile_rename_failure),
|
||||
R.drawable.ic_task_rename
|
||||
) {
|
||||
euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
|
||||
euiccChannelManager.withEuiccChannel(slotId, portId, seId) { channel ->
|
||||
channel.lpa.setNickname(
|
||||
iccid,
|
||||
name
|
||||
|
@ -432,6 +435,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
fun launchProfileDeleteTask(
|
||||
slotId: Int,
|
||||
portId: Int,
|
||||
seId: EuiccChannel.SecureElementId,
|
||||
iccid: String
|
||||
): ForegroundTaskSubscriberFlow =
|
||||
launchForegroundTask(
|
||||
|
@ -439,8 +443,8 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
getString(R.string.task_profile_delete_failure),
|
||||
R.drawable.ic_task_delete
|
||||
) {
|
||||
euiccChannelManager.beginTrackedOperation(slotId, portId) {
|
||||
euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
|
||||
euiccChannelManager.beginTrackedOperation(slotId, portId, seId) {
|
||||
euiccChannelManager.withEuiccChannel(slotId, portId, seId) { channel ->
|
||||
channel.lpa.deleteProfile(iccid)
|
||||
}
|
||||
|
||||
|
@ -453,6 +457,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
fun launchProfileSwitchTask(
|
||||
slotId: Int,
|
||||
portId: Int,
|
||||
seId: EuiccChannel.SecureElementId,
|
||||
iccid: String,
|
||||
enable: Boolean, // Enable or disable the profile indicated in iccid
|
||||
reconnectTimeoutMillis: Long = 0 // 0 = do not wait for reconnect
|
||||
|
@ -462,9 +467,9 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
getString(R.string.task_profile_switch_failure),
|
||||
R.drawable.ic_task_switch
|
||||
) {
|
||||
euiccChannelManager.beginTrackedOperation(slotId, portId) {
|
||||
euiccChannelManager.beginTrackedOperation(slotId, portId, seId) {
|
||||
val (response, refreshed) =
|
||||
euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
|
||||
euiccChannelManager.withEuiccChannel(slotId, portId, seId) { channel ->
|
||||
val refresh = preferenceRepository.refreshAfterSwitchFlow.first()
|
||||
val response = channel.lpa.switchProfile(iccid, enable, 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(
|
||||
getString(R.string.task_euicc_memory_reset),
|
||||
getString(R.string.task_euicc_memory_reset_failure),
|
||||
R.drawable.ic_euicc_memory_reset
|
||||
) {
|
||||
euiccChannelManager.beginTrackedOperation(slotId, portId) {
|
||||
euiccChannelManager.beginTrackedOperation(slotId, portId, seId) {
|
||||
euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
|
||||
channel.lpa.euiccMemoryReset()
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
private lateinit var infoList: RecyclerView
|
||||
|
||||
private var logicalSlotId: Int = -1
|
||||
private var seId: EuiccChannel.SecureElementId = EuiccChannel.SecureElementId.DEFAULT
|
||||
|
||||
data class Item(
|
||||
@StringRes
|
||||
|
@ -67,11 +68,17 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
}
|
||||
|
||||
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) {
|
||||
getString(R.string.channel_type_usb)
|
||||
} else {
|
||||
appContainer.customizableTextProvider.formatInternalChannelName(logicalSlotId)
|
||||
appContainer.customizableTextProvider.formatNonUsbChannelName(logicalSlotId)
|
||||
}
|
||||
|
||||
title = getString(R.string.euicc_info_activity_title, channelTitle)
|
||||
|
@ -99,7 +106,7 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
|
||||
lifecycleScope.launch {
|
||||
(infoList.adapter!! as EuiccInfoAdapter).euiccInfoItems =
|
||||
euiccChannelManager.withEuiccChannel(logicalSlotId, ::buildEuiccInfoItems)
|
||||
euiccChannelManager.withEuiccChannel(logicalSlotId, fn = ::buildEuiccInfoItems)
|
||||
|
||||
swipeRefresh.isRefreshing = false
|
||||
}
|
||||
|
@ -107,12 +114,31 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
|
||||
private fun buildEuiccInfoItems(channel: EuiccChannel) = buildList {
|
||||
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(Item(R.string.euicc_info_eid, channel.lpa.eID, copiedToastResId = R.string.toast_eid_copied))
|
||||
add(
|
||||
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()))
|
||||
channel.tryParseEuiccVendorInfo()?.let { vendorInfo ->
|
||||
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.bootloaderVersion?.let { add(Item(R.string.euicc_info_bl_ver, it)) }
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
|||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import net.typeblog.lpac_jni.LocalProfileInfo
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
import im.angry.openeuicc.service.EuiccChannelManagerService
|
||||
import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
|
||||
import im.angry.openeuicc.ui.wizard.DownloadWizardActivity
|
||||
|
@ -49,8 +50,12 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
companion object {
|
||||
const val TAG = "EuiccManagementFragment"
|
||||
|
||||
fun newInstance(slotId: Int, portId: Int): EuiccManagementFragment =
|
||||
newInstanceEuicc(EuiccManagementFragment::class.java, slotId, portId)
|
||||
fun newInstance(
|
||||
slotId: Int,
|
||||
portId: Int,
|
||||
seId: EuiccChannel.SecureElementId
|
||||
): EuiccManagementFragment =
|
||||
newInstanceEuicc(EuiccManagementFragment::class.java, slotId, portId, seId)
|
||||
}
|
||||
|
||||
private lateinit var swipeRefresh: SwipeRefreshLayout
|
||||
|
@ -148,6 +153,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
R.id.show_notifications -> {
|
||||
Intent(requireContext(), NotificationsActivity::class.java).apply {
|
||||
putExtra("logicalSlotId", logicalSlotId)
|
||||
putExtra("seId", seId)
|
||||
startActivity(this)
|
||||
}
|
||||
true
|
||||
|
@ -156,13 +162,14 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
R.id.euicc_info -> {
|
||||
Intent(requireContext(), EuiccInfoActivity::class.java).apply {
|
||||
putExtra("logicalSlotId", logicalSlotId)
|
||||
putExtra("seId", seId)
|
||||
startActivity(this)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.euicc_memory_reset -> {
|
||||
EuiccMemoryResetFragment.newInstance(slotId, portId, eid)
|
||||
EuiccMemoryResetFragment.newInstance(slotId, portId, seId, eid)
|
||||
.show(childFragmentManager, EuiccMemoryResetFragment.TAG)
|
||||
true
|
||||
}
|
||||
|
@ -241,6 +248,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
val err = euiccChannelManagerService.launchProfileSwitchTask(
|
||||
slotId,
|
||||
portId,
|
||||
seId,
|
||||
iccid,
|
||||
enable,
|
||||
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)
|
||||
if (profile.isEnabled) {
|
||||
popup.menu.findItem(R.id.enable).isVisible = false
|
||||
|
@ -423,20 +434,36 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
enableOrDisableProfile(profile.iccid, true)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.disable -> {
|
||||
enableOrDisableProfile(profile.iccid, false)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.rename -> {
|
||||
ProfileRenameFragment.newInstance(slotId, portId, profile.iccid, profile.displayName)
|
||||
ProfileRenameFragment.newInstance(
|
||||
slotId,
|
||||
portId,
|
||||
seId,
|
||||
profile.iccid,
|
||||
profile.displayName
|
||||
)
|
||||
.show(childFragmentManager, ProfileRenameFragment.TAG)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.delete -> {
|
||||
ProfileDeleteFragment.newInstance(slotId, portId, profile.iccid, profile.displayName)
|
||||
ProfileDeleteFragment.newInstance(
|
||||
slotId,
|
||||
portId,
|
||||
seId,
|
||||
profile.iccid,
|
||||
profile.displayName
|
||||
)
|
||||
.show(childFragmentManager, ProfileDeleteFragment.TAG)
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
@ -448,9 +475,11 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
|
||||
when (ViewHolder.Type.fromInt(viewType)) {
|
||||
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)
|
||||
}
|
||||
|
||||
ViewHolder.Type.FOOTER -> {
|
||||
FooterViewHolder()
|
||||
}
|
||||
|
@ -461,9 +490,11 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
position < profiles.size -> {
|
||||
ViewHolder.Type.PROFILE.value
|
||||
}
|
||||
|
||||
position >= profiles.size && position < profiles.size + footerViews.size -> {
|
||||
ViewHolder.Type.FOOTER.value
|
||||
}
|
||||
|
||||
else -> -1
|
||||
}
|
||||
|
||||
|
@ -473,6 +504,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
holder.setProfile(profiles[position])
|
||||
holder.setProfileSequenceNumber(position + 1)
|
||||
}
|
||||
|
||||
is FooterViewHolder -> {
|
||||
holder.attach(footerViews[position - profiles.size])
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import androidx.fragment.app.DialogFragment
|
|||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
|
||||
import im.angry.openeuicc.util.EuiccChannelFragmentMarker
|
||||
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.notifyEuiccProfilesChanged
|
||||
import im.angry.openeuicc.util.portId
|
||||
import im.angry.openeuicc.util.seId
|
||||
import im.angry.openeuicc.util.slotId
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -29,8 +31,8 @@ class EuiccMemoryResetFragment : DialogFragment(), EuiccChannelFragmentMarker {
|
|||
|
||||
private const val FIELD_EID = "eid"
|
||||
|
||||
fun newInstance(slotId: Int, portId: Int, eid: String) =
|
||||
newInstanceEuicc(EuiccMemoryResetFragment::class.java, slotId, portId) {
|
||||
fun newInstance(slotId: Int, portId: Int, seId: EuiccChannel.SecureElementId, eid: String) =
|
||||
newInstanceEuicc(EuiccMemoryResetFragment::class.java, slotId, portId, seId) {
|
||||
putString(FIELD_EID, eid)
|
||||
}
|
||||
}
|
||||
|
@ -103,7 +105,7 @@ class EuiccMemoryResetFragment : DialogFragment(), EuiccChannelFragmentMarker {
|
|||
ensureEuiccChannelManager()
|
||||
euiccChannelManagerService.waitForForegroundTask()
|
||||
|
||||
euiccChannelManagerService.launchMemoryReset(slotId, portId)
|
||||
euiccChannelManagerService.launchMemoryReset(slotId, portId, seId)
|
||||
.onStart {
|
||||
parentFragment?.notifyEuiccProfilesChanged()
|
||||
|
||||
|
|
|
@ -112,10 +112,12 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
startActivity(Intent(this, SettingsActivity::class.java))
|
||||
true
|
||||
}
|
||||
|
||||
R.id.reload -> {
|
||||
refresh()
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
|
@ -154,7 +156,8 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
euiccChannelManager.flowInternalEuiccPorts().onEach { (slotId, 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()) {
|
||||
Log.d(TAG, channel.lpa.eID)
|
||||
}
|
||||
|
@ -164,12 +167,17 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId)
|
||||
|
||||
val channelName =
|
||||
appContainer.customizableTextProvider.formatInternalChannelName(channel.logicalSlotId)
|
||||
appContainer.customizableTextProvider.formatNonUsbChannelName(channel.logicalSlotId)
|
||||
newPages.add(Page(channel.logicalSlotId, channelName) {
|
||||
appContainer.uiComponentFactory.createEuiccManagementFragment(slotId, portId)
|
||||
appContainer.uiComponentFactory.createEuiccManagementFragment(
|
||||
slotId,
|
||||
portId,
|
||||
seId
|
||||
)
|
||||
})
|
||||
}
|
||||
}.collect()
|
||||
}.collect()
|
||||
|
||||
// If USB readers exist, add them at the very last
|
||||
// We use a wrapper fragment to handle logic specific to USB readers
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package im.angry.openeuicc.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.Html
|
||||
import android.view.ContextMenu
|
||||
|
@ -20,6 +21,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
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
|
||||
|
@ -33,6 +35,7 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
private val notificationAdapter = NotificationAdapter()
|
||||
|
||||
private var logicalSlotId = -1
|
||||
private var seId = EuiccChannel.SecureElementId.DEFAULT
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
|
@ -51,18 +54,29 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
override fun onInit() {
|
||||
notificationList.layoutManager =
|
||||
LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
|
||||
notificationList.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
|
||||
notificationList.addItemDecoration(
|
||||
DividerItemDecoration(
|
||||
this,
|
||||
LinearLayoutManager.VERTICAL
|
||||
)
|
||||
)
|
||||
notificationList.adapter = notificationAdapter
|
||||
registerForContextMenu(notificationList)
|
||||
|
||||
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
|
||||
// due to the length (we don't want to display the full USB product name)
|
||||
val channelTitle = if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
getString(R.string.channel_type_usb)
|
||||
} else {
|
||||
appContainer.customizableTextProvider.formatInternalChannelName(logicalSlotId)
|
||||
appContainer.customizableTextProvider.formatNonUsbChannelName(logicalSlotId)
|
||||
}
|
||||
|
||||
title = getString(R.string.profile_notifications_detailed_format, channelTitle)
|
||||
|
@ -86,6 +100,7 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
finish()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.help -> {
|
||||
AlertDialog.Builder(this, R.style.AlertDialogTheme).apply {
|
||||
setMessage(R.string.profile_notifications_help)
|
||||
|
@ -96,6 +111,7 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
}
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
|
@ -170,7 +186,8 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
LocalProfileNotification.Operation.Delete -> R.string.profile_notification_operation_delete
|
||||
LocalProfileNotification.Operation.Enable -> R.string.profile_notification_operation_enable
|
||||
LocalProfileNotification.Operation.Disable -> R.string.profile_notification_operation_disable
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
fun updateNotification(value: LocalProfileNotificationWrapper) {
|
||||
notification = value
|
||||
|
@ -181,10 +198,13 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
value.inner.seqNumber
|
||||
)
|
||||
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),
|
||||
value.profileName, value.inner.iccid),
|
||||
Html.FROM_HTML_MODE_COMPACT)
|
||||
value.profileName, value.inner.iccid
|
||||
),
|
||||
Html.FROM_HTML_MODE_COMPACT
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreateContextMenu(
|
||||
|
@ -213,6 +233,7 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.notification_delete -> {
|
||||
launchTask {
|
||||
withContext(Dispatchers.IO) {
|
||||
|
@ -225,6 +246,7 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
}
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import androidx.appcompat.app.AlertDialog
|
|||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
|
@ -20,8 +21,8 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
|
|||
private const val FIELD_ICCID = "iccid"
|
||||
private const val FIELD_NAME = "name"
|
||||
|
||||
fun newInstance(slotId: Int, portId: Int, iccid: String, name: String) =
|
||||
newInstanceEuicc(ProfileDeleteFragment::class.java, slotId, portId) {
|
||||
fun newInstance(slotId: Int, portId: Int, seId: EuiccChannel.SecureElementId, iccid: String, name: String) =
|
||||
newInstanceEuicc(ProfileDeleteFragment::class.java, slotId, portId, seId) {
|
||||
putString(FIELD_ICCID, iccid)
|
||||
putString(FIELD_NAME, name)
|
||||
}
|
||||
|
@ -88,7 +89,7 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
|
|||
requireParentFragment().lifecycleScope.launch {
|
||||
ensureEuiccChannelManager()
|
||||
euiccChannelManagerService.waitForForegroundTask()
|
||||
euiccChannelManagerService.launchProfileDeleteTask(slotId, portId, iccid)
|
||||
euiccChannelManagerService.launchProfileDeleteTask(slotId, portId, seId, iccid)
|
||||
.onStart {
|
||||
parentFragment?.notifyEuiccProfilesChanged()
|
||||
runCatching(::dismiss)
|
||||
|
|
|
@ -12,6 +12,7 @@ import androidx.appcompat.widget.Toolbar
|
|||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -24,8 +25,8 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
|
|||
|
||||
const val TAG = "ProfileRenameFragment"
|
||||
|
||||
fun newInstance(slotId: Int, portId: Int, iccid: String, currentName: String) =
|
||||
newInstanceEuicc(ProfileRenameFragment::class.java, slotId, portId) {
|
||||
fun newInstance(slotId: Int, portId: Int, seId: EuiccChannel.SecureElementId, iccid: String, currentName: String) =
|
||||
newInstanceEuicc(ProfileRenameFragment::class.java, slotId, portId, seId) {
|
||||
putString(FIELD_ICCID, iccid)
|
||||
putString(FIELD_CURRENT_NAME, currentName)
|
||||
}
|
||||
|
@ -105,7 +106,7 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
|
|||
ensureEuiccChannelManager()
|
||||
euiccChannelManagerService.waitForForegroundTask()
|
||||
val response = euiccChannelManagerService
|
||||
.launchProfileRenameTask(slotId, portId, iccid, newName).waitDone()
|
||||
.launchProfileRenameTask(slotId, portId, seId, iccid, newName).waitDone()
|
||||
|
||||
when (response) {
|
||||
is LocalProfileAssistant.ProfileNameTooLongException -> {
|
||||
|
|
|
@ -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
|
||||
|
@ -156,7 +157,9 @@ class UsbCcidReaderFragment : Fragment(), OpenEuiccContextMarker {
|
|||
R.id.child_container,
|
||||
appContainer.uiComponentFactory.createEuiccManagementFragment(
|
||||
slotId = EuiccChannelManager.USB_CHANNEL_ID,
|
||||
portId = 0
|
||||
portId = 0,
|
||||
// TODO: What if a USB card has multiple SEs?
|
||||
seId = EuiccChannel.SecureElementId.DEFAULT
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import androidx.core.view.updatePadding
|
|||
import androidx.fragment.app.Fragment
|
||||
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.ui.BaseEuiccAccessActivity
|
||||
import im.angry.openeuicc.util.*
|
||||
|
@ -27,7 +28,7 @@ import net.typeblog.lpac_jni.LocalProfileAssistant
|
|||
class DownloadWizardActivity : BaseEuiccAccessActivity() {
|
||||
data class DownloadWizardState(
|
||||
var currentStepFragmentClassName: String?,
|
||||
var selectedLogicalSlot: Int,
|
||||
var selectedSyntheticSlotId: Int,
|
||||
var smdp: String,
|
||||
var matchingId: String?,
|
||||
var confirmationCode: String?,
|
||||
|
@ -66,7 +67,7 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
|||
|
||||
state = DownloadWizardState(
|
||||
currentStepFragmentClassName = null,
|
||||
selectedLogicalSlot = intent.getIntExtra("selectedLogicalSlot", 0),
|
||||
selectedSyntheticSlotId = intent.getIntExtra("selectedLogicalSlot", 0),
|
||||
smdp = "",
|
||||
matchingId = null,
|
||||
confirmationCode = null,
|
||||
|
@ -151,7 +152,7 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
|||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putString("currentStepFragmentClassName", state.currentStepFragmentClassName)
|
||||
outState.putInt("selectedLogicalSlot", state.selectedLogicalSlot)
|
||||
outState.putInt("selectedLogicalSlot", state.selectedSyntheticSlotId)
|
||||
outState.putString("smdp", state.smdp)
|
||||
outState.putString("matchingId", state.matchingId)
|
||||
outState.putString("confirmationCode", state.confirmationCode)
|
||||
|
@ -167,16 +168,20 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
|||
"currentStepFragmentClassName",
|
||||
state.currentStepFragmentClassName
|
||||
)
|
||||
state.selectedLogicalSlot =
|
||||
savedInstanceState.getInt("selectedLogicalSlot", state.selectedLogicalSlot)
|
||||
state.selectedSyntheticSlotId =
|
||||
savedInstanceState.getInt("selectedSyntheticSlotId", state.selectedSyntheticSlotId)
|
||||
state.smdp = savedInstanceState.getString("smdp", state.smdp)
|
||||
state.matchingId = savedInstanceState.getString("matchingId", state.matchingId)
|
||||
state.imei = savedInstanceState.getString("imei", state.imei)
|
||||
state.downloadStarted =
|
||||
savedInstanceState.getBoolean("downloadStarted", state.downloadStarted)
|
||||
state.downloadTaskID = savedInstanceState.getLong("downloadTaskID", state.downloadTaskID)
|
||||
state.confirmationCode = savedInstanceState.getString("confirmationCode", state.confirmationCode)
|
||||
state.confirmationCodeRequired = savedInstanceState.getBoolean("confirmationCodeRequired", state.confirmationCodeRequired)
|
||||
state.confirmationCode =
|
||||
savedInstanceState.getString("confirmationCode", state.confirmationCode)
|
||||
state.confirmationCodeRequired = savedInstanceState.getBoolean(
|
||||
"confirmationCodeRequired",
|
||||
state.confirmationCodeRequired
|
||||
)
|
||||
}
|
||||
|
||||
private fun onPrevPressed() {
|
||||
|
@ -200,10 +205,13 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
|||
progressBar.isIndeterminate = true
|
||||
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
if (state.selectedLogicalSlot >= 0) {
|
||||
if (state.selectedSyntheticSlotId >= 0) {
|
||||
try {
|
||||
val (slotId, seId) = DownloadWizardSlotSelectFragment.decodeSyntheticSlotId(
|
||||
state.selectedSyntheticSlotId
|
||||
)
|
||||
// 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
|
||||
if (!channel.valid) throw EuiccChannelManager.EuiccChannelNotFoundException()
|
||||
}
|
||||
|
|
|
@ -153,7 +153,12 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep
|
|||
} else {
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -163,6 +168,7 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep
|
|||
val ret = euiccChannelManagerService.launchProfileDownloadTask(
|
||||
slotId,
|
||||
portId,
|
||||
seId,
|
||||
state.smdp,
|
||||
state.matchingId,
|
||||
state.confirmationCode,
|
||||
|
|
|
@ -14,8 +14,11 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
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.flow.asFlow
|
||||
import kotlinx.coroutines.flow.flatMapConcat
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -24,19 +27,28 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
|||
companion object {
|
||||
const val LOW_NVRAM_THRESHOLD =
|
||||
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(
|
||||
val logicalSlotId: Int,
|
||||
val isRemovable: Boolean,
|
||||
val hasMultiplePorts: Boolean,
|
||||
val hasMultipleSEs: Boolean,
|
||||
val portId: Int,
|
||||
val seId: EuiccChannel.SecureElementId,
|
||||
val eID: String,
|
||||
val freeSpace: Int,
|
||||
val imei: String,
|
||||
val enabledProfileName: 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
|
||||
|
||||
|
@ -85,7 +97,12 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
|||
recyclerView.adapter = adapter
|
||||
recyclerView.layoutManager =
|
||||
LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false)
|
||||
recyclerView.addItemDecoration(DividerItemDecoration(requireContext(), LinearLayoutManager.VERTICAL))
|
||||
recyclerView.addItemDecoration(
|
||||
DividerItemDecoration(
|
||||
requireContext(),
|
||||
LinearLayoutManager.VERTICAL
|
||||
)
|
||||
)
|
||||
return view
|
||||
}
|
||||
|
||||
|
@ -97,16 +114,21 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
|||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged", "MissingPermission")
|
||||
@OptIn(kotlinx.coroutines.FlowPreview::class)
|
||||
private suspend fun init() {
|
||||
ensureEuiccChannelManager()
|
||||
showProgressBar(-1)
|
||||
val slots = euiccChannelManager.flowAllOpenEuiccPorts().map { (slotId, portId) ->
|
||||
euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
|
||||
val slots = euiccChannelManager.flowAllOpenEuiccPorts().flatMapConcat { (slotId, portId) ->
|
||||
val ses = euiccChannelManager.flowEuiccSecureElements(slotId, portId).toList()
|
||||
ses.asFlow().map { seId ->
|
||||
euiccChannelManager.withEuiccChannel(slotId, portId, seId) { channel ->
|
||||
SlotInfo(
|
||||
channel.logicalSlotId,
|
||||
channel.port.card.isRemovable,
|
||||
channel.port.card.ports.size > 1,
|
||||
ses.size > 1,
|
||||
channel.portId,
|
||||
channel.seId,
|
||||
channel.lpa.eID,
|
||||
channel.lpa.euiccInfo2?.freeNvram ?: 0,
|
||||
try {
|
||||
|
@ -118,16 +140,17 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
|||
channel.intrinsicChannelName,
|
||||
)
|
||||
}
|
||||
}.toList().sortedBy { it.logicalSlotId }
|
||||
}
|
||||
}.toList().sortedBy { it.syntheticSlotId }
|
||||
adapter.slots = slots
|
||||
|
||||
// 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) {
|
||||
selectedIdx
|
||||
} else {
|
||||
if (slots.isNotEmpty()) {
|
||||
state.selectedLogicalSlot = slots[0].logicalSlotId
|
||||
state.selectedSyntheticSlotId = slots[0].syntheticSlotId
|
||||
}
|
||||
0
|
||||
}
|
||||
|
@ -167,7 +190,8 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
|||
adapter.notifyItemChanged(lastIdx)
|
||||
adapter.notifyItemChanged(curIdx)
|
||||
// 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
|
||||
}
|
||||
|
||||
|
@ -187,11 +211,17 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
|||
|
||||
title.text = if (item.logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
item.intrinsicChannelName ?: root.context.getString(R.string.channel_type_usb)
|
||||
} else if (item.hasMultipleSEs) {
|
||||
appContainer.customizableTextProvider.formatNonUsbChannelNameWithSeId(
|
||||
item.logicalSlotId,
|
||||
item.seId
|
||||
)
|
||||
} else {
|
||||
appContainer.customizableTextProvider.formatInternalChannelName(item.logicalSlotId)
|
||||
appContainer.customizableTextProvider.formatNonUsbChannelName(item.logicalSlotId)
|
||||
}
|
||||
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)
|
||||
checkBox.isChecked = adapter.currentSelectedIdx == idx
|
||||
}
|
||||
|
@ -205,7 +235,8 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
|||
get() = slots[currentSelectedIdx]
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package im.angry.openeuicc.util
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
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_PORT_ID = "portId"
|
||||
private const val FIELD_SE_ID = "seId"
|
||||
|
||||
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"
|
||||
// in the definition of an interface, so the only way is to limit where the extension functions
|
||||
// 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 =
|
||||
clazz.getDeclaredConstructor().newInstance().apply {
|
||||
arguments = Bundle()
|
||||
arguments!!.putInt(FIELD_SLOT_ID, slotId)
|
||||
arguments!!.putInt(FIELD_PORT_ID, portId)
|
||||
arguments!!.putParcelable(FIELD_SE_ID, seId)
|
||||
arguments!!.addArguments()
|
||||
}
|
||||
|
||||
|
@ -35,6 +44,18 @@ val <T> T.slotId: Int
|
|||
val <T> T.portId: Int
|
||||
where T : Fragment, T : EuiccChannelFragmentMarker
|
||||
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
|
||||
where T : Fragment, T : EuiccChannelFragmentMarker
|
||||
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
|
||||
where T : Fragment, T : EuiccChannelFragmentMarker {
|
||||
ensureEuiccChannelManager()
|
||||
return euiccChannelManager.withEuiccChannel(slotId, portId, fn)
|
||||
return euiccChannelManager.withEuiccChannel(
|
||||
slotId,
|
||||
portId,
|
||||
seId,
|
||||
fn
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun <T> T.ensureEuiccChannelManager() where T : Fragment, T : OpenEuiccContextMarker =
|
||||
|
|
|
@ -79,9 +79,10 @@ fun LocalProfileAssistant.disableActiveProfileKeepIccId(refresh: Boolean): Strin
|
|||
suspend inline fun EuiccChannelManager.beginTrackedOperation(
|
||||
slotId: Int,
|
||||
portId: Int,
|
||||
seId: EuiccChannel.SecureElementId,
|
||||
op: () -> Boolean
|
||||
) {
|
||||
val latestSeq = withEuiccChannel(slotId, portId) { channel ->
|
||||
val latestSeq = withEuiccChannel(slotId, portId, seId) { channel ->
|
||||
channel.lpa.notifications.firstOrNull()?.seqNumber
|
||||
?: 0
|
||||
}
|
||||
|
@ -91,7 +92,7 @@ suspend inline fun EuiccChannelManager.beginTrackedOperation(
|
|||
try {
|
||||
// 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()
|
||||
withEuiccChannel(slotId, portId) { channel ->
|
||||
withEuiccChannel(slotId, portId, seId) { channel ->
|
||||
channel.lpa.notifications.filter { it.seqNumber > latestSeq }.forEach {
|
||||
Log.d(TAG, "Handling notification $it")
|
||||
channel.lpa.handleNotification(it.seqNumber)
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
<string name="profile_no_enabled_profile">Unknown</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_omapi" translatable="false">OpenMobile API (OMAPI)</string>
|
||||
|
||||
|
|
|
@ -2,9 +2,16 @@ package im.angry.openeuicc.di
|
|||
|
||||
import android.content.Context
|
||||
import im.angry.easyeuicc.R
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
|
||||
class UnprivilegedCustomizableTextProvider(private val context: Context) :
|
||||
DefaultCustomizableTextProvider(context) {
|
||||
override fun formatInternalChannelName(logicalSlotId: Int): String =
|
||||
override fun formatNonUsbChannelName(logicalSlotId: Int): String =
|
||||
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)
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
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.QuickCompatibilityFragment
|
||||
import im.angry.openeuicc.ui.UnprivilegedEuiccManagementFragment
|
||||
|
@ -8,8 +9,12 @@ import im.angry.openeuicc.ui.UnprivilegedNoEuiccPlaceholderFragment
|
|||
import im.angry.openeuicc.ui.UnprivilegedSettingsFragment
|
||||
|
||||
open class UnprivilegedUiComponentFactory : DefaultUiComponentFactory() {
|
||||
override fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment =
|
||||
UnprivilegedEuiccManagementFragment.newInstance(slotId, portId)
|
||||
override fun createEuiccManagementFragment(
|
||||
slotId: Int,
|
||||
portId: Int,
|
||||
seId: EuiccChannel.SecureElementId
|
||||
): EuiccManagementFragment =
|
||||
UnprivilegedEuiccManagementFragment.newInstance(slotId, portId, seId)
|
||||
|
||||
override fun createNoEuiccPlaceholderFragment(): Fragment =
|
||||
UnprivilegedNoEuiccPlaceholderFragment()
|
||||
|
|
|
@ -148,7 +148,7 @@ open class QuickCompatibilityFragment : Fragment(), UnprivilegedEuiccContextMark
|
|||
if (omapiSlots.isEmpty()) {
|
||||
return CompatibilityResult(Compatibility.NOT_COMPATIBLE)
|
||||
}
|
||||
val formatChannelName = appContainer.customizableTextProvider::formatInternalChannelName
|
||||
val formatChannelName = appContainer.customizableTextProvider::formatNonUsbChannelName
|
||||
return CompatibilityResult(
|
||||
Compatibility.COMPATIBLE,
|
||||
slotsOmapi = omapiSlots.map(formatChannelName),
|
||||
|
|
|
@ -7,6 +7,7 @@ import android.view.MenuInflater
|
|||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import im.angry.easyeuicc.R
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
import im.angry.openeuicc.util.SIMToolkit
|
||||
import im.angry.openeuicc.util.newInstanceEuicc
|
||||
import im.angry.openeuicc.util.slotId
|
||||
|
@ -16,8 +17,12 @@ class UnprivilegedEuiccManagementFragment : EuiccManagementFragment() {
|
|||
companion object {
|
||||
const val TAG = "UnprivilegedEuiccManagementFragment"
|
||||
|
||||
fun newInstance(slotId: Int, portId: Int): EuiccManagementFragment =
|
||||
newInstanceEuicc(UnprivilegedEuiccManagementFragment::class.java, slotId, portId)
|
||||
fun newInstance(
|
||||
slotId: Int,
|
||||
portId: Int,
|
||||
seId: EuiccChannel.SecureElementId
|
||||
): EuiccManagementFragment =
|
||||
newInstanceEuicc(UnprivilegedEuiccManagementFragment::class.java, slotId, portId, seId)
|
||||
}
|
||||
|
||||
private val stk by lazy {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<resources>
|
||||
<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_se" translatable="false">SIM %d, SE %d</string>
|
||||
<string name="compatibility_check">Compatibility Check</string>
|
||||
<string name="open_sim_toolkit">Open SIM Toolkit</string>
|
||||
|
||||
|
|
|
@ -15,13 +15,14 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto
|
|||
@Suppress("NAME_SHADOWING")
|
||||
override suspend fun tryOpenEuiccChannel(
|
||||
port: UiccPortInfoCompat,
|
||||
isdrAid: ByteArray
|
||||
isdrAid: ByteArray,
|
||||
seId: EuiccChannel.SecureElementId,
|
||||
): EuiccChannel? {
|
||||
val port = port as RealUiccPortInfoCompat
|
||||
if (port.card.isRemovable) {
|
||||
// Attempt unprivileged (OMAPI) before TelephonyManager
|
||||
// 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()) {
|
||||
|
@ -40,6 +41,7 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto
|
|||
context.preferenceRepository.verboseLoggingFlow
|
||||
),
|
||||
isdrAid,
|
||||
seId,
|
||||
context.preferenceRepository.verboseLoggingFlow,
|
||||
context.preferenceRepository.ignoreTLSCertificateFlow,
|
||||
context.preferenceRepository.es10xMssFlow,
|
||||
|
@ -53,6 +55,6 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto
|
|||
}
|
||||
}
|
||||
|
||||
return super.tryOpenEuiccChannel(port, isdrAid)
|
||||
return super.tryOpenEuiccChannel(port, isdrAid, seId)
|
||||
}
|
||||
}
|
|
@ -1,13 +1,18 @@
|
|||
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.PrivilegedEuiccManagementFragment
|
||||
import im.angry.openeuicc.ui.PrivilegedSettingsFragment
|
||||
|
||||
class PrivilegedUiComponentFactory : DefaultUiComponentFactory() {
|
||||
override fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment =
|
||||
PrivilegedEuiccManagementFragment.newInstance(slotId, portId)
|
||||
override fun createEuiccManagementFragment(
|
||||
slotId: Int,
|
||||
portId: Int,
|
||||
seId: EuiccChannel.SecureElementId
|
||||
): EuiccManagementFragment =
|
||||
PrivilegedEuiccManagementFragment.newInstance(slotId, portId, seId)
|
||||
|
||||
override fun createSettingsFragment(): Fragment =
|
||||
PrivilegedSettingsFragment()
|
||||
|
|
|
@ -8,6 +8,7 @@ import android.telephony.UiccSlotMapping
|
|||
import android.telephony.euicc.DownloadableSubscription
|
||||
import android.telephony.euicc.EuiccInfo
|
||||
import android.util.Log
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
import net.typeblog.lpac_jni.LocalProfileInfo
|
||||
import im.angry.openeuicc.core.EuiccChannelManager
|
||||
import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
|
||||
|
@ -165,7 +166,8 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
|
|||
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")
|
||||
if (slotId == -1 || shouldIgnoreSlot(slotId)) {
|
||||
Log.i(TAG, "ignoring slot $slotId")
|
||||
|
@ -250,7 +252,12 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
|
|||
if (enabledAnywhere) return@withEuiccChannelManager RESULT_FIRST_USER
|
||||
|
||||
euiccChannelManagerService.waitForForegroundTask()
|
||||
val success = euiccChannelManagerService.launchProfileDeleteTask(slotId, ports[0], iccid)
|
||||
val success = euiccChannelManagerService.launchProfileDeleteTask(
|
||||
slotId,
|
||||
ports[0],
|
||||
EuiccChannel.SecureElementId.DEFAULT,
|
||||
iccid
|
||||
)
|
||||
.waitDone() == null
|
||||
|
||||
return@withEuiccChannelManager if (success) {
|
||||
|
@ -275,7 +282,10 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
|
|||
iccid: String?,
|
||||
forceDeactivateSim: Boolean
|
||||
): 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
|
||||
|
||||
try {
|
||||
|
@ -357,6 +367,7 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
|
|||
val res = euiccChannelManagerService.launchProfileSwitchTask(
|
||||
foundSlotId,
|
||||
foundPortId,
|
||||
EuiccChannel.SecureElementId.DEFAULT,
|
||||
foundIccid,
|
||||
enable,
|
||||
30 * 1000
|
||||
|
@ -386,7 +397,13 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
|
|||
|
||||
euiccChannelManagerService.waitForForegroundTask()
|
||||
val success =
|
||||
(euiccChannelManagerService.launchProfileRenameTask(slotId, port, iccid, nickname!!)
|
||||
(euiccChannelManagerService.launchProfileRenameTask(
|
||||
slotId,
|
||||
port,
|
||||
EuiccChannel.SecureElementId.DEFAULT,
|
||||
iccid,
|
||||
nickname!!
|
||||
)
|
||||
.waitDone()) == null
|
||||
|
||||
euiccChannelManager.withEuiccChannel(slotId, port) { channel ->
|
||||
|
|
|
@ -5,13 +5,18 @@ import android.view.ViewGroup
|
|||
import android.widget.Button
|
||||
import android.widget.PopupMenu
|
||||
import im.angry.openeuicc.R
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
import im.angry.openeuicc.util.*
|
||||
import net.typeblog.lpac_jni.LocalProfileInfo
|
||||
|
||||
class PrivilegedEuiccManagementFragment : EuiccManagementFragment() {
|
||||
companion object {
|
||||
fun newInstance(slotId: Int, portId: Int): EuiccManagementFragment =
|
||||
newInstanceEuicc(PrivilegedEuiccManagementFragment::class.java, slotId, portId)
|
||||
fun newInstance(
|
||||
slotId: Int,
|
||||
portId: Int,
|
||||
seId: EuiccChannel.SecureElementId
|
||||
): EuiccManagementFragment =
|
||||
newInstanceEuicc(PrivilegedEuiccManagementFragment::class.java, slotId, portId, seId)
|
||||
}
|
||||
|
||||
private var isMEP = false
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue