diff --git a/.forgejo/workflows/build-debug.yml b/.forgejo/workflows/build-debug.yml index 660dabc..0818b8b 100644 --- a/.forgejo/workflows/build-debug.yml +++ b/.forgejo/workflows/build-debug.yml @@ -33,14 +33,23 @@ jobs: uses: https://gitea.angry.im/actions/setup-android@v3 - name: Build Debug APKs - run: ./gradlew --no-daemon assembleDebug + run: ./gradlew --no-daemon assembleDebug :app:assembleDebugMagiskModuleDir - name: Copy Artifacts - run: find . -name 'app*-debug.apk' -exec cp {} . \; + run: | + find . -name 'app*-debug.apk' -exec cp {} . \; + cp -r app/build/magisk/debug ./magisk-debug - - name: Upload Artifacts + - name: Upload APK Artifacts uses: https://gitea.angry.im/actions/upload-artifact@v3 with: name: Debug APKs compression-level: 0 path: app*-debug.apk + + - name: Upload Magisk Artifacts + uses: https://gitea.angry.im/actions/upload-artifact@v3 + with: + name: magisk-debug + compression-level: 0 + path: magisk-debug diff --git a/.gitmodules b/.gitmodules index f888959..863f185 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/lpac + url = https://github.com/estkme-group/lpac.git diff --git a/.idea/.gitignore b/.idea/.gitignore index b7c2402..d2293f6 100644 --- a/.idea/.gitignore +++ b/.idea/.gitignore @@ -1,14 +1,7 @@ -/shelf -/caches -/libraries -/assetWizardSettings.xml -/deploymentTargetDropDown.xml -/gradle.xml -/misc.xml -/modules.xml -/navEditor.xml -/runConfigurations.xml -/workspace.xml -/AndroidProjectSystem.xml - -**/*.iml \ No newline at end of file +* +!/codeStyles/Project.xml +!/codeStyles/codeStyleConfig.xml +!/vcs.xml +!/kotlinc.xml +!/compiler.xml +!/migrations.xml diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml deleted file mode 100644 index e40be60..0000000 --- a/.idea/deploymentTargetSelector.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/README.md b/README.md index f953f9e..80ab300 100644 --- a/README.md +++ b/README.md @@ -4,27 +4,44 @@ A fully free and open-source Local Profile Assistant implementation for Android There are two variants of this project, OpenEUICC and EasyEUICC: -| | OpenEUICC | EasyEUICC | -|:------------------------------|:-----------------------------------------------:|:-----------------:| -| Privileged | Must be installed as system app | No | -| Internal eSIM | Supported | Unsupported | -| External (Removable) eSIM | Supported | Supported | -| USB Readers | Supported | Supported | -| Requires allowlisting by eSIM | No | Yes -- except USB | -| System Integration | Partial (carrier partner API unimplemented yet) | No | +| | OpenEUICC | EasyEUICC | +| :---------------------------- | :-----------------------------: | :-----------------: | +| Privileged | Must be installed as system app | No | +| Internal eSIM | Supported | Unsupported | +| External eSIM [^1] | Supported | Supported | +| USB Readers | Supported | Supported | +| Requires allowlisting by eSIM | No | Yes -- except USB | +| System Integration | Partial [^2] | No | +| Minimum Android Version | Android 11 or higher | Android 9 or higher | + +[^1]: Also known as "Removable eSIM" +[^2]: Carrier Partner API unimplemented yet Some side notes: -1. When privileged, OpenEUICC supports any eUICC chip that implements the SGP.22 standard, internal or external. However, there is __no guarantee__ that external (removable) eSIMs actually follow the standard. Please __DO NOT__ submit bug reports for non-functioning removable eSIMs. They are __NOT__ officially supported unless they also support / are supported by EasyEUICC, the unprivileged variant. -2. Both variants support accessing eUICC chips through USB CCID readers, regardless of whether the chip contains the correct ARA-M hash to allow for unprivileged access. However, only `T=0` readers that use the standard [USB CCID protocol](https://en.wikipedia.org/wiki/CCID_(protocol)) are supported. -3. Prebuilt release-mode EasyEUICC apks can be downloaded [here](https://gitea.angry.im/PeterCxy/OpenEUICC/releases). For OpenEUICC, no official release is currently provided and only debug mode APKs can be found in the CI page. -4. For removable eSIM chip vendors: to have your chip supported by official builds of EasyEUICC when inserted, include the ARA-M hash `2A2FA878BC7C3354C2CF82935A5945A3EDAE4AFA`. -__This project is Free Software licensed under GNU GPL v3, WITHOUT the "or later" clause.__ Any modification and derivative work __MUST__ be released under the SAME license, which means, at the very least, that the source code __MUST__ be available upon request. +1. When privileged, OpenEUICC supports any eUICC chip that implements the [SGP.22] standard, internal or external. + However, there is **no guarantee** that external (removable) eSIMs actually follow the standard. + Please **DO NOT** submit bug reports for non-functioning removable eSIMs. + They are **NOT** officially supported unless they also support / are supported by EasyEUICC, the unprivileged variant. +2. Both variants support accessing eUICC chips through USB CCID readers, + regardless of whether the chip contains the correct ARA-M hash to allow for unprivileged access. + However, only `T=0` readers that use the standard [USB CCID protocol][usb-ccid] are supported. +3. Prebuilt release-mode EasyEUICC apks can be downloaded [here][releases]. + For OpenEUICC, no official release is currently provided and only debug mode APKs and Magisk modules can be found in the [CI page][actions]. +4. For removable eSIM chip vendors: to have your chip supported by official builds of EasyEUICC when inserted, + include the ARA-M hash `2A2FA878BC7C3354C2CF82935A5945A3EDAE4AFA`. -__If you are releasing a modification of this app, you are kindly asked to make changes to at least the app name and package name.__ +[sgp.22]: https://www.gsma.com/solutions-and-impact/technologies/esim/gsma_resources/sgp-22-v2-2-2/ "SGP.22 v2.2.2" +[usb-ccid]: https://en.wikipedia.org/wiki/CCID_%28protocol%29 "USB CCID Protocol" +[releases]: https://gitea.angry.im/PeterCxy/OpenEUICC/releases "EasyEUICC Releases" +[actions]: https://gitea.angry.im/PeterCxy/OpenEUICC/actions "OpenEUICC Actions" -Building (Gradle) -=== +**This project is Free Software licensed under GNU GPL v3, WITHOUT the "or later" clause.** +Any modification and derivative work **MUST** be released under the SAME license, which means, at the very least, that the source code **MUST** be available upon request. + +**If you are releasing a modification of this app, you are kindly asked to make changes to at least the app name and package name.** + +# Building (Gradle) Make sure you have all submodules cloned and updated by running @@ -57,8 +74,7 @@ For EasyEUICC: ./gradlew :app-unpriv:assembleRelease ``` -Building (AOSP) -=== +# Building (AOSP) There are two ways to include OpenEUICC in your AOSP-based system image: @@ -68,25 +84,22 @@ There are two ways to include OpenEUICC in your AOSP-based system image: - Compilation of this project is **only** tested against the latest AOSP release version. The app itself should be compatible with older AOSP versions, but the source may not compile against an older AOSP source tree. 2. If compilation against AOSP source tree is not possible, consider [building with gradle](#building-gradle) and import the apk as a prebuilt. - No official `Android.bp` is provided for this case but it should be straightforward to write. - - You might want to include `privapp_whitelist_im.angry.openeuicc.xml` as well. + - You might want to include [`privapp_whitelist_im.angry.openeuicc.xml`] as well. -FAQs -=== +[`privapp_whitelist_im.angry.openeuicc.xml`]: privapp_whitelist_im.angry.openeuicc.xml "OpenEUICC Privapp Whitelist" -- Q: Do you provide prebuilt binaries for OpenEUICC? -- A: Debug-mode APKs are available continuously as an artifact of the [Actions](https://gitea.angry.im/PeterCxy/OpenEUICC/actions) CI used by this project. However, these debug-mode APKs are **not** intended for inclusion inside system images, nor are they supported by the developer in any sense. If you are a custom ROM developer, either include the entire OpenEUICC repository in your AOSP source tree, or generate an APK using `gradle` and import that as a prebuilt system app. Note that you might want `privapp_whitelist_im.angry.openeuicc.xml` as well. +# FAQs -- Q: AOSP's Settings app seems to be confused by OpenEUICC (for example, disabling / enabling profiles from the Networks page do not work properly) -- A: When your device has internal eSIM chip(s) __and__ you have inserted a removable eSIM chip, the Settings app can misbehave since it was never designed for this scenario. __Please prefer using OpenEUICC's own management interface whenever possible.__ In the future, there might be an option to exclude removable SIMs from being reported to the Android system. +- Q: Do you provide prebuilt binaries for OpenEUICC? \ + A: Debug-mode APKs and Magisk modules are available continuously as an artifact of the [Actions] CI used by this project. However, these debug-mode APKs are **not** intended for inclusion inside system images, nor are they supported by the developer in any sense. If you are a custom ROM developer, either include the entire OpenEUICC repository in your AOSP source tree, or generate an APK using `gradle` and import that as a prebuilt system app. Note that you might want [`privapp_whitelist_im.angry.openeuicc.xml`] as well. -- Q: Can EasyEUICC manage my phone's internal eSIM? -- A: No. For EasyEUICC to work, the eSIM chip MUST proactively grant access via its ARA-M field. +- Q: Can EasyEUICC manage my phone's internal eSIM? \ + A: No. For EasyEUICC to work, the eSIM chip MUST proactively grant access via its ARA-M field. -- Q: Removable eSIMs? Are they a joke? -- A: No, even though the name "removable embedded SIM" can sound like an oxymoron. In fact, there can be many advantages to these chips compared to fully embedded ones. For example, the ability to transfer eSIM profiles without carrier support or approval, or the ability to use eSIM on devices that do not and may never get the support, such as Wi-Fi hotspots. +- Q: Removable eSIMs? Are they a joke? \ + A: No, even though the name "removable embedded SIM" can sound like an oxymoron. In fact, there can be many advantages to these chips compared to fully embedded ones. For example, the ability to transfer eSIM profiles without carrier support or approval, or the ability to use eSIM on devices that do not and may never get the support, such as Wi-Fi hotspots. -Copyright -=== +# Copyright Everything except `libs/lpac-jni` and `art/`: @@ -126,4 +139,4 @@ License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA ``` -`art/`: Courtesy of [Aikoyori](https://github.com/Aikoyori), CC NC-SA 4.0. \ No newline at end of file +`art/`: Courtesy of [Aikoyori](https://github.com/Aikoyori), CC NC-SA 4.0. diff --git a/app-common/src/main/AndroidManifest.xml b/app-common/src/main/AndroidManifest.xml index b0324dc..44c82c0 100644 --- a/app-common/src/main/AndroidManifest.xml +++ b/app-common/src/main/AndroidManifest.xml @@ -45,8 +45,9 @@ - - + + + 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 b975313..78a8c3f 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,9 +20,8 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha override suspend fun tryOpenEuiccChannel( port: UiccPortInfoCompat, - isdrAid: ByteArray, - seId: EuiccChannel.SecureElementId, - ): EuiccChannel? { + isdrAid: ByteArray + ): EuiccChannel? = try { if (port.portIndex != 0) { Log.w( DefaultEuiccChannelManager.TAG, @@ -36,61 +35,52 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha DefaultEuiccChannelManager.TAG, "Trying OMAPI for physical slot ${port.card.physicalSlotIndex}" ) - try { - return EuiccChannelImpl( - context.getString(R.string.omapi), + EuiccChannelImpl( + context.getString(R.string.channel_type_omapi), + port, + intrinsicChannelName = null, + OmapiApduInterface( + seService!!, port, - intrinsicChannelName = null, - OmapiApduInterface( - seService!!, - port, - context.preferenceRepository.verboseLoggingFlow - ), - isdrAid, - seId, - context.preferenceRepository.verboseLoggingFlow, - context.preferenceRepository.ignoreTLSCertificateFlow, - ).also { - Log.i(DefaultEuiccChannelManager.TAG, "Is OMAPI channel, setting MSS to 60") - it.lpa.setEs10xMss(60) - } - } catch (_: IllegalArgumentException) { - // Failed - Log.w( - DefaultEuiccChannelManager.TAG, - "OMAPI APDU interface unavailable for physical slot ${port.card.physicalSlotIndex} with ISD-R AID: ${isdrAid.encodeHex()}." - ) - } - - return null + context.preferenceRepository.verboseLoggingFlow + ), + isdrAid, + context.preferenceRepository.verboseLoggingFlow, + context.preferenceRepository.ignoreTLSCertificateFlow, + context.preferenceRepository.es10xMssFlow, + ) + } catch (_: IllegalArgumentException) { + // Failed + Log.w( + DefaultEuiccChannelManager.TAG, + "OMAPI APDU interface unavailable for physical slot ${port.card.physicalSlotIndex} with ISD-R AID: ${isdrAid.encodeHex()}." + ) + null } override fun tryOpenUsbEuiccChannel( ccidCtx: UsbCcidContext, - isdrAid: ByteArray, - seId: EuiccChannel.SecureElementId - ): EuiccChannel? { - try { - return EuiccChannelImpl( - context.getString(R.string.usb), - FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)), - intrinsicChannelName = ccidCtx.productName, - UsbApduInterface( - ccidCtx - ), - isdrAid, - seId, - context.preferenceRepository.verboseLoggingFlow, - context.preferenceRepository.ignoreTLSCertificateFlow, - ) - } catch (_: IllegalArgumentException) { - // Failed - Log.w( - DefaultEuiccChannelManager.TAG, - "USB APDU interface unavailable for ISD-R AID: ${isdrAid.encodeHex()}." - ) - } - return null + isdrAid: ByteArray + ): EuiccChannel? = try { + EuiccChannelImpl( + context.getString(R.string.channel_type_usb), + FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)), + intrinsicChannelName = ccidCtx.productName, + UsbApduInterface( + ccidCtx + ), + isdrAid, + context.preferenceRepository.verboseLoggingFlow, + context.preferenceRepository.ignoreTLSCertificateFlow, + context.preferenceRepository.es10xMssFlow, + ) + } catch (_: IllegalArgumentException) { + // Failed + Log.w( + DefaultEuiccChannelManager.TAG, + "USB APDU interface unavailable for ISD-R AID: ${isdrAid.encodeHex()}." + ) + null } override fun cleanup() { 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 2d0493b..6b336cd 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 usbChannels = mutableListOf() + private var usbChannel: EuiccChannel? = null private val lock = Mutex() @@ -51,20 +51,15 @@ open class DefaultEuiccChannelManager( protected open val uiccCards: Collection get() = (0.. EuiccChannel?): List { + private suspend inline fun tryOpenChannelFirstValidAid(openFn: (ByteArray) -> EuiccChannel?): EuiccChannel? { val isdrAidList = parseIsdrAidList(appContainer.preferenceRepository.isdrAidListFlow.first()) - var seId = 0 - return isdrAidList.mapNotNull { - Log.i( - TAG, - "Opening channel, trying ISDR AID ${it.encodeHex()}, this will be seId $seId" - ) + return isdrAidList.firstNotNullOfOrNull { + Log.i(TAG, "Opening channel, trying ISDR AID ${it.encodeHex()}") - openFn(it, EuiccChannel.SecureElementId.createFromInt(seId))?.let { channel -> + openFn(it)?.let { channel -> if (channel.valid) { - seId += 1 channel } else { channel.close() @@ -74,18 +69,19 @@ open class DefaultEuiccChannelManager( } } - private suspend fun tryOpenEuiccChannel( - port: UiccPortInfoCompat, - seId: EuiccChannel.SecureElementId = EuiccChannel.SecureElementId.DEFAULT - ): EuiccChannel? { + private suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel? { lock.withLock { if (port.card.physicalSlotIndex == EuiccChannelManager.USB_CHANNEL_ID) { - // We only compare seId because we assume we can only open 1 card from USB - return usbChannels.find { it.seId == seId } + return if (usbChannel != null && usbChannel!!.valid) { + usbChannel + } else { + usbChannel = null + null + } } val existing = - channelCache.find { it.slotId == port.card.physicalSlotIndex && it.portId == port.portIndex && it.seId == seId } + channelCache.find { it.slotId == port.card.physicalSlotIndex && it.portId == port.portIndex } if (existing != null) { if (existing.valid && port.logicalSlotIndex == existing.logicalSlotId) { return existing @@ -100,18 +96,12 @@ open class DefaultEuiccChannelManager( return null } - val channels = - tryOpenChannelWithKnownAids { isdrAid, seId -> - euiccChannelFactory.tryOpenEuiccChannel( - port, - isdrAid, - seId - ) - } + val channel = + tryOpenChannelFirstValidAid { euiccChannelFactory.tryOpenEuiccChannel(port, it) } - if (channels.isNotEmpty()) { - channelCache.addAll(channels) - return channels.find { it.seId == seId } + if (channel != null) { + channelCache.add(channel) + return channel } else { Log.i( TAG, @@ -122,13 +112,10 @@ open class DefaultEuiccChannelManager( } } - protected suspend fun findEuiccChannelByLogicalSlot( - logicalSlotId: Int, - seId: EuiccChannel.SecureElementId = EuiccChannel.SecureElementId.DEFAULT - ): EuiccChannel? = + protected suspend fun findEuiccChannelByLogicalSlot(logicalSlotId: Int): EuiccChannel? = withContext(Dispatchers.IO) { if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { - return@withContext usbChannels.find { it.seId == seId } + return@withContext usbChannel } for (card in uiccCards) { @@ -144,7 +131,7 @@ open class DefaultEuiccChannelManager( private suspend fun findAllEuiccChannelsByPhysicalSlot(physicalSlotId: Int): List? { if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { - return usbChannels.ifEmpty { null } + return usbChannel?.let { listOf(it) } } for (card in uiccCards) { @@ -155,18 +142,14 @@ open class DefaultEuiccChannelManager( return null } - private suspend fun findEuiccChannelByPort( - physicalSlotId: Int, - portId: Int, - seId: EuiccChannel.SecureElementId = EuiccChannel.SecureElementId.DEFAULT - ): EuiccChannel? = + private suspend fun findEuiccChannelByPort(physicalSlotId: Int, portId: Int): EuiccChannel? = withContext(Dispatchers.IO) { if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { - return@withContext usbChannels.find { it.seId == seId } + return@withContext usbChannel } uiccCards.find { it.physicalSlotIndex == physicalSlotId }?.let { card -> - card.ports.find { it.portIndex == portId }?.let { tryOpenEuiccChannel(it, seId) } + card.ports.find { it.portIndex == portId }?.let { tryOpenEuiccChannel(it) } } } @@ -185,17 +168,15 @@ open class DefaultEuiccChannelManager( return@withContext listOf(0) } - findAllEuiccChannelsByPhysicalSlot(physicalSlotId)?.map { it.portId }?.toSet()?.toList() - ?: listOf() + findAllEuiccChannelsByPhysicalSlot(physicalSlotId)?.map { it.portId } ?: listOf() } override suspend fun withEuiccChannel( physicalSlotId: Int, portId: Int, - seId: EuiccChannel.SecureElementId, fn: suspend (EuiccChannel) -> R ): R { - val channel = findEuiccChannelByPort(physicalSlotId, portId, seId) + val channel = findEuiccChannelByPort(physicalSlotId, portId) ?: throw EuiccChannelManager.EuiccChannelNotFoundException() val wrapper = EuiccChannelWrapper(channel) try { @@ -209,10 +190,9 @@ open class DefaultEuiccChannelManager( override suspend fun withEuiccChannel( logicalSlotId: Int, - seId: EuiccChannel.SecureElementId, fn: suspend (EuiccChannel) -> R ): R { - val channel = findEuiccChannelByLogicalSlot(logicalSlotId, seId) + val channel = findEuiccChannelByLogicalSlot(logicalSlotId) ?: throw EuiccChannelManager.EuiccChannelNotFoundException() val wrapper = EuiccChannelWrapper(channel) try { @@ -226,8 +206,8 @@ open class DefaultEuiccChannelManager( override suspend fun waitForReconnect(physicalSlotId: Int, portId: Int, timeoutMillis: Long) { if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { - usbChannels.forEach { it.close() } - usbChannels.clear() + usbChannel?.close() + usbChannel = null } 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 @@ -243,7 +223,7 @@ open class DefaultEuiccChannelManager( // tryOpenUsbEuiccChannel() will always try to reopen the channel, even if // a USB channel already exists tryOpenUsbEuiccChannel() - usbChannels.getOrNull(0)!! + usbChannel!! } else { // tryOpenEuiccChannel() will automatically dispose of invalid channels // and recreate when needed @@ -284,20 +264,6 @@ 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 -> @@ -311,17 +277,15 @@ 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 channels = tryOpenChannelWithKnownAids { isdrAid, seId -> - euiccChannelFactory.tryOpenUsbEuiccChannel(ccidCtx, isdrAid, seId) + val channel = tryOpenChannelFirstValidAid { + euiccChannelFactory.tryOpenUsbEuiccChannel(ccidCtx, it) } - if (channels.isNotEmpty() && channels[0].valid) { + if (channel != null && channel.lpa.valid) { ccidCtx.allowDisconnect = true - usbChannels.clear() - usbChannels.addAll(channels) + usbChannel = channel return@withContext Pair(device, true) } } catch (e: Exception) { @@ -345,8 +309,8 @@ open class DefaultEuiccChannelManager( channel.close() } - usbChannels.forEach { it.close() } - usbChannels.clear() + usbChannel?.close() + usbChannel = null 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 19dd682..b20932f 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,7 +1,5 @@ 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 @@ -15,56 +13,6 @@ 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 a8051af..ba587a6 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,12 +6,11 @@ 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, seId: EuiccChannel.SecureElementId): EuiccChannel? + suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat, isdrAid: ByteArray): EuiccChannel? fun tryOpenUsbEuiccChannel( ccidCtx: UsbCcidContext, - isdrAid: ByteArray, - seId: EuiccChannel.SecureElementId + isdrAid: ByteArray ): 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 491f6d2..eaec522 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 @@ -1,8 +1,9 @@ package im.angry.openeuicc.core import im.angry.openeuicc.util.UiccPortInfoCompat -import im.angry.openeuicc.util.decodeHex import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking import net.typeblog.lpac_jni.ApduInterface import net.typeblog.lpac_jni.LocalProfileAssistant import net.typeblog.lpac_jni.impl.HttpInterfaceImpl @@ -14,9 +15,9 @@ class EuiccChannelImpl( override val intrinsicChannelName: String?, override val apduInterface: ApduInterface, override val isdrAid: ByteArray, - override val seId: EuiccChannel.SecureElementId, verboseLoggingFlow: Flow, - ignoreTLSCertificateFlow: Flow + ignoreTLSCertificateFlow: Flow, + es10xMssFlow: Flow, ) : EuiccChannel { override val slotId = port.card.physicalSlotIndex override val logicalSlotId = port.logicalSlotIndex @@ -26,8 +27,10 @@ class EuiccChannelImpl( LocalProfileAssistantImpl( isdrAid, apduInterface, - HttpInterfaceImpl(verboseLoggingFlow, ignoreTLSCertificateFlow) - ) + HttpInterfaceImpl(verboseLoggingFlow, ignoreTLSCertificateFlow), + ).also { + it.setEs10xMss(runBlocking { es10xMssFlow.first().toByte() }) + } override val atr: ByteArray? get() = (apduInterface as? ApduInterfaceAtrProvider)?.atr 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 eb8e87c..17f3130 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,14 +37,6 @@ 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 @@ -89,16 +81,14 @@ 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, SecureElementId, (EuiccChannel) -> R) but instead uses logical slot ID + * Same as withEuiccChannel(Int, Int, (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 a36c1b6..361a943 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,8 +26,6 @@ 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 b493611..76227fd 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.enable_disable_timeout) + get() = context.getString(R.string.profile_switch_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 d268da8..52a501a 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,18 +2,13 @@ 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, - seId: EuiccChannel.SecureElementId - ): EuiccManagementFragment = - EuiccManagementFragment.newInstance(slotId, portId, seId) + override fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment = + EuiccManagementFragment.newInstance(slotId, portId) 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 6a4d13f..2c3c72b 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,16 +2,10 @@ 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, - seId: EuiccChannel.SecureElementId - ): EuiccManagementFragment - + fun createEuiccManagementFragment(slotId: Int, portId: Int): 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 bd869ca..248afaf 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,14 +36,13 @@ private val RE_SAS = Regex( class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { companion object { - private val YES_NO = Pair(R.string.yes, R.string.no) + private val YES_NO = Pair(R.string.euicc_info_yes, R.string.euicc_info_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 @@ -68,15 +67,9 @@ 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.usb) + getString(R.string.channel_type_usb) } else { appContainer.customizableTextProvider.formatInternalChannelName(logicalSlotId) } @@ -106,7 +99,7 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { lifecycleScope.launch { (infoList.adapter!! as EuiccInfoAdapter).euiccInfoItems = - euiccChannelManager.withEuiccChannel(logicalSlotId, fn = ::buildEuiccInfoItems) + euiccChannelManager.withEuiccChannel(logicalSlotId, ::buildEuiccInfoItems) swipeRefresh.isRefreshing = false } @@ -114,42 +107,29 @@ 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_globalplatform_version, info.globalPlatformVersion.toString())) + add(Item(R.string.euicc_info_gp_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())) } - add(Item(R.string.euicc_info_free_nvram, info.freeNvram.let(::formatFreeSpace))) + + val nvramText = buildString { + append(formatFreeSpace(info.freeNvram)) + append(' ') + append(getString(R.string.euicc_info_free_nvram_hint)) + } + add(Item(R.string.euicc_info_free_nvram, nvramText)) } channel.lpa.euiccInfo2?.euiccCiPKIdListForSigning.orEmpty().let { signers -> // SGP.28 v1.0, eSIM CI Registration Criteria (Page 5 of 9, 2019-10-24) @@ -157,14 +137,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.unknown // the case is not mp, but it's is not common + signers.isEmpty() -> R.string.euicc_info_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.information_unavailable) + val atr = channel.atr?.encodeHex() ?: getString(R.string.euicc_info_unavailable) add(Item(R.string.euicc_info_atr, atr, copiedToastResId = R.string.toast_atr_copied)) } @@ -197,7 +177,7 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { fun bind(item: Item) { copiedToastResId = item.copiedToastResId title.setText(item.titleResId) - content.text = item.content ?: getString(R.string.unknown) + content.text = item.content ?: getString(R.string.euicc_info_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 4be3ebc..016e96f 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,7 +31,6 @@ 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 @@ -50,12 +49,8 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, companion object { const val TAG = "EuiccManagementFragment" - fun newInstance( - slotId: Int, - portId: Int, - seId: EuiccChannel.SecureElementId - ): EuiccManagementFragment = - newInstanceEuicc(EuiccManagementFragment::class.java, slotId, portId, seId) + fun newInstance(slotId: Int, portId: Int): EuiccManagementFragment = + newInstanceEuicc(EuiccManagementFragment::class.java, slotId, portId) } private lateinit var swipeRefresh: SwipeRefreshLayout @@ -153,7 +148,6 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, R.id.show_notifications -> { Intent(requireContext(), NotificationsActivity::class.java).apply { putExtra("logicalSlotId", logicalSlotId) - putExtra("seId", seId) startActivity(this) } true @@ -162,14 +156,13 @@ 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, seId, eid) + EuiccMemoryResetFragment.newInstance(slotId, portId, eid) .show(childFragmentManager, EuiccMemoryResetFragment.TAG) true } @@ -260,7 +253,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, if (!isUsb) { withContext(Dispatchers.Main) { AlertDialog.Builder(requireContext()).apply { - setMessage(R.string.switch_did_not_refresh) + setMessage(R.string.profile_switch_did_not_refresh) setPositiveButton(android.R.string.ok) { dialog, _ -> dialog.dismiss() requireActivity().finish() @@ -301,10 +294,7 @@ 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 @@ -331,7 +321,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, @@ -357,6 +347,7 @@ 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 { @@ -376,7 +367,9 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, true } - profileMenu.setOnClickListener { showOptionsMenu() } + profileMenu.setOnClickListener { + showOptionsMenu() + } } private lateinit var profile: LocalProfileInfo @@ -387,9 +380,9 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, state.setText( if (profile.isEnabled) { - R.string.enabled + R.string.profile_state_enabled } else { - R.string.disabled + R.string.profile_state_disabled } ) provider.text = profile.providerName @@ -406,6 +399,13 @@ 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,36 +423,20 @@ 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, - seId, - profile.iccid, - profile.displayName - ) + ProfileRenameFragment.newInstance(slotId, portId, profile.iccid, profile.displayName) .show(childFragmentManager, ProfileRenameFragment.TAG) true } - R.id.delete -> { - ProfileDeleteFragment.newInstance( - slotId, - portId, - seId, - profile.iccid, - profile.displayName - ) + ProfileDeleteFragment.newInstance(slotId, portId, profile.iccid, profile.displayName) .show(childFragmentManager, ProfileDeleteFragment.TAG) true } - else -> false } } @@ -464,11 +448,9 @@ 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() } @@ -479,11 +461,9 @@ 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 } @@ -491,8 +471,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 79a8f0a..086a849 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,7 +11,6 @@ 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 @@ -30,8 +29,8 @@ class EuiccMemoryResetFragment : DialogFragment(), EuiccChannelFragmentMarker { private const val FIELD_EID = "eid" - fun newInstance(slotId: Int, portId: Int, seId: EuiccChannel.SecureElementId, eid: String) = - newInstanceEuicc(EuiccMemoryResetFragment::class.java, slotId, portId, seId) { + fun newInstance(slotId: Int, portId: Int, eid: String) = + newInstanceEuicc(EuiccMemoryResetFragment::class.java, slotId, portId) { 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 93f51dd..b42f4cf 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,7 +23,6 @@ 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 @@ -113,12 +112,10 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { startActivity(Intent(this, SettingsActivity::class.java)) true } - R.id.reload -> { refresh() true } - else -> super.onOptionsItemSelected(item) } @@ -157,33 +154,27 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { euiccChannelManager.flowInternalEuiccPorts().onEach { (slotId, portId) -> Log.d(TAG, "slot $slotId port $portId") - 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, - seId - ) - }) + euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> + if (preferenceRepository.verboseLoggingFlow.first()) { + Log.d(TAG, channel.lpa.eID) } - }.collect() + // 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) + }) + } }.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.usb) + val productName = it.productName ?: getString(R.string.channel_type_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 594fdd1..07d5f13 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,7 +1,6 @@ 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 @@ -21,7 +20,6 @@ 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 @@ -29,13 +27,12 @@ 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() @@ -54,27 +51,16 @@ 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.usb) + getString(R.string.channel_type_usb) } else { appContainer.customizableTextProvider.formatInternalChannelName(logicalSlotId) } @@ -100,7 +86,6 @@ class NotificationsActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker finish() true } - R.id.help -> { AlertDialog.Builder(this, R.style.AlertDialogTheme).apply { setMessage(R.string.profile_notifications_help) @@ -111,7 +96,6 @@ class NotificationsActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker } true } - else -> super.onOptionsItemSelected(item) } @@ -130,20 +114,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( @@ -152,7 +136,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 = @@ -186,8 +170,7 @@ 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 @@ -198,13 +181,10 @@ 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( @@ -233,7 +213,6 @@ class NotificationsActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker } true } - R.id.notification_delete -> { launchTask { withContext(Dispatchers.IO) { @@ -246,12 +225,11 @@ 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 d5cf496..38d1bc6 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,7 +9,6 @@ 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 @@ -21,8 +20,8 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker { private const val FIELD_ICCID = "iccid" private const val FIELD_NAME = "name" - fun newInstance(slotId: Int, portId: Int, seId: EuiccChannel.SecureElementId, iccid: String, name: String) = - newInstanceEuicc(ProfileDeleteFragment::class.java, slotId, portId, seId) { + fun newInstance(slotId: Int, portId: Int, iccid: String, name: String) = + newInstanceEuicc(ProfileDeleteFragment::class.java, slotId, portId) { 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 e5f8251..281e625 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,7 +12,6 @@ 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 @@ -25,8 +24,8 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment const val TAG = "ProfileRenameFragment" - fun newInstance(slotId: Int, portId: Int, seId: EuiccChannel.SecureElementId, iccid: String, currentName: String) = - newInstanceEuicc(ProfileRenameFragment::class.java, slotId, portId, seId) { + fun newInstance(slotId: Int, portId: Int, iccid: String, currentName: String) = + newInstanceEuicc(ProfileRenameFragment::class.java, slotId, portId) { putString(FIELD_ICCID, iccid) putString(FIELD_CURRENT_NAME, currentName) } @@ -66,7 +65,7 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment super.onViewCreated(view, savedInstanceState) profileRenameNewName.editText!!.setText(currentName) toolbar.apply { - setTitle(R.string.rename) + setTitle(R.string.profile_rename) setNavigationOnClickListener { if (!renaming) dismiss() } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt index 6554142..7a717ac 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt @@ -8,6 +8,7 @@ import android.provider.Settings import android.widget.Toast import androidx.lifecycle.lifecycleScope import androidx.preference.CheckBoxPreference +import androidx.preference.ListPreference import androidx.preference.Preference import androidx.preference.PreferenceCategory import androidx.preference.PreferenceFragmentCompat @@ -16,7 +17,6 @@ import im.angry.openeuicc.util.* import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking open class SettingsFragment: PreferenceFragmentCompat() { private lateinit var developerPref: PreferenceCategory @@ -34,7 +34,7 @@ open class SettingsFragment: PreferenceFragmentCompat() { // Show / hide developer preference based on whether it is enabled lifecycleScope.launch { preferenceRepository.developerOptionsEnabledFlow - .onEach { developerPref.isVisible = it } + .onEach(developerPref::setVisible) .collect() } @@ -84,6 +84,9 @@ open class SettingsFragment: PreferenceFragmentCompat() { requirePreference("pref_developer_euicc_memory_reset") .bindBooleanFlow(preferenceRepository.euiccMemoryResetFlow) + requirePreference("pref_developer_es10x_mss") + .bindIntFlow(preferenceRepository.es10xMssFlow, 63) + requirePreference("pref_developer_isdr_aid_list").apply { intent = Intent(requireContext(), IsdrAidListActivity::class.java) } @@ -100,51 +103,53 @@ open class SettingsFragment: PreferenceFragmentCompat() { @Suppress("UNUSED_PARAMETER") private fun onAppVersionClicked(pref: Preference): Boolean { if (developerPref.isVisible) return false + val now = System.currentTimeMillis() - if (now - lastClickTimestamp >= 1000) { - numClicks = 1 - } else { - numClicks++ - } + numClicks = if (now - lastClickTimestamp >= 1000) 1 else numClicks + 1 lastClickTimestamp = now - if (numClicks == 7) { - lifecycleScope.launch { - preferenceRepository.developerOptionsEnabledFlow.updatePreference(true) - - lastToast?.cancel() - Toast.makeText( - requireContext(), - R.string.developer_options_enabled, - Toast.LENGTH_SHORT - ).show() - } - } else if (numClicks > 1) { - lastToast?.cancel() - lastToast = Toast.makeText( - requireContext(), - getString(R.string.developer_options_steps, 7 - numClicks), - Toast.LENGTH_SHORT - ) - lastToast!!.show() + lifecycleScope.launch { + preferenceRepository.developerOptionsEnabledFlow.updatePreference(numClicks >= 7) } + val toastText = when { + numClicks == 7 -> getString(R.string.developer_options_enabled) + numClicks > 1 -> getString(R.string.developer_options_steps, 7 - numClicks) + else -> return true + } + + lastToast?.cancel() + lastToast = Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT) + lastToast!!.show() return true } protected fun CheckBoxPreference.bindBooleanFlow(flow: PreferenceFlowWrapper) { lifecycleScope.launch { - flow.collect { isChecked = it } + flow.collect(::setChecked) } setOnPreferenceChangeListener { _, newValue -> - runBlocking { + lifecycleScope.launch { flow.updatePreference(newValue as Boolean) } true } } + private fun ListPreference.bindIntFlow(flow: PreferenceFlowWrapper, defaultValue: Int) { + lifecycleScope.launch { + flow.collect { value = it.toString() } + } + + setOnPreferenceChangeListener { _, newValue -> + lifecycleScope.launch { + flow.updatePreference((newValue as String).toIntOrNull() ?: defaultValue) + } + true + } + } + protected fun mergePreferenceOverlay(overlayKey: String, targetKey: String) { val overlayCat = requirePreference(overlayKey) val targetCat = requirePreference(targetKey) 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 a14e47d..7a52ca0 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,7 +20,6 @@ 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 @@ -157,9 +156,7 @@ class UsbCcidReaderFragment : Fragment(), OpenEuiccContextMarker { R.id.child_container, appContainer.uiComponentFactory.createEuiccManagementFragment( slotId = EuiccChannelManager.USB_CHANNEL_ID, - portId = 0, - // TODO: What if a USB card has multiple SEs? - seId = EuiccChannel.SecureElementId.DEFAULT + portId = 0 ) ) } 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 9e312d4..6574645 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 - if (uri?.scheme == "lpa") { + val uri = intent.data ?: return + if (uri.scheme.contentEquals("lpa", ignoreCase = true)) { 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 e282196..3841868 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,6 +8,7 @@ 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() { @@ -86,9 +87,10 @@ class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardS ret.appendLine() val str = resp.data.decodeToString(throwOnInvalidSequence = false) + ret.appendLine( if (str.startsWith('{')) { - str.prettyPrintJson() + JSONObject(str).toString(2) } else { str } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt index 342a687..0048190 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt @@ -7,6 +7,7 @@ import android.view.ViewGroup import android.widget.ImageView import android.widget.ProgressBar import android.widget.TextView +import androidx.annotation.StringRes import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager @@ -42,19 +43,17 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep } private data class ProgressItem( - val titleRes: Int, - var state: ProgressState + @StringRes val titleRes: Int, + var state: ProgressState = ProgressState.NotStarted, + var errorMessage: SimplifiedErrorMessages? = null, ) private val progressItems = arrayOf( - ProgressItem(R.string.download_wizard_progress_step_preparing, ProgressState.NotStarted), - ProgressItem(R.string.download_wizard_progress_step_connecting, ProgressState.NotStarted), - ProgressItem( - R.string.download_wizard_progress_step_authenticating, - ProgressState.NotStarted - ), - ProgressItem(R.string.download_wizard_progress_step_downloading, ProgressState.NotStarted), - ProgressItem(R.string.download_wizard_progress_step_finalizing, ProgressState.NotStarted) + ProgressItem(R.string.download_wizard_progress_step_preparing), + ProgressItem(R.string.download_wizard_progress_step_connecting), + ProgressItem(R.string.download_wizard_progress_step_authenticating), + ProgressItem(R.string.download_wizard_progress_step_downloading), + ProgressItem(R.string.download_wizard_progress_step_finalizing) ) private val adapter = ProgressItemAdapter() @@ -122,8 +121,13 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep // Change the state of the last InProgress item to success (or error) progressItems.forEachIndexed { index, progressItem -> if (progressItem.state == ProgressState.InProgress) { - progressItem.state = - if (state.downloadError == null) ProgressState.Done else ProgressState.Error + if (state.downloadError == null) { + progressItem.state = ProgressState.Done + } else { + progressItem.state = ProgressState.Error + progressItem.errorMessage = + SimplifiedErrorMessages.fromDownloadError(state.downloadError!!) + } } adapter.notifyItemChanged(index) @@ -133,9 +137,8 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep refreshButtons() } - is EuiccChannelManagerService.ForegroundTaskState.InProgress -> { + is EuiccChannelManagerService.ForegroundTaskState.InProgress -> updateProgress(it.progress) - } else -> {} } @@ -197,9 +200,15 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep private val progressBar = root.requireViewById(R.id.download_progress_icon_progress) private val icon = root.requireViewById(R.id.download_progress_icon) + private val errorTitle = + root.requireViewById(R.id.download_progress_item_error_title) + private val errorSuggestion = + root.requireViewById(R.id.download_progress_item_error_suggestion) fun bind(item: ProgressItem) { title.text = getString(item.titleRes) + errorTitle.visibility = View.GONE + errorSuggestion.visibility = View.GONE when (item.state) { ProgressState.NotStarted -> { @@ -222,6 +231,15 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep progressBar.visibility = View.GONE icon.setImageResource(R.drawable.ic_error_outline) icon.visibility = View.VISIBLE + + item.errorMessage?.titleResId?.let { + errorTitle.visibility = View.VISIBLE + errorTitle.text = getString(it) + } + item.errorMessage?.suggestResId?.let { + errorSuggestion.visibility = View.VISIBLE + errorSuggestion.text = getString(it) + } } } } 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 28bc9f0..8097058 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,7 +19,6 @@ 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 { @@ -187,12 +186,12 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt } title.text = if (item.logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { - item.intrinsicChannelName ?: root.context.getString(R.string.usb) + item.intrinsicChannelName ?: root.context.getString(R.string.channel_type_usb) } else { appContainer.customizableTextProvider.formatInternalChannelName(item.logicalSlotId) } eID.text = item.eID - activeProfile.text = item.enabledProfileName ?: root.context.getString(R.string.unknown) + activeProfile.text = item.enabledProfileName ?: root.context.getString(R.string.profile_no_enabled_profile) freeSpace.text = formatFreeSpace(item.freeSpace) checkBox.isChecked = adapter.currentSelectedIdx == idx } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/SimplifiedErrorMessages.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/SimplifiedErrorMessages.kt new file mode 100644 index 0000000..8ce5740 --- /dev/null +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/SimplifiedErrorMessages.kt @@ -0,0 +1,154 @@ +package im.angry.openeuicc.ui.wizard + +import androidx.annotation.StringRes +import im.angry.openeuicc.common.R +import net.typeblog.lpac_jni.LocalProfileAssistant +import org.json.JSONObject +import java.net.NoRouteToHostException +import java.net.PortUnreachableException +import java.net.SocketException +import java.net.SocketTimeoutException +import java.net.UnknownHostException +import javax.net.ssl.SSLException + +enum class SimplifiedErrorMessages( + @StringRes val titleResId: Int, + @StringRes val suggestResId: Int? +) { + ICCIDAlreadyInUse( + R.string.download_wizard_error_iccid_already, + R.string.download_wizard_error_suggest_profile_installed + ), + InsufficientMemory( + R.string.download_wizard_error_insufficient_memory, + R.string.download_wizard_error_suggest_insufficient_memory + ), + UnsupportedProfile( + R.string.download_wizard_error_unsupported_profile, + null + ), + CardInternalError( + R.string.download_wizard_error_card_internal_error, + null + ), + EIDNotSupported( + R.string.download_wizard_error_eid_not_supported, + R.string.download_wizard_error_suggest_contact_carrier + ), + EIDMismatch( + R.string.download_wizard_error_eid_mismatch, + R.string.download_wizard_error_suggest_contact_reissue + ), + UnreleasedProfile( + R.string.download_wizard_error_profile_unreleased, + R.string.download_wizard_error_suggest_contact_reissue + ), + MatchingIDRefused( + R.string.download_wizard_error_matching_id_refused, + R.string.download_wizard_error_suggest_contact_carrier + ), + ProfileRetriesExceeded( + R.string.download_wizard_error_profile_retries_exceeded, + R.string.download_wizard_error_suggest_contact_carrier + ), + ConfirmationCodeMissing( + R.string.download_wizard_error_confirmation_code_missing, + R.string.download_wizard_error_suggest_contact_carrier + ), + ConfirmationCodeRefused( + R.string.download_wizard_error_confirmation_code_refused, + R.string.download_wizard_error_suggest_contact_carrier + ), + ConfirmationCodeRetriesExceeded( + R.string.download_wizard_error_confirmation_code_retries_exceeded, + R.string.download_wizard_error_suggest_contact_carrier + ), + ProfileExpired( + R.string.download_wizard_error_profile_expired, + R.string.download_wizard_error_suggest_contact_carrier + ), + UnknownHost( + R.string.download_wizard_error_unknown_hostname, + null + ), + NetworkUnreachable( + R.string.download_wizard_error_network_unreachable, + R.string.download_wizard_error_suggest_network_unreachable + ), + TLSError( + R.string.download_wizard_error_tls_certificate, + null + ); + + companion object { + private val httpErrors = buildMap { + // Stage: AuthenticateClient + put("8.1" to "4.8", InsufficientMemory) + put("8.1.1" to "2.1", EIDNotSupported) + put("8.1.1" to "3.8", EIDMismatch) + put("8.2" to "1.2", UnreleasedProfile) + put("8.2.6" to "3.8", MatchingIDRefused) + put("8.8.5" to "6.4", ProfileRetriesExceeded) + + // Stage: GetBoundProfilePackage + put("8.2.7" to "2.2", ConfirmationCodeMissing) + put("8.2.7" to "3.8", ConfirmationCodeRefused) + put("8.2.7" to "6.4", ConfirmationCodeRetriesExceeded) + + // Stage: AuthenticateClient, GetBoundProfilePackage + put("8.8.5" to "4.10", ProfileExpired) + } + + fun fromDownloadError(exc: LocalProfileAssistant.ProfileDownloadException) = when { + exc.lpaErrorReason != "ES10B_ERROR_REASON_UNDEFINED" -> fromLPAErrorReason(exc.lpaErrorReason) + exc.lastHttpResponse?.rcode == 200 -> fromHTTPResponse(exc.lastHttpResponse!!) + exc.lastHttpException != null -> fromHTTPException(exc.lastHttpException!!) + exc.lastApduResponse != null -> fromAPDUResponse(exc.lastApduResponse!!) + else -> null + } + + private fun fromLPAErrorReason(reason: String) = when (reason) { + "ES10B_ERROR_REASON_UNSUPPORTED_CRT_VALUES" -> UnsupportedProfile + "ES10B_ERROR_REASON_UNSUPPORTED_REMOTE_OPERATION_TYPE" -> UnsupportedProfile + "ES10B_ERROR_REASON_UNSUPPORTED_PROFILE_CLASS" -> UnsupportedProfile + "ES10B_ERROR_REASON_INSTALL_FAILED_DUE_TO_ICCID_ALREADY_EXISTS_ON_EUICC" -> ICCIDAlreadyInUse + "ES10B_ERROR_REASON_INSTALL_FAILED_DUE_TO_INSUFFICIENT_MEMORY_FOR_PROFILE" -> InsufficientMemory + "ES10B_ERROR_REASON_INSTALL_FAILED_DUE_TO_INTERRUPTION" -> CardInternalError + "ES10B_ERROR_REASON_INSTALL_FAILED_DUE_TO_PE_PROCESSING_ERROR" -> CardInternalError + else -> null + } + + private fun fromHTTPResponse(httpResponse: net.typeblog.lpac_jni.HttpInterface.HttpResponse): SimplifiedErrorMessages? { + if (httpResponse.data.first().toInt() != '{'.code) return null + val response = JSONObject(httpResponse.data.decodeToString()) + val statusCodeData = response.optJSONObject("header") + ?.optJSONObject("functionExecutionStatus") + ?.optJSONObject("statusCodeData") + ?: return null + val subjectCode = statusCodeData.optString("subjectCode") + val reasonCode = statusCodeData.optString("reasonCode") + return httpErrors[subjectCode to reasonCode] + } + + private fun fromHTTPException(exc: Exception) = when (exc) { + is SSLException -> TLSError + is UnknownHostException -> UnknownHost + is NoRouteToHostException -> NetworkUnreachable + is PortUnreachableException -> NetworkUnreachable + is SocketTimeoutException -> NetworkUnreachable + is SocketException -> exc.message + ?.contains("Connection reset", ignoreCase = true) + ?.let { if (it) NetworkUnreachable else null } + + else -> null + } + + private fun fromAPDUResponse(resp: ByteArray): SimplifiedErrorMessages? { + val isSuccess = resp.size >= 2 && + resp[resp.size - 2] == 0x90.toByte() && + resp[resp.size - 1] == 0x00.toByte() + if (isSuccess) return null + return CardInternalError + } + } +} 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 d2d8c4b..b44bef8 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,6 +1,5 @@ package im.angry.openeuicc.util -import android.os.Build import android.os.Bundle import androidx.fragment.app.Fragment import im.angry.openeuicc.core.EuiccChannel @@ -10,7 +9,6 @@ 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 @@ -19,19 +17,12 @@ 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, - seId: EuiccChannel.SecureElementId, - addArguments: BundleSetter = {} -): T +fun newInstanceEuicc(clazz: Class, slotId: Int, portId: Int, 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() } @@ -44,18 +35,6 @@ 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 @@ -75,12 +54,7 @@ val T.euiccChannelManagerService: EuiccChannelManagerService suspend fun T.withEuiccChannel(fn: suspend (EuiccChannel) -> R): R where T : Fragment, T : EuiccChannelFragmentMarker { ensureEuiccChannelManager() - return euiccChannelManager.withEuiccChannel( - slotId, - portId, - seId, - fn - ) + return euiccChannelManager.withEuiccChannel(slotId, portId, 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 5f4aec4..2fef3db 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 @@ -5,6 +5,7 @@ import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import androidx.fragment.app.Fragment @@ -38,6 +39,7 @@ internal object PreferenceKeys { val IGNORE_TLS_CERTIFICATE = booleanPreferencesKey("ignore_tls_certificate") val EUICC_MEMORY_RESET = booleanPreferencesKey("euicc_memory_reset") val ISDR_AID_LIST = stringPreferencesKey("isdr_aid_list") + val ES10X_MSS = intPreferencesKey("es10x_mss") } const val EUICC_DEFAULT_ISDR_AID = "A0000005591010FFFFFFFF8900000100" @@ -50,9 +52,12 @@ internal object PreferenceConstants { # eUICC standard $EUICC_DEFAULT_ISDR_AID - # eSTK.me + # ESTKme AUX (deprecated, use SE0 instead) A06573746B6D65FFFFFFFF4953442D52 + # ESTKme SE0 + A06573746B6D65FFFF4953442D522030 + # eSIM.me A0000005591010000000008900000300 @@ -86,6 +91,7 @@ open class PreferenceRepository(private val context: Context) { PreferenceConstants.DEFAULT_AID_LIST, { Base64.getEncoder().encodeToString(it.encodeToByteArray()) }, { Base64.getDecoder().decode(it).decodeToString() }) + val es10xMssFlow = bindFlow(PreferenceKeys.ES10X_MSS, 63) protected fun bindFlow( key: Preferences.Key, 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 079853e..57d150b 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,73 +41,3 @@ 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 a73d7fe..c7c859d 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(R.string.no) { _, _ -> } - setPositiveButton(R.string.yes) { _, _ -> + setNegativeButton(android.R.string.cancel) { _, _ -> } + setPositiveButton(android.R.string.ok) { _, _ -> 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/download_progress_item.xml b/app-common/src/main/res/layout/download_progress_item.xml index f1d0852..c59673b 100644 --- a/app-common/src/main/res/layout/download_progress_item.xml +++ b/app-common/src/main/res/layout/download_progress_item.xml @@ -1,30 +1,32 @@ + android:layout_height="wrap_content"> + app:layout_constraintBottom_toBottomOf="@id/download_progress_icon_container" + app:layout_constraintEnd_toStartOf="@id/download_progress_icon_container" + app:layout_constraintHorizontal_bias="0.0" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@id/download_progress_icon_container" + app:layout_constraintVertical_bias="0.5" /> + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="0.0"> + + + + \ No newline at end of file diff --git a/app-common/src/main/res/layout/euicc_profile.xml b/app-common/src/main/res/layout/euicc_profile.xml index 58d55ab..021c53b 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 4ae7523..c5fde7b 100644 --- a/app-common/src/main/res/layout/fragment_euicc.xml +++ b/app-common/src/main/res/layout/fragment_euicc.xml @@ -27,6 +27,7 @@ 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 32f178a..99492d6 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 0e00292..c15663f 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 87f96a6..b80e06e 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 bde850f..f55c56c 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 6add53d..60722d6 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/profile_enable"/> + android:title="@string/profile_disable"/> + android:title="@string/profile_rename"/> + android:title="@string/profile_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 d51e2c7..b171972 100644 --- a/app-common/src/main/res/values-ja/strings.xml +++ b/app-common/src/main/res/values-ja/strings.xml @@ -2,26 +2,28 @@ このアプリでアクセスできるリムーバブル eUICC カードがデバイス上で検出されていません。互換性のあるカード挿入または USB リーダーを接続してください。 この eSIM にはプロファイルがありません。 - 不明 - 情報がありません - ヘルプ - スロットを再読み込み + ヘルプ + スロットを再読み込み + 不明 論理スロット %d - 有効済み - 無効済み - プロバイダー: + + 有効化済み + 無効化済み + プロバイダー: クラス: テスト中 プロビジョニング 稼働中 - 有効化 - 無効化 - 削除 - 名前を変更 - eSIM チップがプロファイルの切り替えの待機中にタイムアウトしました。これはデバイスのモデムファームウェアのバグの可能性があります。機内モードに切り替えるかアプリを再起動、デバイスを再起動してください。 - 操作は成功しましたが、デバイスのモデムが更新を拒否しました。新しいプロファイルを使用するには機内モードに切り替えるか、再起動する必要があります。 + 有効化 + 無効化 + 削除 + 名前を変更 + eSIM チップがプロファイルの切り替えの待機中にタイムアウトしました。これはデバイスのモデムファームウェアのバグの可能性があります。機内モードに切り替えるかアプリを再起動、デバイスを再起動してください。 + 操作は成功しましたが、デバイスのモデムが更新を拒否しました。新しいプロファイルを使用するには機内モードに切り替えるか、再起動する必要があります。 新しい eSIM プロファイルに切り替えることができません。 確認文字列が一致しません + 確認文字列が一致しません + このチップは消去されました ICCID をクリップボードにコピーしました シリアル番号をクリップボードにコピーしました EID をクリップボードにコピーしました @@ -38,14 +40,16 @@ eSIM プロファイルの削除に失敗しました eSIM プロファイルを切り替え中 eSIM プロファイルの切り替えに失敗しました + eSIM チップを消去中 + eSIM チップの消去に失敗しました 新しい eSIM サーバー (RSP / SM-DP+) アクティベーションコード 確認コード (オプション) 確認コード (必須) IMEI (オプション) - 残り容量が少ない - 残り容量が少ないため、ダウンロードに失敗する可能性があります。 + 残りの容量が少量です + 残り容量が少ないため、このプロファイルのダウンロードに失敗する可能性があります。 クリップボードに LPA コードがありません 解析できません QR コードまたはクリップボードの内容を LPA コードとして解析できませんでした。 @@ -83,6 +87,27 @@ 最終の APDU 例外: 保存 「%s」での診断 + この eSIM プロファイルはすでに eSIM チップに存在します。 + eSIM チップにはプロファイルをダウンロードするのに必要なメモリが残っていません。 + この eSIM プロファイルは、eSIM チップではサポートされていません。 + eSIM チップでエラーが発生しました。 + 使用しているデバイスまたは eSIM チップの EID は、通信事業者によってサポートされていません。 + この eSIM プロファイルは、別のデバイスにダウンロードされています。 + この eSIM プロファイルは取り消されました。 + アクティベーションコードが無効です。 + eSIM プロファイルのダウンロード試行回数の上限を超えました。 + このプロファイルをダウンロードするには確認コードが必要です。 + 入力した確認コードは無効です。 + この eSIM プロファイルは有効期限が切れています。 + 確認コードのダウンロード試行回数の上限を超えました。 + 不明な SM-DP+ アドレス + ネットワークにアクセスできません + TLS 証明書エラー、この eSIM プロファイルはサポートされていません + ダウンロード済みの eSIM プロファイルを再インストールしようとしています + 未使用の eSIM プロファイルをいくつか削除して、再度お試しください + サポートについては、通信事業者にお問い合わせください。 + この eSIM プロファイルを再発行するには、通信事業者にお問い合わせください。 + 別のネットワークに接続後 (例: Wi-Fi とデータを切り替え) を行った後に再度お試しください。 ログは共有したパスに保存されました。別のアプリで共有しますか? 新しいニックネーム ニックネームを UTF-8 にエンコードできませんでした @@ -96,8 +121,8 @@ eSIM プロファイルはダウンロードや削除、有効化や無効化されたときに通信事業者に通知を送信できます。送信されるこれらの通知のキューはここにリストされます。\n\n設定では、各タイプの通知を自動的に送信するかどうかを指定できます。通知が送信された場合でもキューのスペースが不足していない限り、記録から自動的に削除されることはありません。\n\nここでは保留中の各通知を手動で送信または削除できます。 ダウンロードしました 削除しました - 有効化しました - 無効化しました + 有効化済み + 無効化済み 処理 削除 eUICC 情報 @@ -110,21 +135,33 @@ 製品ファームウェアバージョン SGP.22 バージョン eUICC OS バージョン - グローバルプラットフォームのバージョン + グローバルプラットフォームのバージョン SAS 認定番号 保護されたプロファイルのバージョン NVRAM の空き容量 (eSIM プロファイルストレージ) + (参照用) 証明書発行者 (CI) GSMA ライブ CI GSMA テスト CI 不明な eSIM CI - はい - いいえ + eUICC を消去 + eUICC を消去 + このチップ上のすべてのプロファイルを削除することを確認してください。この操作は元に戻せないことを理解してください。\n\nEID: %1$s\n\n%2$s + 確認のために「%s」を入力してください + EID が %s で終わるチップを消去することを確認し、この操作は元に戻せないことを理解してください + 消去 + + はい + いいえ + 不明 + 情報がありません 保存 %s のログ 開発者になるまであと %d ステップです。 あなたは開発者になりました! - カスタム ISD-R AID リストが保存されました + ISD-R AID リスト + カスタム ISD-R AID リストを保存しました。 + リセット 設定 通知 eSIM のプロファイル操作により、通信事業者に通知が送信されます。必要に応じてこの動作を微調整できます。 @@ -136,7 +173,7 @@ プロファイルを切り替え中の通知を送信します\nこのタイプの通知は信頼できないことに注意してください。 高度な設定 プロファイルの無効化と削除を許可 - デフォルトでは、このアプリでデバイスに挿入された取り外し可能な eSIM の有効なプロファイルを無効化することを防いでいます。なぜなのかというと時々アクセスができなくなるからです。\nこのチェックボックスを ON にすることで、この保護機能を解除します。 + デフォルトでは、このアプリでデバイスに挿入されたリムーバブル eSIM の有効なプロファイルを無効化することを防いでいます。なぜなのかというと時々アクセスができなくなるからです。\nこのチェックボックスを ON にすることで、この保護機能を解除します。 詳細ログ 詳細ログを有効化します。これには個人的な情報が含まれている可能性があります。この機能を ON にした後は、信頼できるユーザーとのみログを共有してください。 言語 @@ -144,29 +181,22 @@ ログ アプリの最新デバッグログを表示します 開発者オプション - プロファイルを切り替えた後にモデムに更新コマンドを送信するかどうか。クラッシュが発生する場合は、これを無効にしてみてください。 + モデムに更新コマンドを送信 + プロファイルを切り替えた後にモデムに更新コマンドを送信するかどうかを設定します。クラッシュが発生する場合は、この機能を無効化してください。 フィルタリングされていないプロファイル一覧を表示 非運用のプロファイルも含めます SM-DP+ TLS 証明書を無視する RSP サーバーで使用される TLS 証明書を受け入れます - 一部のブランドの取り外し可能な eUICC では、独自の非標準 ISD-R AID が使用されている場合があり、サードパーティ アプリからアクセスできなくなります。アプリはこのリストに追加された非標準の AID の使用を試みる可能性がありますが、動作することは保証されません。 + eUICC の消去を許可 + これは危険な操作であり、デフォルトでは非表示になっています。代わりとしてすべてのプロファイルを手動で削除することもできます。 + グローバル ES10x MSS + + 高速 + 互換モード + + ISD-R AID リストをカスタマイズ + 一部ブランドのリムーバブル eUICC は独自の非標準な ISD-R AID を使用しているため、サードパーティー製アプリからアクセスできない場合があります。このリストに追加された非標準な AID の使用を試みますが、動作の保証はできません。 情報 アプリバージョン ソースコード - 確認文字列が一致しません - このチップは消去されました - eSIM チップを消去しています - eSIM チップの消去は失敗しました - eSIM を消去する - eSIM を消去する - このチップ内のすべてのプロファイルを削除することをご確認してください。この操作は元に戻せないことをご理解してください。\n\nEID: %1$s\n\n%2$s - 確認のため、ここに「%s」を入力してください - EID が %s で終わるチップを消去することに同意します。これは元に戻せないことを理解しています。 - 消去する - eUICC の消去を可能にする - この操作は、デフォルトでは非表示になっている危険な操作です。代わりに、すべての構成ファイルを手動で削除することもできます。 - モデムに更新コマンドを送信 - 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 32ced90..7303d2d 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,21 @@ 在此设备上未检测到此应用程序可访问的可插拔 eUICC 卡。请插入兼容卡或 USB 读卡器。 此 eSIM 上还没有配置文件 - 未知 - 帮助 - 重新加载卡槽 + 未知 + 帮助 + 重新加载卡槽 + 未知 逻辑卡槽 %d - 已启用 - 已禁用 - 提供商: + 已启用 + 已禁用 + 提供商: 类型: - 启用 - 禁用 - 删除 - 重命名 - 等待 eSIM 芯片切换配置文件时超时。这可能是您手机基带固件中的一个错误。请尝试切换飞行模式、重新启动应用程序或重新启动手机 - 操作成功, 但是您手机的基带拒绝刷新。您可能需要切换飞行模式或重新启动,以便使用新的配置文件。 + 启用 + 禁用 + 删除 + 重命名 + 等待 eSIM 芯片切换配置文件时超时。这可能是您手机基带固件中的一个错误。请尝试切换飞行模式、重新启动应用程序或重新启动手机 + 操作成功, 但是您手机的基带拒绝刷新。您可能需要切换飞行模式或重新启动,以便使用新的配置文件。 无法切换到新的 eSIM 配置文件。 输入的确认文本不匹配 已复制 ICCID 到剪贴板 @@ -46,6 +47,7 @@ IMEI (可选) 剩余空间不足 当前芯片的剩余空间不足,可能导致配置下载失败。\n是否继续下载? + 请连接到其他网络(例如在 Wi-Fi 和数据之间切换)后重试。 日志已保存到指定路径。需要通过其他 App 分享吗? 新昵称 无法将昵称编码为 UTF-8 @@ -83,6 +85,11 @@ 日志 查看应用程序的最新调试日志 某些品牌的可移除 eUICC 可能会使用自己的非标准 ISD-R AID,导致第三方应用无法访问。此 App 可以尝试使用此列表中添加的非标准 AID,但不能保证它们一定有效。 + 全局 ES10x MSS + + 最佳效率 + 最佳兼容性 + 信息 App 版本 源码 @@ -132,16 +139,17 @@ 可插拔 SGP.22 版本 eUICC OS 版本 - GlobalPlatform 版本 + GlobalPlatform 版本 SAS 认证号码 Protected Profile 版本 NVRAM 剩余空间 (eSIM 存储容量) + (仅供参考) 证书签发者 (CI) GSMA 生产环境 CI GSMA 测试 CI 未知 eSIM CI - - + + 还有 %d 步成为开发者 你现在是开发者了! 语言 @@ -152,7 +160,7 @@ 在配置文件列表中包括非生产环境的配置文件 无视 SM-DP+ 的 TLS 证书 允许 RSP 服务器使用任意证书 - 无信息 + 无信息 输入的确认文本不匹配 此芯片已被擦除 正在擦除 eSIM 芯片 @@ -167,6 +175,26 @@ 此操作是默认隐藏的危险操作。作为替代方案,您可以手动删除所有配置文件。 向基带发送刷新命令 自定义 ISD-R AID 列表 - 重置 + 重置 ISD-R AID 列表 + 此 eSIM 配置文件已存在于您的 eSIM 芯片上。 + 您的 eSIM 芯片没有足够的空间来下载配置文件。 + 您的 eSIM 芯片不支持此 eSIM 配置文件。 + eSIM 芯片错误。 + 您的设备或 eSIM 芯片的 EID 不受您的运营商支持。 + 此 eSIM 配置文件已被下载到另一台设备上。 + 此 eSIM 配置文件已被撤销。 + 激活码无效。 + 已超出 eSIM 配置文件的最大下载尝试次数。 + 下载此配置文件需要确认码。 + 您输入的确认码无效。 + 此 eSIM 配置文件已过期。 + 已超出确认码的最大下载尝试次数。 + 未知的 SM-DP+ 地址 + 网络不可达 + TLS 证书错误,不支持此 eSIM 配置文件 + 您正在尝试重新安装已下载的 eSIM 配置文件 + 请删除一些未使用的 eSIM 配置文件,然后重试 + 请联系您的运营商寻求帮助。 + 请联系您的运营商重新签发此 eSIM 配置文件。 \ 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 5136bf7..ef6c842 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,21 @@ 在此裝置上未檢測到此應用程式可訪問的可插拔 eUICC 卡。請插入相容卡或 USB 晶片讀卡機。 此 eSIM 上還沒有設定檔 - 未知 - 幫助 - 重新載入卡槽 + 未知 + 幫助 + 重新載入卡槽 + 未知 虛擬卡槽 %d - 已啟用 - 已停用 - 電信業者: + 已啟用 + 已停用 + 電信業者: 類型: - 啟用 - 停用 - 刪除 - 重新命名 - 等待 eSIM 切換設定檔時逾時。這可能是您手機基頻處理器韌體中的一個錯誤。請嘗試切換飛航模式、重新啟動應用程式或重新啟動手機 - 操作成功, 但是您手機的基頻處理器沒有重新整理。您可能需要切換飛航模式或重新啟動,以便使用新的設定檔。 + 啟用 + 停用 + 刪除 + 重新命名 + 等待 eSIM 切換設定檔時逾時。這可能是您手機基頻處理器韌體中的一個錯誤。請嘗試切換飛航模式、重新啟動應用程式或重新啟動手機 + 操作成功, 但是您手機的基頻處理器沒有重新整理。您可能需要切換飛航模式或重新啟動,以便使用新的設定檔。 無法切換到新的 eSIM 設定檔。 輸入的確認文字不匹配 已複製 ICCID 到剪貼簿 @@ -46,6 +47,7 @@ IMEI (可選) 剩餘空間不足 目前晶片的剩餘空間不足,可能導致配置下載失敗。\n是否繼續下載? + 請連接到其他網路(例如在 Wi-Fi 和資料之間切換)後重試。 日誌已儲存到指定路徑。需要透過其他 App 分享嗎? 新名稱 無法將名稱編碼為 UTF-8 @@ -83,6 +85,7 @@ 允許 停用/刪除 已啟用的設定檔 預設情況下,此應用程式會阻止您停用可插拔 eSIM 中已啟用的設定檔。\n因為這樣做 有時 會導致無法存取。\n勾選此框以 移除 此保護措施。 某些品牌的可移除 eUICC 可能會使用自己的非標準 ISD-R AID,導致第三方應用程式無法存取。此 App 可以嘗試使用此清單中新增的非標準 AID,但不能保證它們一定有效。 + 全局 ES10x MSS 資訊 App 版本 原始碼 @@ -132,16 +135,17 @@ 可插拔 SGP.22 版本 eUICC OS 版本 - GlobalPlatform 版本 + GlobalPlatform 版本 SAS 認證號碼 Protected Profile 版本 NVRAM 剩餘空間 (eSIM 儲存容量) + (僅供參考) 證書簽發者 (CI) GSMA 生產環境 CI GSMA 測試 CI 未知 eSIM CI - - + + 還有 %d 步成為開發者 您現在是開發者了! 語言 @@ -152,7 +156,7 @@ 在設定檔列表中包括非生產環境的設定檔 忽略 SM-DP+ 的 TLS 證書 允許 RSP 伺服器使用任意證書 - 無資訊 + 無資訊 輸入的確認文字不匹配 此晶片已被擦除 正在擦除 eSIM 晶片 @@ -167,6 +171,26 @@ 此操作是預設隱藏的危險操作。作為替代方案,您可以手動刪除所有設定檔。 向基帶發送刷新命令 自訂 ISD-R AID 列表 - 重置 + 重置 ISD-R AID 列表 + 此 eSIM 設定檔已存在於您的 eSIM 晶片上。 + 您的 eSIM 晶片沒有足夠的空間來下載設定檔。 + 您的 eSIM 晶片不支援此 eSIM 設定檔。 + eSIM 晶片錯誤。 + 您的裝置或 eSIM 晶片的 EID 不受您的電信業者支援。 + 此 eSIM 設定檔已被下載到另一台裝置上。 + 此 eSIM 設定檔已被撤銷。 + 啟用碼無效。 + 已超出 eSIM 設定檔的最大下載嘗試次數。 + 下載此設定檔需要確認碼。 + 您輸入的確認碼無效。 + 此 eSIM 設定檔已過期。 + 已超出確認碼的最大下載嘗試次數。 + 未知的 SM-DP+ 位址 + 網路不可達 + TLS 憑證錯誤,不支援此 eSIM 設定檔 + 您正在嘗試重新安裝已下載的 eSIM 設定文件 + 請刪除一些未使用的 eSIM 設定文件,然後重試 + 請聯絡您的電信業者尋求協助。 + 請聯絡您的電信業者重新簽發此 eSIM 設定檔。 \ 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 05ca15a..39a762f 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -2,31 +2,34 @@ 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. - Unknown - Information Unavailable - Help - Reload Slots + + Help + + Reload Slots + Unknown Logical Slot %d - USB - OpenMobile API (OMAPI) + USB + OpenMobile API (OMAPI) - Enabled - Disabled - Provider: + + Enabled + Disabled + Provider: Class: Testing Provisioning Operational - ICCID: + ICCID: + #%d - 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 @@ -101,6 +104,27 @@ Last APDU exception: Save Diagnostics at %s + This eSIM profile is already present on your eSIM chip. + Your eSIM chip does not have sufficient memory left to download the profile. + This eSIM profile is unsupported by your eSIM chip. + An error occurred in your eSIM chip. + The EID of your device or eSIM chip is unsupported by your carrier. + This eSIM profile has been downloaded on another device. + This eSIM profile has been revoked. + The activation code is invalid. + The maximum number of download attempts for the eSIM profile has been exceeded. + Confirmation code is required to download this profile. + The confirmation code you entered is invalid. + This eSIM profile has expired. + The maximum number of download attempts for the confirmation code has been exceeded. + Unknown SM-DP+ address + Network is unreachable + TLS certificate error, this eSIM profile is not supported + You are trying to reinstall an already downloaded eSIM profile + Please delete some unused eSIM profiles and try again + Please contact your carrier for assistance. + Please contact your carrier to reissue this eSIM profile. + Please try again after connecting to a different network (e.g. switching between Wi-Fi and data). Logs have been saved to the selected path. Would you like to share the log through another app? @@ -137,10 +161,11 @@ ISD-R AID SGP.22 Version eUICC OS Version - GlobalPlatform Version + GlobalPlatform Version SAS Accreditation Number Protected Profile Version Free NVRAM (eSIM profile storage) + (for reference only) Certificate Issuer (CI) GSMA Live CI GSMA Test CI @@ -154,8 +179,11 @@ I CONFIRM TO ERASE THE CHIP WHOSE EID ENDS WITH %s AND UNDERSTAND THAT THIS IS IRREVERSIBLE Erase - Yes - No + + Yes + No + Unknown + Information Unavailable Save Logs at %s @@ -163,10 +191,9 @@ 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 @@ -195,6 +222,16 @@ Accept any TLS certificate used by the RSP server Allow erasing eUICC This is a dangerous operation and hidden by default. As an alternative, you can delete all profiles manually. + ES10x MSS + Global ES10x MSS + + High Efficiency + Most Compatible + + + 255 + 63 + Customize ISD-R AID list Some brands of removable eUICCs may use their own non-standard ISD-R AID, rendering them inaccessible to third-party apps. We can attempt to use non-standard AIDs added in this list, but there is no guarantee that they will work. Info diff --git a/app-common/src/main/res/xml/pref_settings.xml b/app-common/src/main/res/xml/pref_settings.xml index 690a120..831b04d 100644 --- a/app-common/src/main/res/xml/pref_settings.xml +++ b/app-common/src/main/res/xml/pref_settings.xml @@ -81,6 +81,14 @@ app:summary="@string/pref_developer_euicc_memory_reset_desc" app:title="@string/pref_developer_euicc_memory_reset" /> + + + android:label="@string/quick_compatibility" /> 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 new file mode 100644 index 0000000..d5e599f --- /dev/null +++ b/app-unpriv/src/main/java/im/angry/openeuicc/ui/QuickCompatibilityActivity.kt @@ -0,0 +1,24 @@ +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 new file mode 100644 index 0000000..9b41730 --- /dev/null +++ b/app-unpriv/src/main/java/im/angry/openeuicc/ui/QuickCompatibilityFragment.kt @@ -0,0 +1,186 @@ +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