diff --git a/.gitmodules b/.gitmodules index 863f185..f888959 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "libs/lpac-jni/src/main/jni/lpac"] path = libs/lpac-jni/src/main/jni/lpac - url = https://github.com/estkme-group/lpac.git + url = https://github.com/estkme/lpac diff --git a/app-common/src/main/AndroidManifest.xml b/app-common/src/main/AndroidManifest.xml index 44c82c0..b0324dc 100644 --- a/app-common/src/main/AndroidManifest.xml +++ b/app-common/src/main/AndroidManifest.xml @@ -45,9 +45,8 @@ - - - + + diff --git a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt index 87a0eea..b975313 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt @@ -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? { if (port.portIndex != 0) { Log.w( @@ -37,7 +38,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha ) try { return EuiccChannelImpl( - context.getString(R.string.channel_type_omapi), + context.getString(R.string.omapi), port, intrinsicChannelName = null, OmapiApduInterface( @@ -46,6 +47,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha context.preferenceRepository.verboseLoggingFlow ), isdrAid, + seId, context.preferenceRepository.verboseLoggingFlow, context.preferenceRepository.ignoreTLSCertificateFlow, ).also { @@ -65,17 +67,19 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha override fun tryOpenUsbEuiccChannel( ccidCtx: UsbCcidContext, - isdrAid: ByteArray + isdrAid: ByteArray, + seId: EuiccChannel.SecureElementId ): EuiccChannel? { try { return EuiccChannelImpl( - context.getString(R.string.channel_type_usb), + context.getString(R.string.usb), FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)), intrinsicChannelName = ccidCtx.productName, UsbApduInterface( ccidCtx ), isdrAid, + seId, context.preferenceRepository.verboseLoggingFlow, context.preferenceRepository.ignoreTLSCertificateFlow, ) diff --git a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt index 6b336cd..2d0493b 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt @@ -32,7 +32,7 @@ open class DefaultEuiccChannelManager( private val channelCache = mutableListOf() - private var usbChannel: EuiccChannel? = null + private var usbChannels = mutableListOf() private val lock = Mutex() @@ -51,15 +51,20 @@ open class DefaultEuiccChannelManager( protected open val uiccCards: Collection get() = (0.. EuiccChannel?): EuiccChannel? { + private suspend inline fun tryOpenChannelWithKnownAids(openFn: (ByteArray, EuiccChannel.SecureElementId) -> EuiccChannel?): List { 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? { 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 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 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 = 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 = 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() } diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt index b20932f..19dd682 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt @@ -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,56 @@ 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 + */ + fun createFromInt(id: Int): SecureElementId = + SecureElementId(id) + + @Suppress("unused") + @JvmField + val CREATOR = object : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): SecureElementId = + createFromInt(parcel.readInt()) + + override fun newArray(size: Int): Array = 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 diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelFactory.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelFactory.kt index ba587a6..a8051af 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelFactory.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelFactory.kt @@ -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? /** diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt index 2a33c20..491f6d2 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt @@ -14,6 +14,7 @@ class EuiccChannelImpl( override val intrinsicChannelName: String?, override val apduInterface: ApduInterface, override val isdrAid: ByteArray, + override val seId: EuiccChannel.SecureElementId, verboseLoggingFlow: Flow, ignoreTLSCertificateFlow: Flow ) : EuiccChannel { diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt index 17f3130..eb8e87c 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt @@ -37,6 +37,14 @@ interface EuiccChannelManager { */ fun flowAllOpenEuiccPorts(): Flow> + /** + * 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 + /** * 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 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 withEuiccChannel( logicalSlotId: Int, + seId: EuiccChannel.SecureElementId = EuiccChannel.SecureElementId.DEFAULT, fn: suspend (EuiccChannel) -> R ): R diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt index 361a943..a36c1b6 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt @@ -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) } diff --git a/app-common/src/main/java/im/angry/openeuicc/di/DefaultCustomizableTextProvider.kt b/app-common/src/main/java/im/angry/openeuicc/di/DefaultCustomizableTextProvider.kt index 76227fd..b493611 100644 --- a/app-common/src/main/java/im/angry/openeuicc/di/DefaultCustomizableTextProvider.kt +++ b/app-common/src/main/java/im/angry/openeuicc/di/DefaultCustomizableTextProvider.kt @@ -8,7 +8,7 @@ open class DefaultCustomizableTextProvider(private val context: Context) : Custo get() = context.getString(R.string.no_euicc) override val profileSwitchingTimeoutMessage: String - get() = context.getString(R.string.profile_switch_timeout) + get() = context.getString(R.string.enable_disable_timeout) override fun formatInternalChannelName(logicalSlotId: Int): String = context.getString(R.string.channel_name_format, logicalSlotId) diff --git a/app-common/src/main/java/im/angry/openeuicc/di/DefaultUiComponentFactory.kt b/app-common/src/main/java/im/angry/openeuicc/di/DefaultUiComponentFactory.kt index 52a501a..d268da8 100644 --- a/app-common/src/main/java/im/angry/openeuicc/di/DefaultUiComponentFactory.kt +++ b/app-common/src/main/java/im/angry/openeuicc/di/DefaultUiComponentFactory.kt @@ -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() diff --git a/app-common/src/main/java/im/angry/openeuicc/di/UiComponentFactory.kt b/app-common/src/main/java/im/angry/openeuicc/di/UiComponentFactory.kt index 2c3c72b..6a4d13f 100644 --- a/app-common/src/main/java/im/angry/openeuicc/di/UiComponentFactory.kt +++ b/app-common/src/main/java/im/angry/openeuicc/di/UiComponentFactory.kt @@ -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 } \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt index bfbcbd8..bd869ca 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt @@ -36,13 +36,14 @@ private val RE_SAS = Regex( class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { companion object { - private val YES_NO = Pair(R.string.euicc_info_yes, R.string.euicc_info_no) + private val YES_NO = Pair(R.string.yes, R.string.no) } private lateinit var swipeRefresh: SwipeRefreshLayout private lateinit var infoList: RecyclerView private var logicalSlotId: Int = -1 + private var seId: EuiccChannel.SecureElementId = EuiccChannel.SecureElementId.DEFAULT data class Item( @StringRes @@ -67,9 +68,15 @@ 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) + getString(R.string.usb) } else { appContainer.customizableTextProvider.formatInternalChannelName(logicalSlotId) } @@ -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,19 +114,38 @@ 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)) } } channel.lpa.euiccInfo2?.let { info -> add(Item(R.string.euicc_info_sgp22_version, info.sgp22Version.toString())) add(Item(R.string.euicc_info_firmware_version, info.euiccFirmwareVersion.toString())) - add(Item(R.string.euicc_info_gp_version, info.globalPlatformVersion.toString())) + add(Item(R.string.euicc_info_globalplatform_version, info.globalPlatformVersion.toString())) add(Item(R.string.euicc_info_pp_version, info.ppVersion.toString())) info.sasAccreditationNumber.trim().takeIf(RE_SAS::matches) ?.let { add(Item(R.string.euicc_info_sas_accreditation_number, it.uppercase())) } @@ -131,14 +157,14 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { // FS.27 v2.0, Security Guidelines for UICC Profiles (Page 25 of 27, 2024-01-30) // https://www.gsma.com/solutions-and-impact/technologies/security/wp-content/uploads/2024/01/FS.27-Security-Guidelines-for-UICC-Credentials-v2.0-FINAL-23-July.pdf#page=25 val resId = when { - signers.isEmpty() -> R.string.euicc_info_unknown // the case is not mp, but it's is not common + signers.isEmpty() -> R.string.unknown // the case is not mp, but it's is not common PKID_GSMA_LIVE_CI.any(signers::contains) -> R.string.euicc_info_ci_gsma_live PKID_GSMA_TEST_CI.any(signers::contains) -> R.string.euicc_info_ci_gsma_test else -> R.string.euicc_info_ci_unknown } add(Item(R.string.euicc_info_ci_type, getString(resId))) } - val atr = channel.atr?.encodeHex() ?: getString(R.string.euicc_info_unavailable) + val atr = channel.atr?.encodeHex() ?: getString(R.string.information_unavailable) add(Item(R.string.euicc_info_atr, atr, copiedToastResId = R.string.toast_atr_copied)) } @@ -171,7 +197,7 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { fun bind(item: Item) { copiedToastResId = item.copiedToastResId title.setText(item.titleResId) - content.text = item.content ?: getString(R.string.euicc_info_unknown) + content.text = item.content ?: getString(R.string.unknown) } } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt index 016e96f..4be3ebc 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt @@ -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 } @@ -253,7 +260,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, if (!isUsb) { withContext(Dispatchers.Main) { AlertDialog.Builder(requireContext()).apply { - setMessage(R.string.profile_switch_did_not_refresh) + setMessage(R.string.switch_did_not_refresh) setPositiveButton(android.R.string.ok) { dialog, _ -> dialog.dismiss() requireActivity().finish() @@ -294,7 +301,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 @@ -321,7 +331,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, } } - inner class FooterViewHolder: ViewHolder(FrameLayout(requireContext())) { + inner class FooterViewHolder : ViewHolder(FrameLayout(requireContext())) { init { itemView.layoutParams = ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, @@ -347,7 +357,6 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, private val profileClassLabel: TextView = root.requireViewById(R.id.profile_class_label) private val profileClass: TextView = root.requireViewById(R.id.profile_class) private val profileMenu: ImageButton = root.requireViewById(R.id.profile_menu) - private val profileSeqNumber: TextView = root.requireViewById(R.id.profile_sequence_number) init { iccid.setOnClickListener { @@ -367,9 +376,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, true } - profileMenu.setOnClickListener { - showOptionsMenu() - } + profileMenu.setOnClickListener { showOptionsMenu() } } private lateinit var profile: LocalProfileInfo @@ -380,9 +387,9 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, state.setText( if (profile.isEnabled) { - R.string.profile_state_enabled + R.string.enabled } else { - R.string.profile_state_disabled + R.string.disabled } ) provider.text = profile.providerName @@ -399,13 +406,6 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, iccid.transformationMethod = PasswordTransformationMethod.getInstance() } - fun setProfileSequenceNumber(index: Int) { - profileSeqNumber.text = root.context.getString( - R.string.profile_sequence_number_format, - index, - ) - } - private fun showOptionsMenu() { // Prevent users from doing multiple things at once if (invalid || swipeRefresh.isRefreshing) return @@ -423,20 +423,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 +464,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 +479,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 } @@ -471,8 +491,8 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, when (holder) { is ProfileViewHolder -> { holder.setProfile(profiles[position]) - holder.setProfileSequenceNumber(position + 1) } + is FooterViewHolder -> { holder.attach(footerViews[position - profiles.size]) } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccMemoryResetFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccMemoryResetFragment.kt index 086a849..79a8f0a 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccMemoryResetFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccMemoryResetFragment.kt @@ -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 @@ -29,8 +30,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) } } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt index b42f4cf..93f51dd 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt @@ -23,6 +23,7 @@ import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator import im.angry.openeuicc.common.R +import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.util.* import kotlinx.coroutines.Dispatchers @@ -112,10 +113,12 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { startActivity(Intent(this, SettingsActivity::class.java)) true } + R.id.reload -> { refresh() true } + else -> super.onOptionsItemSelected(item) } @@ -154,27 +157,33 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { euiccChannelManager.flowInternalEuiccPorts().onEach { (slotId, portId) -> Log.d(TAG, "slot $slotId port $portId") - euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> - if (preferenceRepository.verboseLoggingFlow.first()) { - Log.d(TAG, channel.lpa.eID) - } - // Request the system to refresh the list of profiles every time we start - // Note that this is currently supposed to be no-op when unprivileged, - // but it could change in the future - euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId) + euiccChannelManager.flowEuiccSecureElements(slotId, portId).onEach { seId -> + euiccChannelManager.withEuiccChannel(slotId, portId, seId) { channel -> + if (preferenceRepository.verboseLoggingFlow.first()) { + Log.d(TAG, channel.lpa.eID) + } + // Request the system to refresh the list of profiles every time we start + // Note that this is currently supposed to be no-op when unprivileged, + // but it could change in the future + euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId) - val channelName = - appContainer.customizableTextProvider.formatInternalChannelName(channel.logicalSlotId) - newPages.add(Page(channel.logicalSlotId, channelName) { - appContainer.uiComponentFactory.createEuiccManagementFragment(slotId, portId) - }) - } + val channelName = + appContainer.customizableTextProvider.formatInternalChannelName(channel.logicalSlotId) + newPages.add(Page(channel.logicalSlotId, channelName) { + 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 usbDevice?.let { - val productName = it.productName ?: getString(R.string.channel_type_usb) + val productName = it.productName ?: getString(R.string.usb) newPages.add(Page(EuiccChannelManager.USB_CHANNEL_ID, productName) { UsbCcidReaderFragment() }) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt index 07d5f13..594fdd1 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt @@ -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 @@ -27,12 +29,13 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import net.typeblog.lpac_jni.LocalProfileNotification -class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker { +class NotificationsActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { private lateinit var swipeRefresh: SwipeRefreshLayout private lateinit var notificationList: RecyclerView private val notificationAdapter = NotificationAdapter() private var logicalSlotId = -1 + private var seId = EuiccChannel.SecureElementId.DEFAULT override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() @@ -51,16 +54,27 @@ 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) + getString(R.string.usb) } else { appContainer.customizableTextProvider.formatInternalChannelName(logicalSlotId) } @@ -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) } @@ -114,20 +130,20 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker { } private fun refresh() { - launchTask { - notificationAdapter.notifications = - euiccChannelManager.withEuiccChannel(logicalSlotId) { channel -> - val nameMap = buildMap { - for (profile in channel.lpa.profiles) { - put(profile.iccid, profile.displayName) - } - } + launchTask { + notificationAdapter.notifications = + euiccChannelManager.withEuiccChannel(logicalSlotId) { channel -> + val nameMap = buildMap { + for (profile in channel.lpa.profiles) { + put(profile.iccid, profile.displayName) + } + } - channel.lpa.notifications.map { - LocalProfileNotificationWrapper(it, nameMap[it.iccid] ?: "???") - } - } - } + channel.lpa.notifications.map { + LocalProfileNotificationWrapper(it, nameMap[it.iccid] ?: "???") + } + } + } } data class LocalProfileNotificationWrapper( @@ -136,7 +152,7 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker { ) @SuppressLint("ClickableViewAccessibility") - inner class NotificationViewHolder(private val root: View): + inner class NotificationViewHolder(private val root: View) : RecyclerView.ViewHolder(root), View.OnCreateContextMenuListener, OnMenuItemClickListener { private val address: TextView = root.requireViewById(R.id.notification_address) private val sequenceNumber: TextView = @@ -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,11 +246,12 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker { } true } + else -> false } } - inner class NotificationAdapter: RecyclerView.Adapter() { + inner class NotificationAdapter : RecyclerView.Adapter() { var notifications: List = listOf() @SuppressLint("NotifyDataSetChanged") set(value) { diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt index 38d1bc6..d5cf496 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt @@ -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) } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt index 281e625..e5f8251 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt @@ -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) } @@ -65,7 +66,7 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment super.onViewCreated(view, savedInstanceState) profileRenameNewName.editText!!.setText(currentName) toolbar.apply { - setTitle(R.string.profile_rename) + setTitle(R.string.rename) setNavigationOnClickListener { if (!renaming) dismiss() } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/UsbCcidReaderFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/UsbCcidReaderFragment.kt index 7a52ca0..a14e47d 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/UsbCcidReaderFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/UsbCcidReaderFragment.kt @@ -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 ) ) } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt index 6574645..9e312d4 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt @@ -123,8 +123,8 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { // If we get an LPA string from deep-link intents, extract from there. // Note that `onRestoreInstanceState` could override this with user input, // but that _is_ the desired behavior. - val uri = intent.data ?: return - if (uri.scheme.contentEquals("lpa", ignoreCase = true)) { + val uri = intent.data + if (uri?.scheme == "lpa") { val parsed = LPAString.parse(uri.schemeSpecificPart) state.smdp = parsed.address state.matchingId = parsed.matchingId diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt index 3841868..e282196 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt @@ -8,7 +8,6 @@ import android.view.ViewGroup import android.widget.TextView import im.angry.openeuicc.common.R import im.angry.openeuicc.util.* -import org.json.JSONObject import java.util.Date class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardStepFragment() { @@ -87,10 +86,9 @@ class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardS ret.appendLine() val str = resp.data.decodeToString(throwOnInvalidSequence = false) - ret.appendLine( if (str.startsWith('{')) { - JSONObject(str).toString(2) + str.prettyPrintJson() } else { str } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt index 8097058..28bc9f0 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt @@ -19,6 +19,7 @@ import im.angry.openeuicc.util.* import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch +import net.typeblog.lpac_jni.LocalProfileInfo class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardStepFragment() { companion object { @@ -186,12 +187,12 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt } 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.usb) } else { appContainer.customizableTextProvider.formatInternalChannelName(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.unknown) freeSpace.text = formatFreeSpace(item.freeSpace) checkBox.isChecked = adapter.currentSelectedIdx == idx } diff --git a/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt index b44bef8..d2d8c4b 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt @@ -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 newInstanceEuicc(clazz: Class, slotId: Int, portId: Int, addArguments: BundleSetter = {}): T +fun newInstanceEuicc( + clazz: Class, + 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.slotId: Int val T.portId: Int where T : Fragment, T : EuiccChannelFragmentMarker get() = requireArguments().getInt(FIELD_PORT_ID) +val 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.isUsb: Boolean where T : Fragment, T : EuiccChannelFragmentMarker get() = slotId == EuiccChannelManager.USB_CHANNEL_ID @@ -54,7 +75,12 @@ val T.euiccChannelManagerService: EuiccChannelManagerService suspend fun 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.ensureEuiccChannelManager() where T : Fragment, T : OpenEuiccContextMarker = diff --git a/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt index 464aeee..5f4aec4 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt @@ -50,12 +50,9 @@ internal object PreferenceConstants { # eUICC standard $EUICC_DEFAULT_ISDR_AID - # ESTKme AUX (deprecated, use SE0 instead) + # eSTK.me A06573746B6D65FFFFFFFF4953442D52 - # ESTKme SE0 - A06573746B6D65FFFF4953442D522030 - # eSIM.me A0000005591010000000008900000300 diff --git a/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt index 57d150b..079853e 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt @@ -41,3 +41,73 @@ fun parseIsdrAidList(s: String): List = .filter(String::isNotEmpty) .mapNotNull { runCatching(it::decodeHex).getOrNull() } .ifEmpty { listOf(EUICC_DEFAULT_ISDR_AID.decodeHex()) } + +fun String.prettyPrintJson(): String { + val ret = StringBuilder() + var inQuotes = false + var escaped = false + val indentSymbolStack = ArrayDeque() + + val addNewLine = { + ret.append('\n') + repeat(indentSymbolStack.size) { + ret.append('\t') + } + } + + var lastChar = ' ' + + for (c in this) { + when { + !inQuotes && (c == '{' || c == '[') -> { + ret.append(c) + indentSymbolStack.addLast(c) + addNewLine() + } + + !inQuotes && (c == '}' || c == ']') -> { + indentSymbolStack.removeLast() + if (lastChar != ',') { + addNewLine() + } + ret.append(c) + } + + !inQuotes && c == ',' -> { + ret.append(c) + addNewLine() + } + + !inQuotes && c == ':' -> { + ret.append(c) + ret.append(' ') + } + + inQuotes && c == '\\' -> { + ret.append(c) + escaped = true + continue + } + + !escaped && c == '"' -> { + ret.append(c) + inQuotes = !inQuotes + } + + !inQuotes && c == ' ' -> { + // Do nothing -- we ignore spaces outside of quotes by default + // This is to ensure predictable formatting + } + + else -> ret.append(c) + } + + if (escaped) { + escaped = false + } + + lastChar = c + } + + return ret.toString() +} \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt index c7c859d..a73d7fe 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt @@ -102,8 +102,8 @@ fun T.setupLogSaving( AlertDialog.Builder(context).apply { setMessage(R.string.logs_saved_message) - setNegativeButton(android.R.string.cancel) { _, _ -> } - setPositiveButton(android.R.string.ok) { _, _ -> + setNegativeButton(R.string.no) { _, _ -> } + setPositiveButton(R.string.yes) { _, _ -> val intent = Intent(Intent.ACTION_SEND).apply { type = "text/plain" clipData = ClipData.newUri(context.contentResolver, lastFileName, uri) diff --git a/app-common/src/main/res/layout/euicc_profile.xml b/app-common/src/main/res/layout/euicc_profile.xml index 021c53b..58d55ab 100644 --- a/app-common/src/main/res/layout/euicc_profile.xml +++ b/app-common/src/main/res/layout/euicc_profile.xml @@ -54,7 +54,7 @@ - - diff --git a/app-common/src/main/res/layout/fragment_euicc.xml b/app-common/src/main/res/layout/fragment_euicc.xml index c5fde7b..4ae7523 100644 --- a/app-common/src/main/res/layout/fragment_euicc.xml +++ b/app-common/src/main/res/layout/fragment_euicc.xml @@ -27,7 +27,6 @@ android:layout_height="wrap_content" android:layout_marginEnd="16dp" android:layout_marginBottom="16dp" - android:contentDescription="@string/profile_download" android:src="@drawable/ic_add" app:layout_constraintRight_toRightOf="parent" app:layout_constraintBottom_toBottomOf="parent"/> diff --git a/app-common/src/main/res/menu/activity_isdr_aid_list.xml b/app-common/src/main/res/menu/activity_isdr_aid_list.xml index 99492d6..32f178a 100644 --- a/app-common/src/main/res/menu/activity_isdr_aid_list.xml +++ b/app-common/src/main/res/menu/activity_isdr_aid_list.xml @@ -9,7 +9,7 @@ \ No newline at end of file diff --git a/app-common/src/main/res/menu/activity_main.xml b/app-common/src/main/res/menu/activity_main.xml index c15663f..0e00292 100644 --- a/app-common/src/main/res/menu/activity_main.xml +++ b/app-common/src/main/res/menu/activity_main.xml @@ -3,7 +3,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> diff --git a/app-common/src/main/res/menu/activity_notifications.xml b/app-common/src/main/res/menu/activity_notifications.xml index b80e06e..87f96a6 100644 --- a/app-common/src/main/res/menu/activity_notifications.xml +++ b/app-common/src/main/res/menu/activity_notifications.xml @@ -4,6 +4,6 @@ \ No newline at end of file diff --git a/app-common/src/main/res/menu/fragment_profile_rename.xml b/app-common/src/main/res/menu/fragment_profile_rename.xml index f55c56c..bde850f 100644 --- a/app-common/src/main/res/menu/fragment_profile_rename.xml +++ b/app-common/src/main/res/menu/fragment_profile_rename.xml @@ -4,6 +4,6 @@ \ No newline at end of file diff --git a/app-common/src/main/res/menu/profile_options.xml b/app-common/src/main/res/menu/profile_options.xml index 60722d6..6add53d 100644 --- a/app-common/src/main/res/menu/profile_options.xml +++ b/app-common/src/main/res/menu/profile_options.xml @@ -2,18 +2,18 @@ + android:title="@string/enable"/> + android:title="@string/disable"/> + android:title="@string/rename"/> + android:title="@string/delete"/> \ No newline at end of file diff --git a/app-common/src/main/res/values-ja/strings.xml b/app-common/src/main/res/values-ja/strings.xml index 946625f..d51e2c7 100644 --- a/app-common/src/main/res/values-ja/strings.xml +++ b/app-common/src/main/res/values-ja/strings.xml @@ -2,24 +2,24 @@ このアプリでアクセスできるリムーバブル eUICC カードがデバイス上で検出されていません。互換性のあるカード挿入または USB リーダーを接続してください。 この eSIM にはプロファイルがありません。 - 不明 - 情報がありません - ヘルプ - スロットを再読み込み + 不明 + 情報がありません + ヘルプ + スロットを再読み込み 論理スロット %d - 有効済み - 無効済み - プロバイダー: + 有効済み + 無効済み + プロバイダー: クラス: テスト中 プロビジョニング 稼働中 - 有効化 - 無効化 - 削除 - 名前を変更 - eSIM チップがプロファイルの切り替えの待機中にタイムアウトしました。これはデバイスのモデムファームウェアのバグの可能性があります。機内モードに切り替えるかアプリを再起動、デバイスを再起動してください。 - 操作は成功しましたが、デバイスのモデムが更新を拒否しました。新しいプロファイルを使用するには機内モードに切り替えるか、再起動する必要があります。 + 有効化 + 無効化 + 削除 + 名前を変更 + eSIM チップがプロファイルの切り替えの待機中にタイムアウトしました。これはデバイスのモデムファームウェアのバグの可能性があります。機内モードに切り替えるかアプリを再起動、デバイスを再起動してください。 + 操作は成功しましたが、デバイスのモデムが更新を拒否しました。新しいプロファイルを使用するには機内モードに切り替えるか、再起動する必要があります。 新しい eSIM プロファイルに切り替えることができません。 確認文字列が一致しません ICCID をクリップボードにコピーしました @@ -110,7 +110,7 @@ 製品ファームウェアバージョン SGP.22 バージョン eUICC OS バージョン - グローバルプラットフォームのバージョン + グローバルプラットフォームのバージョン SAS 認定番号 保護されたプロファイルのバージョン NVRAM の空き容量 (eSIM プロファイルストレージ) @@ -118,8 +118,8 @@ GSMA ライブ CI GSMA テスト CI 不明な eSIM CI - はい - いいえ + はい + いいえ 保存 %s のログ 開発者になるまであと %d ステップです。 @@ -167,6 +167,6 @@ この操作は、デフォルトでは非表示になっている危険な操作です。代わりに、すべての構成ファイルを手動で削除することもできます。 モデムに更新コマンドを送信 ISD-R AID リストのカスタマイズ - リセット + リセット ISD-R AID リスト diff --git a/app-common/src/main/res/values-zh-rCN/strings.xml b/app-common/src/main/res/values-zh-rCN/strings.xml index e947c6a..32ced90 100644 --- a/app-common/src/main/res/values-zh-rCN/strings.xml +++ b/app-common/src/main/res/values-zh-rCN/strings.xml @@ -2,20 +2,20 @@ 在此设备上未检测到此应用程序可访问的可插拔 eUICC 卡。请插入兼容卡或 USB 读卡器。 此 eSIM 上还没有配置文件 - 未知 - 帮助 - 重新加载卡槽 + 未知 + 帮助 + 重新加载卡槽 逻辑卡槽 %d - 已启用 - 已禁用 - 提供商: + 已启用 + 已禁用 + 提供商: 类型: - 启用 - 禁用 - 删除 - 重命名 - 等待 eSIM 芯片切换配置文件时超时。这可能是您手机基带固件中的一个错误。请尝试切换飞行模式、重新启动应用程序或重新启动手机 - 操作成功, 但是您手机的基带拒绝刷新。您可能需要切换飞行模式或重新启动,以便使用新的配置文件。 + 启用 + 禁用 + 删除 + 重命名 + 等待 eSIM 芯片切换配置文件时超时。这可能是您手机基带固件中的一个错误。请尝试切换飞行模式、重新启动应用程序或重新启动手机 + 操作成功, 但是您手机的基带拒绝刷新。您可能需要切换飞行模式或重新启动,以便使用新的配置文件。 无法切换到新的 eSIM 配置文件。 输入的确认文本不匹配 已复制 ICCID 到剪贴板 @@ -132,7 +132,7 @@ 可插拔 SGP.22 版本 eUICC OS 版本 - GlobalPlatform 版本 + GlobalPlatform 版本 SAS 认证号码 Protected Profile 版本 NVRAM 剩余空间 (eSIM 存储容量) @@ -140,8 +140,8 @@ GSMA 生产环境 CI GSMA 测试 CI 未知 eSIM CI - - + + 还有 %d 步成为开发者 你现在是开发者了! 语言 @@ -152,7 +152,7 @@ 在配置文件列表中包括非生产环境的配置文件 无视 SM-DP+ 的 TLS 证书 允许 RSP 服务器使用任意证书 - 无信息 + 无信息 输入的确认文本不匹配 此芯片已被擦除 正在擦除 eSIM 芯片 @@ -167,6 +167,6 @@ 此操作是默认隐藏的危险操作。作为替代方案,您可以手动删除所有配置文件。 向基带发送刷新命令 自定义 ISD-R AID 列表 - 重置 + 重置 ISD-R AID 列表 \ No newline at end of file diff --git a/app-common/src/main/res/values-zh-rTW/strings.xml b/app-common/src/main/res/values-zh-rTW/strings.xml index 88cc011..5136bf7 100644 --- a/app-common/src/main/res/values-zh-rTW/strings.xml +++ b/app-common/src/main/res/values-zh-rTW/strings.xml @@ -2,20 +2,20 @@ 在此裝置上未檢測到此應用程式可訪問的可插拔 eUICC 卡。請插入相容卡或 USB 晶片讀卡機。 此 eSIM 上還沒有設定檔 - 未知 - 幫助 - 重新載入卡槽 + 未知 + 幫助 + 重新載入卡槽 虛擬卡槽 %d - 已啟用 - 已停用 - 電信業者: + 已啟用 + 已停用 + 電信業者: 類型: - 啟用 - 停用 - 刪除 - 重新命名 - 等待 eSIM 切換設定檔時逾時。這可能是您手機基頻處理器韌體中的一個錯誤。請嘗試切換飛航模式、重新啟動應用程式或重新啟動手機 - 操作成功, 但是您手機的基頻處理器沒有重新整理。您可能需要切換飛航模式或重新啟動,以便使用新的設定檔。 + 啟用 + 停用 + 刪除 + 重新命名 + 等待 eSIM 切換設定檔時逾時。這可能是您手機基頻處理器韌體中的一個錯誤。請嘗試切換飛航模式、重新啟動應用程式或重新啟動手機 + 操作成功, 但是您手機的基頻處理器沒有重新整理。您可能需要切換飛航模式或重新啟動,以便使用新的設定檔。 無法切換到新的 eSIM 設定檔。 輸入的確認文字不匹配 已複製 ICCID 到剪貼簿 @@ -132,7 +132,7 @@ 可插拔 SGP.22 版本 eUICC OS 版本 - GlobalPlatform 版本 + GlobalPlatform 版本 SAS 認證號碼 Protected Profile 版本 NVRAM 剩餘空間 (eSIM 儲存容量) @@ -140,8 +140,8 @@ GSMA 生產環境 CI GSMA 測試 CI 未知 eSIM CI - - + + 還有 %d 步成為開發者 您現在是開發者了! 語言 @@ -152,7 +152,7 @@ 在設定檔列表中包括非生產環境的設定檔 忽略 SM-DP+ 的 TLS 證書 允許 RSP 伺服器使用任意證書 - 無資訊 + 無資訊 輸入的確認文字不匹配 此晶片已被擦除 正在擦除 eSIM 晶片 @@ -167,6 +167,6 @@ 此操作是預設隱藏的危險操作。作為替代方案,您可以手動刪除所有設定檔。 向基帶發送刷新命令 自訂 ISD-R AID 列表 - 重置 + 重置 ISD-R AID 列表 \ No newline at end of file diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index e09da9f..05ca15a 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -2,34 +2,31 @@ No removable eUICC card accessible by this app is detected on this device. Insert a compatible card or a USB reader. No profiles (yet) on this eSIM. - - Help - - Reload Slots - Unknown + Unknown + Information Unavailable + Help + Reload Slots Logical Slot %d - USB - OpenMobile API (OMAPI) + USB + OpenMobile API (OMAPI) - - Enabled - Disabled - Provider: + Enabled + Disabled + Provider: Class: Testing Provisioning Operational - ICCID: - #%d + ICCID: - Enable - Disable - Delete - Rename + Enable + Disable + Delete + Rename - Timed out waiting for the eSIM chip to switch profiles. This may be a bug in your phone\'s modem firmware. Try toggling airplane mode, restarting the application, or rebooting the phone. - The operation was successful, but your phone\'s modem refused to refresh. You might need to toggle airplane mode or reboot in order to use the new profile. + Timed out waiting for the eSIM chip to switch profiles. This may be a bug in your phone\'s modem firmware. Try toggling airplane mode, restarting the application, or rebooting the phone. + The operation was successful, but your phone\'s modem refused to refresh. You might need to toggle airplane mode or reboot in order to use the new profile. Cannot switch to new eSIM profile. Confirmation string mismatch @@ -140,7 +137,7 @@ ISD-R AID SGP.22 Version eUICC OS Version - GlobalPlatform Version + GlobalPlatform Version SAS Accreditation Number Protected Profile Version Free NVRAM (eSIM profile storage) @@ -157,11 +154,8 @@ I CONFIRM TO ERASE THE CHIP WHOSE EID ENDS WITH %s AND UNDERSTAND THAT THIS IS IRREVERSIBLE Erase - - Yes - No - Unknown - Information Unavailable + Yes + No Save Logs at %s @@ -169,9 +163,10 @@ You are %d steps away from being a developer. You are now a developer! + Reset + ISD-R AID List Saved custom ISD-R AID list. - Reset Settings Notifications diff --git a/app-common/src/main/res/xml/pref_settings.xml b/app-common/src/main/res/xml/pref_settings.xml index 17505e1..690a120 100644 --- a/app-common/src/main/res/xml/pref_settings.xml +++ b/app-common/src/main/res/xml/pref_settings.xml @@ -96,14 +96,12 @@ + android:label="@string/compatibility_check" /> by lazy { getCompatibilityChecks(this) } + private val adapter = CompatibilityChecksAdapter() + + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_compatibility_check) + setSupportActionBar(requireViewById(im.angry.openeuicc.common.R.id.toolbar)) + setupToolbarInsets() + supportActionBar!!.setDisplayHomeAsUpEnabled(true) + + compatibilityCheckList = requireViewById(R.id.recycler_view).also { + it.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) + it.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL)) + it.adapter = adapter + } + + setupRootViewInsets(compatibilityCheckList) + } + + @SuppressLint("NotifyDataSetChanged") + override fun onStart() { + super.onStart() + lifecycleScope.launch { + compatibilityChecks.executeAll { adapter.notifyDataSetChanged() } + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean = + when (item.itemId) { + android.R.id.home -> { + finish() + true + } + else -> super.onOptionsItemSelected(item) + } + + inner class ViewHolder(private val root: View): RecyclerView.ViewHolder(root) { + private val titleView: TextView = root.requireViewById(R.id.compatibility_check_title) + private val descView: TextView = root.requireViewById(R.id.compatibility_check_desc) + private val statusContainer: ViewGroup = root.requireViewById(R.id.compatibility_check_status_container) + + fun bindItem(item: CompatibilityCheck) { + titleView.text = item.title + descView.text = Html.fromHtml(item.description, Html.FROM_HTML_MODE_COMPACT) + + statusContainer.children.forEach { + it.isVisible = false + } + + val viewId = when (item.state) { + CompatibilityCheck.State.SUCCESS -> R.id.compatibility_check_checkmark + CompatibilityCheck.State.FAILURE -> R.id.compatibility_check_error + CompatibilityCheck.State.FAILURE_UNKNOWN -> R.id.compatibility_check_unknown + else -> R.id.compatibility_check_progress_bar + } + root.requireViewById(viewId).isVisible = true + } + } + + inner class CompatibilityChecksAdapter: RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = + ViewHolder(layoutInflater.inflate(R.layout.compatibility_check_item, parent, false)) + + override fun getItemCount(): Int = + compatibilityChecks.indexOfLast { it.state != CompatibilityCheck.State.NOT_STARTED } + 1 + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bindItem(compatibilityChecks[position]) + } + } +} \ No newline at end of file diff --git a/app-unpriv/src/main/java/im/angry/openeuicc/ui/QuickCompatibilityActivity.kt b/app-unpriv/src/main/java/im/angry/openeuicc/ui/QuickCompatibilityActivity.kt deleted file mode 100644 index d5e599f..0000000 --- a/app-unpriv/src/main/java/im/angry/openeuicc/ui/QuickCompatibilityActivity.kt +++ /dev/null @@ -1,24 +0,0 @@ -package im.angry.openeuicc.ui - -import android.os.Bundle -import androidx.activity.enableEdgeToEdge -import androidx.appcompat.app.AppCompatActivity -import im.angry.easyeuicc.R -import im.angry.openeuicc.di.UnprivilegedUiComponentFactory -import im.angry.openeuicc.util.OpenEuiccContextMarker - -class QuickCompatibilityActivity : AppCompatActivity(), OpenEuiccContextMarker { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enableEdgeToEdge() - setContentView(R.layout.activity_quick_compatibility) - - val quickCompatibilityFragment = - (appContainer.uiComponentFactory as UnprivilegedUiComponentFactory) - .createQuickCompatibilityFragment() - - supportFragmentManager.beginTransaction() - .replace(R.id.quick_compatibility_container, quickCompatibilityFragment) - .commit() - } -} diff --git a/app-unpriv/src/main/java/im/angry/openeuicc/ui/QuickCompatibilityFragment.kt b/app-unpriv/src/main/java/im/angry/openeuicc/ui/QuickCompatibilityFragment.kt deleted file mode 100644 index 9b41730..0000000 --- a/app-unpriv/src/main/java/im/angry/openeuicc/ui/QuickCompatibilityFragment.kt +++ /dev/null @@ -1,186 +0,0 @@ -package im.angry.openeuicc.ui - -import android.content.pm.PackageManager -import android.icu.text.ListFormatter -import android.os.Build -import android.os.Bundle -import android.se.omapi.Reader -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Button -import android.widget.CheckBox -import android.widget.TextView -import androidx.core.view.isVisible -import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope -import im.angry.easyeuicc.R -import im.angry.openeuicc.util.EUICC_DEFAULT_ISDR_AID -import im.angry.openeuicc.util.UnprivilegedEuiccContextMarker -import im.angry.openeuicc.util.connectSEService -import im.angry.openeuicc.util.decodeHex -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext - -open class QuickCompatibilityFragment : Fragment(), UnprivilegedEuiccContextMarker { - companion object { - enum class Compatibility { - COMPATIBLE, - NOT_COMPATIBLE, - } - - data class CompatibilityResult( - val compatibility: Compatibility, - val slotsOmapi: List = emptyList(), - val slotsIsdr: List = emptyList() - ) - } - - private val conclusion: TextView by lazy { - requireView().requireViewById(R.id.quick_compatibility_conclusion) - } - - private val resultSlots: TextView by lazy { - requireView().requireViewById(R.id.quick_compatibility_result_slots) - } - - private val resultSlotsIsdr: TextView by lazy { - requireView().requireViewById(R.id.quick_compatibility_result_slots_isdr) - } - - private val resultNotes: TextView by lazy { - requireView().requireViewById(R.id.quick_compatibility_result_notes) - } - - private val skipCheckBox: CheckBox by lazy { - requireView().requireViewById(R.id.quick_compatibility_skip) - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View = inflater.inflate(R.layout.fragment_quick_compatibility, container, false).apply { - requireViewById(R.id.quick_compatibility_device_information) - .text = formatDeviceInformation() - requireViewById