diff --git a/.forgejo/workflows/build-debug.yml b/.forgejo/workflows/build-debug.yml
index 0818b8b..51e802c 100644
--- a/.forgejo/workflows/build-debug.yml
+++ b/.forgejo/workflows/build-debug.yml
@@ -1,7 +1,7 @@
on:
push:
branches:
- - '*'
+ - 'master'
jobs:
build-debug:
@@ -33,23 +33,14 @@ jobs:
uses: https://gitea.angry.im/actions/setup-android@v3
- name: Build Debug APKs
- run: ./gradlew --no-daemon assembleDebug :app:assembleDebugMagiskModuleDir
+ run: ./gradlew --no-daemon assembleDebug
- name: Copy Artifacts
- run: |
- find . -name 'app*-debug.apk' -exec cp {} . \;
- cp -r app/build/magisk/debug ./magisk-debug
+ run: find . -name 'app*-debug.apk' -exec cp {} . \;
- - name: Upload APK Artifacts
+ - name: Upload 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 863f185..f888959 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,3 @@
[submodule "libs/lpac-jni/src/main/jni/lpac"]
path = libs/lpac-jni/src/main/jni/lpac
- url = https://github.com/estkme-group/lpac.git
+ url = https://github.com/estkme/lpac
diff --git a/.idea/.gitignore b/.idea/.gitignore
index d2293f6..0d51aca 100644
--- a/.idea/.gitignore
+++ b/.idea/.gitignore
@@ -1,7 +1,13 @@
-*
-!/codeStyles/Project.xml
-!/codeStyles/codeStyleConfig.xml
-!/vcs.xml
-!/kotlinc.xml
-!/compiler.xml
-!/migrations.xml
+/shelf
+/caches
+/libraries
+/assetWizardSettings.xml
+/deploymentTargetDropDown.xml
+/gradle.xml
+/misc.xml
+/modules.xml
+/navEditor.xml
+/runConfigurations.xml
+/workspace.xml
+
+**/*.iml
\ No newline at end of file
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index 7643783..4bec4ea 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -1,8 +1,5 @@
-
-
-
@@ -116,8 +113,5 @@
-
-
-
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
index 6e6eec1..a55e7a1 100644
--- a/.idea/codeStyles/codeStyleConfig.xml
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -1,6 +1,5 @@
-
\ No newline at end of file
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
new file mode 100644
index 0000000..8096d6c
--- /dev/null
+++ b/.idea/deploymentTargetSelector.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index 148fdd2..e805548 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 8a67e41..f8019b2 100644
--- a/README.md
+++ b/README.md
@@ -2,26 +2,18 @@
A fully free and open-source Local Profile Assistant implementation for Android devices.
-There are two variants of this project, OpenEUICC and EasyEUICC:
+There are two variants of this project:
-| | 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 and Magisk modules can be found in the [CI page](https://gitea.angry.im/PeterCxy/OpenEUICC/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`.
+- OpenEUICC: The full-fledged privileged variant.
+ - Due to its privilege requirement, OpenEUICC must be placed inside `/system/priv-app` and be signed with the platform certificate.
+ - The preferred way to including OpenEUICC in a system image is to [build it along with AOSP](#building-aosp).
+ - __Note__: 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.
+- EasyEUICC: Unprivileged version that can run as a user app.
+ - This version supports two modes of operation:
+ 1. Inserted, removable eSIMs: Due to obvious security requirements, EasyEUICC is only able to access eSIM chips whose [ARF/ARA](https://source.android.com/docs/core/connect/uicc#arf) contains the hash of EasyEUICC's signing certificate.
+ 2. USB CCID Card Readers: Only `T=0` readers that use the standard [USB CCID protocol](https://en.wikipedia.org/wiki/CCID_(protocol)) are supported. In this mode, EasyEUICC can access any eSIM chip loaded in the card reader regardless of their ARF/ARA, as long as they implement the [SGP.22 standard](https://www.gsma.com/solutions-and-impact/technologies/esim/wp-content/uploads/2021/07/SGP.22-v2.3.pdf).
+ - Prebuilt release-mode EasyEUICC apks can be downloaded [here](https://gitea.angry.im/PeterCxy/OpenEUICC/releases)
+ - 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.
@@ -78,7 +70,10 @@ FAQs
===
- Q: Do you provide prebuilt binaries for OpenEUICC?
-- A: Debug-mode APKs and Magisk modules 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.
+- 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.
+
+- 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: 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.
diff --git a/app-common/src/main/AndroidManifest.xml b/app-common/src/main/AndroidManifest.xml
index 44c82c0..f53e6ff 100644
--- a/app-common/src/main/AndroidManifest.xml
+++ b/app-common/src/main/AndroidManifest.xml
@@ -7,7 +7,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:label="@string/download_wizard" />
get() = (0.. EuiccChannel?): EuiccChannel? {
- val isdrAidList =
- parseIsdrAidList(appContainer.preferenceRepository.isdrAidListFlow.first())
-
- return isdrAidList.firstNotNullOfOrNull {
- Log.i(TAG, "Opening channel, trying ISDR AID ${it.encodeHex()}")
-
- openFn(it)?.let { channel ->
- if (channel.valid) {
- channel
- } else {
- channel.close()
- null
- }
- }
- }
- }
-
private suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel? {
lock.withLock {
if (port.card.physicalSlotIndex == EuiccChannelManager.USB_CHANNEL_ID) {
@@ -96,10 +75,9 @@ open class DefaultEuiccChannelManager(
return null
}
- val channel =
- tryOpenChannelFirstValidAid { euiccChannelFactory.tryOpenEuiccChannel(port, it) }
+ val channel = euiccChannelFactory.tryOpenEuiccChannel(port) ?: return null
- if (channel != null) {
+ if (channel.valid) {
channelCache.add(channel)
return channel
} else {
@@ -107,6 +85,7 @@ open class DefaultEuiccChannelManager(
TAG,
"Was able to open channel for logical slot ${port.logicalSlotIndex}, but the channel is invalid (cannot get eID or profiles without errors). This slot might be broken, aborting."
)
+ channel.close()
return null
}
}
@@ -232,10 +211,7 @@ open class DefaultEuiccChannelManager(
check(channel.valid) { "Invalid channel" }
break
} catch (e: Exception) {
- Log.d(
- TAG,
- "Slot $physicalSlotId port $portId reconnect failure, retrying in 1000 ms"
- )
+ Log.d(TAG, "Slot $physicalSlotId port $portId reconnect failure, retrying in 1000 ms")
}
delay(1000)
}
@@ -268,23 +244,14 @@ open class DefaultEuiccChannelManager(
withContext(Dispatchers.IO) {
usbManager.deviceList.values.forEach { device ->
Log.i(TAG, "Scanning USB device ${device.deviceId}:${device.vendorId}")
- val iface = device.interfaces.smartCard ?: return@forEach
+ val iface = device.getSmartCardInterface() ?: return@forEach
// If we don't have permission, tell UI code that we found a candidate device, but we
// need permission to be able to do anything with it
if (!usbManager.hasPermission(device)) return@withContext Pair(device, false)
- Log.i(
- TAG,
- "Found CCID interface on ${device.deviceId}:${device.vendorId}, and has permission; trying to open channel"
- )
-
- val ccidCtx = UsbCcidContext.createFromUsbDevice(context, device, iface) ?: return@forEach
-
+ Log.i(TAG, "Found CCID interface on ${device.deviceId}:${device.vendorId}, and has permission; trying to open channel")
try {
- val channel = tryOpenChannelFirstValidAid {
- euiccChannelFactory.tryOpenUsbEuiccChannel(ccidCtx, it)
- }
+ val channel = euiccChannelFactory.tryOpenUsbEuiccChannel(device, iface)
if (channel != null && channel.lpa.valid) {
- ccidCtx.allowDisconnect = true
usbChannel = channel
return@withContext Pair(device, true)
}
@@ -292,14 +259,7 @@ open class DefaultEuiccChannelManager(
// Ignored -- skip forward
e.printStackTrace()
}
-
- ccidCtx.allowDisconnect = true
- ccidCtx.disconnect()
-
- Log.i(
- TAG,
- "No valid eUICC channel found on USB device ${device.deviceId}:${device.vendorId}"
- )
+ Log.i(TAG, "No valid eUICC channel found on USB device ${device.deviceId}:${device.vendorId}")
}
return@withContext Pair(null, false)
}
diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt
index b20932f..5f399ea 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,6 @@
package im.angry.openeuicc.core
import im.angry.openeuicc.util.*
-import net.typeblog.lpac_jni.ApduInterface
import net.typeblog.lpac_jni.LocalProfileAssistant
interface EuiccChannel {
@@ -29,15 +28,5 @@ interface EuiccChannel {
*/
val intrinsicChannelName: String?
- /**
- * The underlying APDU interface for this channel
- */
- val apduInterface: ApduInterface
-
- /**
- * The AID of the ISD-R channel currently in use
- */
- val isdrAid: ByteArray
-
fun close()
}
\ No newline at end of file
diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelFactory.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelFactory.kt
index ba587a6..fb5d95d 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
@@ -1,17 +1,15 @@
package im.angry.openeuicc.core
-import im.angry.openeuicc.core.usb.UsbCcidContext
+import android.hardware.usb.UsbDevice
+import android.hardware.usb.UsbInterface
import im.angry.openeuicc.util.*
// This class is here instead of inside DI because it contains a bit more logic than just
// "dumb" dependency injection.
interface EuiccChannelFactory {
- suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat, isdrAid: ByteArray): EuiccChannel?
+ suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel?
- fun tryOpenUsbEuiccChannel(
- ccidCtx: UsbCcidContext,
- isdrAid: ByteArray
- ): EuiccChannel?
+ fun tryOpenUsbEuiccChannel(usbDevice: UsbDevice, usbInterface: UsbInterface): EuiccChannel?
/**
* Release all resources used by this EuiccChannelFactory
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 eaec522..a82cb97 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,9 +1,7 @@
package im.angry.openeuicc.core
-import im.angry.openeuicc.util.UiccPortInfoCompat
+import im.angry.openeuicc.util.*
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
@@ -13,24 +11,16 @@ class EuiccChannelImpl(
override val type: String,
override val port: UiccPortInfoCompat,
override val intrinsicChannelName: String?,
- override val apduInterface: ApduInterface,
- override val isdrAid: ByteArray,
+ private val apduInterface: ApduInterface,
verboseLoggingFlow: Flow,
- ignoreTLSCertificateFlow: Flow,
- es10xMssFlow: Flow,
+ ignoreTLSCertificateFlow: Flow
) : EuiccChannel {
override val slotId = port.card.physicalSlotIndex
override val logicalSlotId = port.logicalSlotIndex
override val portId = port.portIndex
override val lpa: LocalProfileAssistant =
- LocalProfileAssistantImpl(
- isdrAid,
- apduInterface,
- HttpInterfaceImpl(verboseLoggingFlow, ignoreTLSCertificateFlow),
- ).also {
- it.setEs10xMss(runBlocking { es10xMssFlow.first().toByte() })
- }
+ LocalProfileAssistantImpl(apduInterface, HttpInterfaceImpl(verboseLoggingFlow, ignoreTLSCertificateFlow))
override val atr: ByteArray?
get() = (apduInterface as? ApduInterfaceAtrProvider)?.atr
diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt
index 361a943..4204e82 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
@@ -1,7 +1,6 @@
package im.angry.openeuicc.core
import im.angry.openeuicc.util.*
-import net.typeblog.lpac_jni.ApduInterface
import net.typeblog.lpac_jni.LocalProfileAssistant
class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel {
@@ -34,12 +33,8 @@ class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel {
get() = channel.valid
override val intrinsicChannelName: String?
get() = channel.intrinsicChannelName
- override val apduInterface: ApduInterface
- get() = channel.apduInterface
override val atr: ByteArray?
get() = channel.atr
- override val isdrAid: ByteArray
- get() = channel.isdrAid
override fun close() = channel.close()
diff --git a/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt b/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt
index b715ca0..cd62fca 100644
--- a/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt
@@ -45,8 +45,9 @@ class LocalProfileAssistantWrapper(orig: LocalProfileAssistant) :
matchingId: String?,
imei: String?,
confirmationCode: String?,
+ preview: Boolean,
callback: ProfileDownloadCallback
- ) = lpa.downloadProfile(smdp, matchingId, imei, confirmationCode, callback)
+ ) = lpa.downloadProfile(smdp, matchingId, imei, confirmationCode, preview, callback)
override fun deleteNotification(seqNumber: Long): Boolean = lpa.deleteNotification(seqNumber)
diff --git a/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt b/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt
index f918494..c70669d 100644
--- a/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt
@@ -7,9 +7,9 @@ import android.util.Log
import im.angry.openeuicc.util.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.single
import kotlinx.coroutines.runBlocking
import net.typeblog.lpac_jni.ApduInterface
-import java.util.concurrent.atomic.AtomicInteger
class OmapiApduInterface(
private val service: SEService,
@@ -21,8 +21,7 @@ class OmapiApduInterface(
}
private lateinit var session: Session
- private val index = AtomicInteger(0)
- private val channels = mutableMapOf()
+ private lateinit var lastChannel: Channel
override val valid: Boolean
get() = service.isConnected && (this::session.isInitialized && !session.isClosed)
@@ -39,23 +38,24 @@ class OmapiApduInterface(
}
override fun logicalChannelOpen(aid: ByteArray): Int {
- val channel = session.openLogicalChannel(aid)
- check(channel != null) { "Failed to open logical channel (${aid.encodeHex()})" }
- val handle = index.incrementAndGet()
- synchronized(channels) { channels[handle] = channel }
- return handle
+ check(!this::lastChannel.isInitialized) {
+ "Can only open one channel"
+ }
+ lastChannel = session.openLogicalChannel(aid)!!
+ return 1
}
override fun logicalChannelClose(handle: Int) {
- val channel = channels[handle]
- check(channel != null) { "Invalid logical channel handle $handle" }
- if (channel.isOpen) channel.close()
- synchronized(channels) { channels.remove(handle) }
+ check(handle == 1 && !this::lastChannel.isInitialized) {
+ "Unknown channel"
+ }
+ lastChannel.close()
}
- override fun transmit(handle: Int, tx: ByteArray): ByteArray {
- val channel = channels[handle]
- check(channel != null) { "Invalid logical channel handle $handle" }
+ override fun transmit(tx: ByteArray): ByteArray {
+ check(this::lastChannel.isInitialized) {
+ "Unknown channel"
+ }
if (runBlocking { verboseLoggingFlow.first() }) {
Log.d(TAG, "OMAPI APDU: ${tx.encodeHex()}")
@@ -63,7 +63,7 @@ class OmapiApduInterface(
try {
for (i in 0..10) {
- val res = channel.transmit(tx)
+ val res = lastChannel.transmit(tx)
if (runBlocking { verboseLoggingFlow.first() }) {
Log.d(TAG, "OMAPI APDU response: ${res.encodeHex()}")
}
diff --git a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbApduInterface.kt b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbApduInterface.kt
index 4a4ccb9..624ef89 100644
--- a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbApduInterface.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbApduInterface.kt
@@ -1,41 +1,56 @@
package im.angry.openeuicc.core.usb
+import android.hardware.usb.UsbDeviceConnection
+import android.hardware.usb.UsbEndpoint
import android.util.Log
import im.angry.openeuicc.core.ApduInterfaceAtrProvider
import im.angry.openeuicc.util.*
+import kotlinx.coroutines.flow.Flow
import net.typeblog.lpac_jni.ApduInterface
class UsbApduInterface(
- private val ccidCtx: UsbCcidContext
+ private val conn: UsbDeviceConnection,
+ private val bulkIn: UsbEndpoint,
+ private val bulkOut: UsbEndpoint,
+ private val verboseLoggingFlow: Flow
) : ApduInterface, ApduInterfaceAtrProvider {
companion object {
private const val TAG = "UsbApduInterface"
}
- override val atr: ByteArray?
- get() = ccidCtx.atr
+ private lateinit var ccidDescription: UsbCcidDescription
+ private lateinit var transceiver: UsbCcidTransceiver
- override val valid: Boolean
- get() = channels.isNotEmpty()
+ private var channelId = -1
- private var channels = mutableSetOf()
+ override var atr: ByteArray? = null
override fun connect() {
- ccidCtx.connect()
+ ccidDescription = UsbCcidDescription.fromRawDescriptors(conn.rawDescriptors)!!
- // Send Terminal Capabilities
- // Specs: ETSI TS 102 221 v15.0.0 - 11.1.19 TERMINAL CAPABILITY
- val terminalCapabilities = buildCmd(
- 0x80.toByte(), 0xaa.toByte(), 0x00, 0x00,
- "A9088100820101830107".decodeHex(),
- le = null,
- )
- transmitApduByChannel(terminalCapabilities, 0)
+ if (!ccidDescription.hasT0Protocol) {
+ throw IllegalArgumentException("Unsupported card reader; T=0 support is required")
+ }
+
+ transceiver = UsbCcidTransceiver(conn, bulkIn, bulkOut, ccidDescription, verboseLoggingFlow)
+
+ try {
+ // 6.1.1.1 PC_to_RDR_IccPowerOn (Page 20 of 40)
+ // https://www.usb.org/sites/default/files/DWG_Smart-Card_USB-ICC_ICCD_rev10.pdf
+ atr = transceiver.iccPowerOn().data
+ } catch (e: Exception) {
+ e.printStackTrace()
+ throw e
+ }
}
- override fun disconnect() = ccidCtx.disconnect()
+ override fun disconnect() {
+ conn.close()
+ }
override fun logicalChannelOpen(aid: ByteArray): Int {
+ check(channelId == -1) { "Logical channel already opened" }
+
// OPEN LOGICAL CHANNEL
val req = manageChannelCmd(true, 0)
@@ -51,7 +66,7 @@ class UsbApduInterface(
return -1
}
- val channelId = resp[0].toInt()
+ channelId = resp[0].toInt()
Log.d(TAG, "channelId = $channelId")
// Then, select AID
@@ -63,32 +78,32 @@ class UsbApduInterface(
return -1
}
- channels.add(channelId)
-
return channelId
}
override fun logicalChannelClose(handle: Int) {
- check(channels.contains(handle)) {
- "Invalid logical channel handle $handle"
- }
+ check(handle == channelId) { "Logical channel ID mismatch" }
+ check(channelId != -1) { "Logical channel is not opened" }
+
// CLOSE LOGICAL CHANNEL
- val req = manageChannelCmd(false, handle.toByte())
- val resp = transmitApduByChannel(req, handle.toByte())
+ val req = manageChannelCmd(false, channelId.toByte())
+ val resp = transmitApduByChannel(req, channelId.toByte())
if (!isSuccessResponse(resp)) {
Log.d(TAG, "CLOSE LOGICAL CHANNEL failed: ${resp.encodeHex()}")
}
- channels.remove(handle)
+
+ channelId = -1
}
- override fun transmit(handle: Int, tx: ByteArray): ByteArray {
- check(channels.contains(handle)) {
- "Invalid logical channel handle $handle"
- }
- return transmitApduByChannel(tx, handle.toByte())
+ override fun transmit(tx: ByteArray): ByteArray {
+ check(channelId != -1) { "Logical channel is not opened" }
+ return transmitApduByChannel(tx, channelId.toByte())
}
+ override val valid: Boolean
+ get() = channelId != -1
+
private fun isSuccessResponse(resp: ByteArray): Boolean =
resp.size >= 2 && resp[resp.size - 2] == 0x90.toByte() && resp[resp.size - 1] == 0x00.toByte()
@@ -122,7 +137,7 @@ class UsbApduInterface(
// OR the channel mask into the CLA byte
realTx[0] = ((realTx[0].toInt() and 0xFC) or channel.toInt()).toByte()
- var resp = ccidCtx.transceiver.sendXfrBlock(realTx).data!!
+ var resp = transceiver.sendXfrBlock(realTx).data!!
if (resp.size < 2) throw RuntimeException("APDU response smaller than 2 (sw1 + sw2)!")
@@ -133,7 +148,7 @@ class UsbApduInterface(
// 0x6C = wrong le
// so we fix the le field here
realTx[realTx.size - 1] = resp[resp.size - 1]
- resp = ccidCtx.transceiver.sendXfrBlock(realTx).data!!
+ resp = transceiver.sendXfrBlock(realTx).data!!
} else if (sw1 == 0x61) {
// 0x61 = X bytes available
// continue reading by GET RESPONSE
@@ -143,7 +158,7 @@ class UsbApduInterface(
realTx[0], 0xC0.toByte(), 0x00, 0x00, sw2.toByte()
)
- val tmp = ccidCtx.transceiver.sendXfrBlock(getResponseCmd).data!!
+ val tmp = transceiver.sendXfrBlock(getResponseCmd).data!!
resp = resp.sliceArray(0 until (resp.size - 2)) + tmp
diff --git a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidContext.kt b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidContext.kt
deleted file mode 100644
index caf69e7..0000000
--- a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidContext.kt
+++ /dev/null
@@ -1,87 +0,0 @@
-package im.angry.openeuicc.core.usb
-
-import android.content.Context
-import android.hardware.usb.UsbDevice
-import android.hardware.usb.UsbDeviceConnection
-import android.hardware.usb.UsbEndpoint
-import android.hardware.usb.UsbInterface
-import android.hardware.usb.UsbManager
-import im.angry.openeuicc.util.preferenceRepository
-import kotlinx.coroutines.flow.Flow
-
-/**
- * A wrapper over an usb device + interface, manages the lifecycle independent
- * of the APDU interface exposed to lpac-jni.
- *
- * This allows us to try multiple AIDs on each interface without opening / closing
- * the USB connection numerous times.
- */
-class UsbCcidContext private constructor(
- private val conn: UsbDeviceConnection,
- private val bulkIn: UsbEndpoint,
- private val bulkOut: UsbEndpoint,
- val productName: String,
- val verboseLoggingFlow: Flow
-) {
- companion object {
- fun createFromUsbDevice(
- context: Context,
- usbDevice: UsbDevice,
- usbInterface: UsbInterface
- ): UsbCcidContext? = runCatching {
- val (bulkIn, bulkOut) = usbInterface.endpoints.bulkPair
- if (bulkIn == null || bulkOut == null) return@runCatching null
- val conn = context.getSystemService(UsbManager::class.java).openDevice(usbDevice)
- ?: return@runCatching null
- if (!conn.claimInterface(usbInterface, true)) return@runCatching null
- UsbCcidContext(
- conn,
- bulkIn,
- bulkOut,
- usbDevice.productName ?: "USB",
- context.preferenceRepository.verboseLoggingFlow
- )
- }.getOrNull()
- }
-
- /**
- * When set to false (the default), the disconnect() method does nothing.
- * This allows the separation of device disconnection from lpac-jni's APDU interface.
- */
- var allowDisconnect = false
- private var initialized = false
- lateinit var transceiver: UsbCcidTransceiver
- var atr: ByteArray? = null
-
- fun connect() {
- if (initialized) {
- return
- }
-
- val ccidDescription = UsbCcidDescription.fromRawDescriptors(conn.rawDescriptors)!!
-
- if (!ccidDescription.hasT0Protocol) {
- throw IllegalArgumentException("Unsupported card reader; T=0 support is required")
- }
-
- transceiver = UsbCcidTransceiver(conn, bulkIn, bulkOut, ccidDescription, verboseLoggingFlow)
-
- try {
- // 6.1.1.1 PC_to_RDR_IccPowerOn (Page 20 of 40)
- // https://www.usb.org/sites/default/files/DWG_Smart-Card_USB-ICC_ICCD_rev10.pdf
- atr = transceiver.iccPowerOn().data
- } catch (e: Exception) {
- e.printStackTrace()
- throw e
- }
-
- initialized = true
- }
-
- fun disconnect() {
- if (initialized && allowDisconnect) {
- conn.close()
- atr = null
- }
- }
-}
diff --git a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidDescription.kt b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidDescription.kt
index bc32fb6..5123d53 100644
--- a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidDescription.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidDescription.kt
@@ -20,12 +20,12 @@ data class UsbCcidDescription(
private const val FEATURE_EXCHANGE_LEVEL_TPDU = 0x10000
private const val FEATURE_EXCHANGE_LEVEL_SHORT_APDU = 0x20000
- private const val FEATURE_EXCHANGE_LEVEL_EXTENDED_APDU = 0x40000
+ private const val FEATURE_EXCHAGE_LEVEL_EXTENDED_APDU = 0x40000
// bVoltageSupport Masks
- private const val VOLTAGE_5V0: Byte = 1
- private const val VOLTAGE_3V0: Byte = 2
- private const val VOLTAGE_1V8: Byte = 4
+ private const val VOLTAGE_5V: Byte = 1
+ private const val VOLTAGE_3V: Byte = 2
+ private const val VOLTAGE_1_8V: Byte = 4
private const val SLOT_OFFSET = 4
private const val FEATURES_OFFSET = 40
@@ -71,24 +71,31 @@ data class UsbCcidDescription(
}
enum class Voltage(powerOnValue: Int, mask: Int) {
- // @formatter:off
- AUTO(0, 0),
- V50(1, VOLTAGE_5V0.toInt()),
- V30(2, VOLTAGE_3V0.toInt()),
- V18(3, VOLTAGE_1V8.toInt());
- // @formatter:on
+ AUTO(0, 0), _5V(1, VOLTAGE_5V.toInt()), _3V(2, VOLTAGE_3V.toInt()), _1_8V(
+ 3,
+ VOLTAGE_1_8V.toInt()
+ );
val mask = powerOnValue.toByte()
val powerOnValue = mask.toByte()
}
- private fun hasFeature(feature: Int) = (dwFeatures and feature) != 0
+ private fun hasFeature(feature: Int): Boolean =
+ (dwFeatures and feature) != 0
- val voltages: List
- get() {
- if (hasFeature(FEATURE_AUTOMATIC_VOLTAGE)) return listOf(Voltage.AUTO)
- return Voltage.entries.filter { (it.mask.toInt() and bVoltageSupport.toInt()) != 0 }
- }
+ val voltages: Array
+ get() =
+ if (hasFeature(FEATURE_AUTOMATIC_VOLTAGE)) {
+ arrayOf(Voltage.AUTO)
+ } else {
+ Voltage.values().mapNotNull {
+ if ((it.mask.toInt() and bVoltageSupport.toInt()) != 0) {
+ it
+ } else {
+ null
+ }
+ }.toTypedArray()
+ }
val hasAutomaticPps: Boolean
get() = hasFeature(FEATURE_AUTOMATIC_PPS)
diff --git a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidTransceiver.kt b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidTransceiver.kt
index 9155721..5ef35af 100644
--- a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidTransceiver.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidTransceiver.kt
@@ -95,7 +95,6 @@ class UsbCcidTransceiver(
data class UsbCcidErrorException(val msg: String, val errorResponse: CcidDataBlock) :
Exception(msg)
- @Suppress("ArrayInDataClass")
data class CcidDataBlock(
val dwLength: Int,
val bSlot: Byte,
@@ -184,26 +183,31 @@ class UsbCcidTransceiver(
usbBulkIn, inputBuffer, inputBuffer.size, DEVICE_COMMUNICATE_TIMEOUT_MILLIS
)
if (runBlocking { verboseLoggingFlow.first() }) {
- Log.d(TAG, "Received $readBytes bytes: ${inputBuffer.encodeHex()}")
+ Log.d(TAG, "Received " + readBytes + " bytes: " + inputBuffer.encodeHex())
}
} while (readBytes <= 0 && attempts-- > 0)
if (readBytes < CCID_HEADER_LENGTH) {
throw UsbTransportException("USB-CCID error - failed to receive CCID header")
}
if (inputBuffer[0] != MESSAGE_TYPE_RDR_TO_PC_DATA_BLOCK.toByte()) {
- throw UsbTransportException(buildString {
- append("USB-CCID error - bad CCID header")
- append(", type ")
- append("%d (expected %d)".format(inputBuffer[0], MESSAGE_TYPE_RDR_TO_PC_DATA_BLOCK))
- if (expectedSequenceNumber != inputBuffer[6]) {
- append(", sequence number ")
- append("%d (expected %d)".format(inputBuffer[6], expectedSequenceNumber))
- }
- })
+ if (expectedSequenceNumber != inputBuffer[6]) {
+ throw UsbTransportException(
+ ((("USB-CCID error - bad CCID header, type " + inputBuffer[0]) + " (expected " +
+ MESSAGE_TYPE_RDR_TO_PC_DATA_BLOCK) + "), sequence number " + inputBuffer[6]
+ ) + " (expected " +
+ expectedSequenceNumber + ")"
+ )
+ }
+ throw UsbTransportException(
+ "USB-CCID error - bad CCID header type " + inputBuffer[0]
+ )
}
var result = CcidDataBlock.parseHeaderFromBytes(inputBuffer)
if (expectedSequenceNumber != result.bSeq) {
- throw UsbTransportException("USB-CCID error - expected sequence number $expectedSequenceNumber, got $result")
+ throw UsbTransportException(
+ ("USB-CCID error - expected sequence number " +
+ expectedSequenceNumber + ", got " + result)
+ )
}
val dataBuffer = ByteArray(result.dwLength)
@@ -214,7 +218,9 @@ class UsbCcidTransceiver(
usbBulkIn, inputBuffer, inputBuffer.size, DEVICE_COMMUNICATE_TIMEOUT_MILLIS
)
if (readBytes < 0) {
- throw UsbTransportException("USB error - failed reading response data! Header: $result")
+ throw UsbTransportException(
+ "USB error - failed reading response data! Header: $result"
+ )
}
System.arraycopy(inputBuffer, 0, dataBuffer, bufferedBytes, readBytes)
bufferedBytes += readBytes
@@ -279,7 +285,7 @@ class UsbCcidTransceiver(
}
val ccidDataBlock = receiveDataBlock(sequenceNumber)
val elapsedTime = SystemClock.elapsedRealtime() - startTime
- Log.d(TAG, "USB XferBlock call took ${elapsedTime}ms")
+ Log.d(TAG, "USB XferBlock call took " + elapsedTime + "ms")
return ccidDataBlock
}
@@ -287,13 +293,13 @@ class UsbCcidTransceiver(
val startTime = SystemClock.elapsedRealtime()
skipAvailableInput()
var response: CcidDataBlock? = null
- for (voltage in usbCcidDescription.voltages) {
- Log.v(TAG, "CCID: attempting to power on with voltage $voltage")
+ for (v in usbCcidDescription.voltages) {
+ Log.v(TAG, "CCID: attempting to power on with voltage $v")
response = try {
- iccPowerOnVoltage(voltage.powerOnValue)
+ iccPowerOnVoltage(v.powerOnValue)
} catch (e: UsbCcidErrorException) {
if (e.errorResponse.bError.toInt() == 7) { // Power select error
- Log.v(TAG, "CCID: failed to power on with voltage $voltage")
+ Log.v(TAG, "CCID: failed to power on with voltage $v")
iccPowerOff()
Log.v(TAG, "CCID: powered off")
continue
@@ -308,11 +314,8 @@ class UsbCcidTransceiver(
val elapsedTime = SystemClock.elapsedRealtime() - startTime
Log.d(
TAG,
- buildString {
- append("Usb transport connected")
- append(", took ", elapsedTime, "ms")
- append(", ATR=", response.data?.encodeHex())
- }
+ "Usb transport connected, took " + elapsedTime + "ms, ATR=" +
+ response.data?.encodeHex()
)
return response
}
diff --git a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidUtils.kt b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidUtils.kt
index 877c7fd..edca7a0 100644
--- a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidUtils.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidUtils.kt
@@ -6,22 +6,31 @@ import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbEndpoint
import android.hardware.usb.UsbInterface
-class UsbTransportException(message: String) : Exception(message)
+class UsbTransportException(msg: String) : Exception(msg)
-val UsbDevice.interfaces: Iterable
- get() = (0 until interfaceCount).map(::getInterface)
-
-val Iterable.smartCard: UsbInterface?
- get() = find { it.interfaceClass == UsbConstants.USB_CLASS_CSCID }
-
-val UsbInterface.endpoints: Iterable
- get() = (0 until endpointCount).map(::getEndpoint)
-
-val Iterable.bulkPair: Pair
- get() {
- val endpoints = filter { it.type == UsbConstants.USB_ENDPOINT_XFER_BULK }
- return Pair(
- endpoints.find { it.direction == UsbConstants.USB_DIR_IN },
- endpoints.find { it.direction == UsbConstants.USB_DIR_OUT },
- )
+fun UsbInterface.getIoEndpoints(): Pair {
+ var bulkIn: UsbEndpoint? = null
+ var bulkOut: UsbEndpoint? = null
+ for (i in 0 until endpointCount) {
+ val endpoint = getEndpoint(i)
+ if (endpoint.type != UsbConstants.USB_ENDPOINT_XFER_BULK) {
+ continue
+ }
+ if (endpoint.direction == UsbConstants.USB_DIR_IN) {
+ bulkIn = endpoint
+ } else if (endpoint.direction == UsbConstants.USB_DIR_OUT) {
+ bulkOut = endpoint
+ }
}
+ return Pair(bulkIn, bulkOut)
+}
+
+fun UsbDevice.getSmartCardInterface(): UsbInterface? {
+ for (i in 0 until interfaceCount) {
+ val anInterface = getInterface(i)
+ if (anInterface.interfaceClass == UsbConstants.USB_CLASS_CSCID) {
+ return anInterface
+ }
+ }
+ return null
+}
\ No newline at end of file
diff --git a/app-common/src/main/java/im/angry/openeuicc/di/DefaultCustomizableTextProvider.kt b/app-common/src/main/java/im/angry/openeuicc/di/DefaultCustomizableTextProvider.kt
index 76227fd..b493611 100644
--- a/app-common/src/main/java/im/angry/openeuicc/di/DefaultCustomizableTextProvider.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/di/DefaultCustomizableTextProvider.kt
@@ -8,7 +8,7 @@ open class DefaultCustomizableTextProvider(private val context: Context) : Custo
get() = context.getString(R.string.no_euicc)
override val profileSwitchingTimeoutMessage: String
- get() = context.getString(R.string.profile_switch_timeout)
+ get() = context.getString(R.string.enable_disable_timeout)
override fun formatInternalChannelName(logicalSlotId: Int): String =
context.getString(R.string.channel_name_format, logicalSlotId)
diff --git a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt
index 4744321..07c8bd4 100644
--- a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt
@@ -4,7 +4,6 @@ import android.content.Intent
import android.content.pm.PackageManager
import android.os.Binder
import android.os.IBinder
-import android.os.PowerManager
import android.util.Log
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
@@ -92,12 +91,6 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
}
val euiccChannelManager: EuiccChannelManager by euiccChannelManagerDelegate
- private val wakeLock: PowerManager.WakeLock by lazy {
- (getSystemService(POWER_SERVICE) as PowerManager).run {
- newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this::class.simpleName)
- }
- }
-
/**
* The state of a "foreground" task (named so due to the need to startForeground())
*/
@@ -282,8 +275,6 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
updateForegroundNotification(title, iconRes)
- wakeLock.acquire(10 * 60 * 1000L /*10 minutes*/)
-
try {
withContext(Dispatchers.IO + NonCancellable) { // Any LPA-related task must always complete
this@EuiccChannelManagerService.task()
@@ -299,7 +290,6 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
postForegroundTaskFailureNotification(failureTitle)
}
} finally {
- wakeLock.release()
if (isActive) {
stopSelf()
}
@@ -383,6 +373,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
smdp: String,
matchingId: String?,
confirmationCode: String?,
+ preview: Boolean,
imei: String?
): ForegroundTaskSubscriberFlow =
launchForegroundTask(
@@ -397,7 +388,10 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
matchingId,
imei,
confirmationCode,
+ preview,
object : ProfileDownloadCallback {
+ override fun onMetadataReceived(profileName: String, iccid: String, appCerts: String) {
+ }
override fun onStateUpdate(state: ProfileDownloadCallback.DownloadState) {
if (state.progress == 0) return
foregroundTaskState.value =
@@ -456,34 +450,30 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
iccid: String,
enable: Boolean, // Enable or disable the profile indicated in iccid
reconnectTimeoutMillis: Long = 0 // 0 = do not wait for reconnect
- ) =
+ ): ForegroundTaskSubscriberFlow =
launchForegroundTask(
getString(R.string.task_profile_switch),
getString(R.string.task_profile_switch_failure),
R.drawable.ic_task_switch
) {
euiccChannelManager.beginTrackedOperation(slotId, portId) {
- val (response, refreshed) =
- euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
- val refresh = preferenceRepository.refreshAfterSwitchFlow.first()
- val response = channel.lpa.switchProfile(iccid, enable, refresh)
- if (response || !refresh) {
- Pair(response, refresh)
- } else {
- // refresh failed, but refresh was requested
- // Sometimes, we *can* enable or disable the profile, but we cannot
- // send the refresh command to the modem because the profile somehow
- // makes the modem "busy". In this case, we can still switch by setting
- // refresh to false, but then the switch cannot take effect until the
- // user resets the modem manually by toggling airplane mode or rebooting.
- Pair(
- channel.lpa.switchProfile(iccid, enable, refresh = false),
- false
- )
- }
+ val (res, refreshed) = euiccChannelManager.withEuiccChannel(
+ slotId,
+ portId
+ ) { channel ->
+ if (!channel.lpa.switchProfile(iccid, enable, refresh = true)) {
+ // Sometimes, we *can* enable or disable the profile, but we cannot
+ // send the refresh command to the modem because the profile somehow
+ // makes the modem "busy". In this case, we can still switch by setting
+ // refresh to false, but then the switch cannot take effect until the
+ // user resets the modem manually by toggling airplane mode or rebooting.
+ Pair(channel.lpa.switchProfile(iccid, enable, refresh = false), false)
+ } else {
+ Pair(true, true)
}
+ }
- if (!response) {
+ if (!res) {
throw RuntimeException("Could not switch profile")
}
@@ -509,19 +499,4 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
preferenceRepository.notificationSwitchFlow.first()
}
}
-
- fun launchMemoryReset(slotId: Int, portId: Int): ForegroundTaskSubscriberFlow =
- launchForegroundTask(
- getString(R.string.task_euicc_memory_reset),
- getString(R.string.task_euicc_memory_reset_failure),
- R.drawable.ic_euicc_memory_reset
- ) {
- euiccChannelManager.beginTrackedOperation(slotId, portId) {
- euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
- channel.lpa.euiccMemoryReset()
- }
-
- preferenceRepository.notificationDeleteFlow.first()
- }
- }
}
\ 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 248afaf..e88ad01 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
@@ -27,16 +27,9 @@ import kotlinx.coroutines.launch
import net.typeblog.lpac_jni.impl.PKID_GSMA_LIVE_CI
import net.typeblog.lpac_jni.impl.PKID_GSMA_TEST_CI
-// https://euicc-manual.osmocom.org/docs/pki/eum/accredited.json
-// ref:
-private val RE_SAS = Regex(
- """^[A-Z]{2}-[A-Z]{2}(?:-UP)?-\d{4}T?(?:-\d+)?T?$""",
- setOf(RegexOption.IGNORE_CASE),
-)
-
class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
companion object {
- private val YES_NO = Pair(R.string.euicc_info_yes, R.string.euicc_info_no)
+ private val YES_NO = Pair(R.string.yes, R.string.no)
}
private lateinit var swipeRefresh: SwipeRefreshLayout
@@ -69,7 +62,7 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
logicalSlotId = intent.getIntExtra("logicalSlotId", 0)
val channelTitle = if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
- getString(R.string.channel_type_usb)
+ getString(R.string.usb)
} else {
appContainer.customizableTextProvider.formatInternalChannelName(logicalSlotId)
}
@@ -107,29 +100,26 @@ 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_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.firmwareVersion?.let { add(Item(R.string.euicc_info_fw_ver, it)) }
- vendorInfo.bootloaderVersion?.let { add(Item(R.string.euicc_info_bl_ver, it)) }
- }
- channel.lpa.euiccInfo2?.let { info ->
- add(Item(R.string.euicc_info_sgp22_version, info.sgp22Version.toString()))
- add(Item(R.string.euicc_info_firmware_version, info.euiccFirmwareVersion.toString()))
- add(Item(R.string.euicc_info_gp_version, info.globalPlatformVersion.toString()))
- add(Item(R.string.euicc_info_pp_version, info.ppVersion.toString()))
- info.sasAccreditationNumber.trim().takeIf(RE_SAS::matches)
- ?.let { add(Item(R.string.euicc_info_sas_accreditation_number, it.uppercase())) }
-
- 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))
+ 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
+ )
+ )
+ channel.lpa.euiccInfo2.let { info ->
+ add(Item(R.string.euicc_info_sgp22_version, info?.sgp22Version))
+ add(Item(R.string.euicc_info_firmware_version, info?.euiccFirmwareVersion))
+ add(Item(R.string.euicc_info_globalplatform_version, info?.globalPlatformVersion))
+ add(Item(R.string.euicc_info_pp_version, info?.ppVersion))
+ add(Item(R.string.euicc_info_sas_accreditation_number, info?.sasAccreditationNumber))
+ add(Item(R.string.euicc_info_free_nvram, info?.freeNvram?.let(::formatFreeSpace)))
}
channel.lpa.euiccInfo2?.euiccCiPKIdListForSigning.orEmpty().let { signers ->
// SGP.28 v1.0, eSIM CI Registration Criteria (Page 5 of 9, 2019-10-24)
@@ -137,20 +127,30 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
// FS.27 v2.0, Security Guidelines for UICC Profiles (Page 25 of 27, 2024-01-30)
// https://www.gsma.com/solutions-and-impact/technologies/security/wp-content/uploads/2024/01/FS.27-Security-Guidelines-for-UICC-Credentials-v2.0-FINAL-23-July.pdf#page=25
val resId = when {
- signers.isEmpty() -> R.string.euicc_info_unknown // the case is not mp, but it's is not common
+ signers.isEmpty() -> R.string.unknown // the case is not mp, but it's is not common
PKID_GSMA_LIVE_CI.any(signers::contains) -> R.string.euicc_info_ci_gsma_live
PKID_GSMA_TEST_CI.any(signers::contains) -> R.string.euicc_info_ci_gsma_test
else -> R.string.euicc_info_ci_unknown
}
add(Item(R.string.euicc_info_ci_type, getString(resId)))
}
- val atr = channel.atr?.encodeHex() ?: getString(R.string.euicc_info_unavailable)
- add(Item(R.string.euicc_info_atr, atr, copiedToastResId = R.string.toast_atr_copied))
+ add(
+ Item(
+ R.string.euicc_info_atr,
+ channel.atr?.encodeHex() ?: getString(R.string.information_unavailable),
+ copiedToastResId = R.string.toast_atr_copied,
+ )
+ )
}
- @Suppress("SameParameterValue")
private fun formatByBoolean(b: Boolean, res: Pair): String =
- getString(if (b) res.first else res.second)
+ getString(
+ if (b) {
+ res.first
+ } else {
+ res.second
+ }
+ )
inner class EuiccInfoViewHolder(root: View) : ViewHolder(root) {
private val title: TextView = root.requireViewById(R.id.euicc_info_title)
@@ -177,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.euicc_info_unknown)
+ content.text = item.content ?: getString(R.string.unknown)
}
}
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt
index 016e96f..842f4ec 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
@@ -38,10 +38,8 @@ import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
@@ -57,7 +55,6 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
private lateinit var fab: FloatingActionButton
private lateinit var profileList: RecyclerView
private var logicalSlotId: Int = -1
- private lateinit var eid: String
private val adapter = EuiccProfileAdapter()
@@ -134,42 +131,31 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
inflater.inflate(R.menu.fragment_euicc, menu)
}
- override fun onPrepareOptionsMenu(menu: Menu) {
- super.onPrepareOptionsMenu(menu)
- menu.findItem(R.id.show_notifications).isVisible =
- logicalSlotId != -1
- menu.findItem(R.id.euicc_info).isVisible =
- logicalSlotId != -1
- menu.findItem(R.id.euicc_memory_reset).isVisible =
- runBlocking { preferenceRepository.euiccMemoryResetFlow.first() }
- }
-
- override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
- R.id.show_notifications -> {
- Intent(requireContext(), NotificationsActivity::class.java).apply {
- putExtra("logicalSlotId", logicalSlotId)
- startActivity(this)
+ override fun onOptionsItemSelected(item: MenuItem): Boolean =
+ when (item.itemId) {
+ R.id.show_notifications -> {
+ if (logicalSlotId != -1) {
+ Intent(requireContext(), NotificationsActivity::class.java).apply {
+ putExtra("logicalSlotId", logicalSlotId)
+ startActivity(this)
+ }
+ }
+ true
}
- true
- }
- R.id.euicc_info -> {
- Intent(requireContext(), EuiccInfoActivity::class.java).apply {
- putExtra("logicalSlotId", logicalSlotId)
- startActivity(this)
+ R.id.euicc_info -> {
+ if (logicalSlotId != -1) {
+ Intent(requireContext(), EuiccInfoActivity::class.java).apply {
+ putExtra("logicalSlotId", logicalSlotId)
+ startActivity(this)
+ }
+ }
+ true
}
- true
- }
- R.id.euicc_memory_reset -> {
- EuiccMemoryResetFragment.newInstance(slotId, portId, eid)
- .show(childFragmentManager, EuiccMemoryResetFragment.TAG)
- true
+ else -> super.onOptionsItemSelected(item)
}
- else -> super.onOptionsItemSelected(item)
- }
-
protected open suspend fun onCreateFooterViews(
parent: ViewGroup,
profiles: List
@@ -206,7 +192,6 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
val profiles = withEuiccChannel { channel ->
logicalSlotId = channel.logicalSlotId
- eid = channel.lpa.eID
euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId)
if (unfilteredProfileListFlow.value)
channel.lpa.profiles
@@ -253,7 +238,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
if (!isUsb) {
withContext(Dispatchers.Main) {
AlertDialog.Builder(requireContext()).apply {
- setMessage(R.string.profile_switch_did_not_refresh)
+ setMessage(R.string.switch_did_not_refresh)
setPositiveButton(android.R.string.ok) { dialog, _ ->
dialog.dismiss()
requireActivity().finish()
@@ -347,7 +332,6 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
private val profileClassLabel: TextView = root.requireViewById(R.id.profile_class_label)
private val profileClass: TextView = root.requireViewById(R.id.profile_class)
private val profileMenu: ImageButton = root.requireViewById(R.id.profile_menu)
- private val profileSeqNumber: TextView = root.requireViewById(R.id.profile_sequence_number)
init {
iccid.setOnClickListener {
@@ -367,9 +351,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
true
}
- profileMenu.setOnClickListener {
- showOptionsMenu()
- }
+ profileMenu.setOnClickListener { showOptionsMenu() }
}
private lateinit var profile: LocalProfileInfo
@@ -380,9 +362,9 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
state.setText(
if (profile.isEnabled) {
- R.string.profile_state_enabled
+ R.string.enabled
} else {
- R.string.profile_state_disabled
+ R.string.disabled
}
)
provider.text = profile.providerName
@@ -399,13 +381,6 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
iccid.transformationMethod = PasswordTransformationMethod.getInstance()
}
- fun setProfileSequenceNumber(index: Int) {
- profileSeqNumber.text = root.context.getString(
- R.string.profile_sequence_number_format,
- index,
- )
- }
-
private fun showOptionsMenu() {
// Prevent users from doing multiple things at once
if (invalid || swipeRefresh.isRefreshing) return
@@ -471,7 +446,6 @@ 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
deleted file mode 100644
index 086a849..0000000
--- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccMemoryResetFragment.kt
+++ /dev/null
@@ -1,126 +0,0 @@
-package im.angry.openeuicc.ui
-
-import android.graphics.Typeface
-import android.os.Bundle
-import android.text.Editable
-import android.util.Log
-import android.widget.EditText
-import android.widget.Toast
-import androidx.appcompat.app.AlertDialog
-import androidx.fragment.app.DialogFragment
-import androidx.fragment.app.Fragment
-import androidx.lifecycle.lifecycleScope
-import im.angry.openeuicc.common.R
-import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
-import im.angry.openeuicc.util.EuiccChannelFragmentMarker
-import im.angry.openeuicc.util.EuiccProfilesChangedListener
-import im.angry.openeuicc.util.ensureEuiccChannelManager
-import im.angry.openeuicc.util.euiccChannelManagerService
-import im.angry.openeuicc.util.newInstanceEuicc
-import im.angry.openeuicc.util.notifyEuiccProfilesChanged
-import im.angry.openeuicc.util.portId
-import im.angry.openeuicc.util.slotId
-import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.launch
-
-class EuiccMemoryResetFragment : DialogFragment(), EuiccChannelFragmentMarker {
- companion object {
- const val TAG = "EuiccMemoryResetFragment"
-
- private const val FIELD_EID = "eid"
-
- fun newInstance(slotId: Int, portId: Int, eid: String) =
- newInstanceEuicc(EuiccMemoryResetFragment::class.java, slotId, portId) {
- putString(FIELD_EID, eid)
- }
- }
-
- private val eid: String by lazy { requireArguments().getString(FIELD_EID)!! }
-
- private val confirmText: String by lazy {
- getString(R.string.euicc_memory_reset_confirm_text, eid.takeLast(8))
- }
-
- private inline val isMatched: Boolean
- get() = editText.text.toString() == confirmText
-
- private var confirmed = false
-
- private var toast: Toast? = null
- set(value) {
- toast?.cancel()
- field = value
- value?.show()
- }
-
- private val editText by lazy {
- EditText(requireContext()).apply {
- isLongClickable = false
- typeface = Typeface.MONOSPACE
- hint = Editable.Factory.getInstance()
- .newEditable(getString(R.string.euicc_memory_reset_hint_text, confirmText))
- }
- }
-
- private inline val alertDialog: AlertDialog
- get() = requireDialog() as AlertDialog
-
- override fun onCreateDialog(savedInstanceState: Bundle?) =
- AlertDialog.Builder(requireContext(), R.style.AlertDialogTheme)
- .setTitle(R.string.euicc_memory_reset_title)
- .setMessage(getString(R.string.euicc_memory_reset_message, eid, confirmText))
- .setView(editText)
- // Set listener to null to prevent auto closing
- .setNegativeButton(android.R.string.cancel, null)
- .setPositiveButton(R.string.euicc_memory_reset_invoke_button, null)
- .create()
-
- override fun onResume() {
- super.onResume()
- alertDialog.setCanceledOnTouchOutside(false)
- alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
- .setOnClickListener { if (!confirmed) confirmation() }
- alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE)
- .setOnClickListener { if (!confirmed) dismiss() }
- }
-
- private fun confirmation() {
- toast?.cancel()
- if (!isMatched) {
- Log.d(TAG, buildString {
- appendLine("User input is mismatch:")
- appendLine(editText.text)
- appendLine(confirmText)
- })
- val resId = R.string.toast_euicc_memory_reset_confirm_text_mismatched
- toast = Toast.makeText(requireContext(), resId, Toast.LENGTH_LONG)
- return
- }
- confirmed = true
- preventUserAction()
-
- requireParentFragment().lifecycleScope.launch {
- ensureEuiccChannelManager()
- euiccChannelManagerService.waitForForegroundTask()
-
- euiccChannelManagerService.launchMemoryReset(slotId, portId)
- .onStart {
- parentFragment?.notifyEuiccProfilesChanged()
-
- val resId = R.string.toast_euicc_memory_reset_finitshed
- toast = Toast.makeText(requireContext(), resId, Toast.LENGTH_LONG)
-
- runCatching(::dismiss)
- }
- .waitDone()
- }
- }
-
- private fun preventUserAction() {
- editText.isEnabled = false
- alertDialog.setCancelable(false)
- alertDialog.setCanceledOnTouchOutside(false)
- alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
- alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).isEnabled = false
- }
-}
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/IsdrAidListActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/IsdrAidListActivity.kt
deleted file mode 100644
index 022a391..0000000
--- a/app-common/src/main/java/im/angry/openeuicc/ui/IsdrAidListActivity.kt
+++ /dev/null
@@ -1,72 +0,0 @@
-package im.angry.openeuicc.ui
-
-import android.os.Bundle
-import android.text.Editable
-import android.view.Menu
-import android.view.MenuItem
-import android.widget.EditText
-import android.widget.Toast
-import androidx.activity.enableEdgeToEdge
-import androidx.appcompat.app.AppCompatActivity
-import androidx.lifecycle.lifecycleScope
-import im.angry.openeuicc.common.R
-import im.angry.openeuicc.util.preferenceRepository
-import im.angry.openeuicc.util.setupToolbarInsets
-import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.launch
-
-class IsdrAidListActivity : AppCompatActivity() {
- private lateinit var isdrAidListEditor: EditText
-
- override fun onCreate(savedInstanceState: Bundle?) {
- enableEdgeToEdge()
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_isdr_aid_list)
- setSupportActionBar(requireViewById(R.id.toolbar))
- setupToolbarInsets()
- supportActionBar!!.setDisplayHomeAsUpEnabled(true)
-
- isdrAidListEditor = requireViewById(R.id.isdr_aid_list_editor)
-
- lifecycleScope.launch {
- preferenceRepository.isdrAidListFlow.onEach {
- isdrAidListEditor.text = Editable.Factory.getInstance().newEditable(it)
- }.collect()
- }
- }
-
- override fun onCreateOptionsMenu(menu: Menu?): Boolean {
- menuInflater.inflate(R.menu.activity_isdr_aid_list, menu)
- return true
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean =
- when (item.itemId) {
- R.id.save -> {
- lifecycleScope.launch {
- preferenceRepository.isdrAidListFlow.updatePreference(isdrAidListEditor.text.toString())
- Toast.makeText(
- this@IsdrAidListActivity,
- R.string.isdr_aid_list_saved,
- Toast.LENGTH_SHORT
- ).show()
- }
- true
- }
-
- R.id.reset -> {
- lifecycleScope.launch {
- preferenceRepository.isdrAidListFlow.removePreference()
- }
- true
- }
-
- android.R.id.home -> {
- finish()
- true
- }
-
- else -> super.onOptionsItemSelected(item)
- }
-}
\ No newline at end of file
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt
index b42f4cf..01d0ab2 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
@@ -174,7 +174,7 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
// If USB readers exist, add them at the very last
// We use a wrapper fragment to handle logic specific to USB readers
usbDevice?.let {
- val productName = it.productName ?: getString(R.string.channel_type_usb)
+ val productName = it.productName ?: getString(R.string.usb)
newPages.add(Page(EuiccChannelManager.USB_CHANNEL_ID, productName) {
UsbCcidReaderFragment()
})
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt
index 07d5f13..21a2d40 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
@@ -60,7 +60,7 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
// This is slightly different from the MainActivity logic
// due to the length (we don't want to display the full USB product name)
val channelTitle = if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
- getString(R.string.channel_type_usb)
+ getString(R.string.usb)
} else {
appContainer.customizableTextProvider.formatInternalChannelName(logicalSlotId)
}
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt
index 38d1bc6..7f82f22 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
@@ -20,10 +20,13 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
private const val FIELD_ICCID = "iccid"
private const val FIELD_NAME = "name"
- fun newInstance(slotId: Int, portId: Int, iccid: String, name: String) =
- newInstanceEuicc(ProfileDeleteFragment::class.java, slotId, portId) {
+ fun newInstance(slotId: Int, portId: Int, iccid: String, name: String): ProfileDeleteFragment {
+ val instance = newInstanceEuicc(ProfileDeleteFragment::class.java, slotId, portId)
+ instance.requireArguments().apply {
putString(FIELD_ICCID, iccid)
putString(FIELD_NAME, name)
+ }
+ return instance
}
}
@@ -88,12 +91,19 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
requireParentFragment().lifecycleScope.launch {
ensureEuiccChannelManager()
euiccChannelManagerService.waitForForegroundTask()
- euiccChannelManagerService.launchProfileDeleteTask(slotId, portId, iccid)
- .onStart {
- parentFragment?.notifyEuiccProfilesChanged()
- runCatching(::dismiss)
+ euiccChannelManagerService.launchProfileDeleteTask(slotId, portId, iccid).onStart {
+ if (parentFragment is EuiccProfilesChangedListener) {
+ // Trigger a refresh in the parent fragment -- it should wait until
+ // any foreground task is completed before actually doing a refresh
+ (parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged()
}
- .waitDone()
+
+ try {
+ dismiss()
+ } catch (e: IllegalStateException) {
+ // Ignored
+ }
+ }.waitDone()
}
}
}
\ No newline at end of file
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt
index 281e625..25c5273 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
@@ -7,7 +7,6 @@ import android.view.View
import android.view.ViewGroup
import android.widget.ProgressBar
import android.widget.Toast
-import androidx.annotation.StringRes
import androidx.appcompat.widget.Toolbar
import androidx.lifecycle.lifecycleScope
import com.google.android.material.textfield.TextInputLayout
@@ -19,16 +18,16 @@ import net.typeblog.lpac_jni.LocalProfileAssistant
class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragmentMarker {
companion object {
- private const val FIELD_ICCID = "iccid"
- private const val FIELD_CURRENT_NAME = "currentName"
-
const val TAG = "ProfileRenameFragment"
- 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)
+ fun newInstance(slotId: Int, portId: Int, iccid: String, currentName: String): ProfileRenameFragment {
+ val instance = newInstanceEuicc(ProfileRenameFragment::class.java, slotId, portId)
+ instance.requireArguments().apply {
+ putString("iccid", iccid)
+ putString("currentName", currentName)
}
+ return instance
+ }
}
private lateinit var toolbar: Toolbar
@@ -37,14 +36,6 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
private var renaming = false
- private val iccid: String by lazy {
- requireArguments().getString(FIELD_ICCID)!!
- }
-
- private val currentName: String by lazy {
- requireArguments().getString(FIELD_CURRENT_NAME)!!
- }
-
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -63,9 +54,9 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- profileRenameNewName.editText!!.setText(currentName)
+ profileRenameNewName.editText!!.setText(requireArguments().getString("currentName"))
toolbar.apply {
- setTitle(R.string.profile_rename)
+ setTitle(R.string.rename)
setNavigationOnClickListener {
if (!renaming) dismiss()
}
@@ -87,8 +78,12 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
}
}
- private fun showErrorAndCancel(@StringRes resId: Int) {
- Toast.makeText(requireContext(), resId, Toast.LENGTH_LONG).show()
+ private fun showErrorAndCancel(errorStrRes: Int) {
+ Toast.makeText(
+ requireContext(),
+ errorStrRes,
+ Toast.LENGTH_LONG
+ ).show()
renaming = false
progress.visibility = View.GONE
@@ -99,15 +94,17 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
progress.isIndeterminate = true
progress.visibility = View.VISIBLE
- val newName = profileRenameNewName.editText!!.text.toString().trim()
-
lifecycleScope.launch {
ensureEuiccChannelManager()
euiccChannelManagerService.waitForForegroundTask()
- val response = euiccChannelManagerService
- .launchProfileRenameTask(slotId, portId, iccid, newName).waitDone()
+ val res = euiccChannelManagerService.launchProfileRenameTask(
+ slotId,
+ portId,
+ requireArguments().getString("iccid")!!,
+ profileRenameNewName.editText!!.text.toString().trim()
+ ).waitDone()
- when (response) {
+ when (res) {
is LocalProfileAssistant.ProfileNameTooLongException -> {
showErrorAndCancel(R.string.profile_rename_too_long)
}
@@ -121,9 +118,15 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
}
else -> {
- parentFragment?.notifyEuiccProfilesChanged()
+ if (parentFragment is EuiccProfilesChangedListener) {
+ (parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged()
+ }
- runCatching(::dismiss)
+ try {
+ dismiss()
+ } catch (e: IllegalStateException) {
+ // Ignored
+ }
}
}
}
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 7a717ac..fab680f 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,7 +8,6 @@ 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
@@ -17,6 +16,7 @@ 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::setVisible)
+ .onEach { developerPref.isVisible = it }
.collect()
}
@@ -77,19 +77,6 @@ open class SettingsFragment: PreferenceFragmentCompat() {
requirePreference("pref_developer_ignore_tls_certificate")
.bindBooleanFlow(preferenceRepository.ignoreTLSCertificateFlow)
-
- requirePreference("pref_developer_refresh_after_switch")
- .bindBooleanFlow(preferenceRepository.refreshAfterSwitchFlow)
-
- 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)
- }
}
protected fun requirePreference(key: CharSequence) =
@@ -103,53 +90,51 @@ open class SettingsFragment: PreferenceFragmentCompat() {
@Suppress("UNUSED_PARAMETER")
private fun onAppVersionClicked(pref: Preference): Boolean {
if (developerPref.isVisible) return false
-
val now = System.currentTimeMillis()
- numClicks = if (now - lastClickTimestamp >= 1000) 1 else numClicks + 1
+ if (now - lastClickTimestamp >= 1000) {
+ numClicks = 1
+ } else {
+ numClicks++
+ }
lastClickTimestamp = now
- lifecycleScope.launch {
- preferenceRepository.developerOptionsEnabledFlow.updatePreference(numClicks >= 7)
+ 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()
}
- 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) {
+ private fun CheckBoxPreference.bindBooleanFlow(flow: PreferenceFlowWrapper) {
lifecycleScope.launch {
- flow.collect(::setChecked)
+ flow.collect { isChecked = it }
}
setOnPreferenceChangeListener { _, newValue ->
- lifecycleScope.launch {
+ runBlocking {
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/wizard/DownloadWizardActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt
index 6574645..11503ac 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
@@ -1,16 +1,13 @@
package im.angry.openeuicc.ui.wizard
-import android.app.assist.AssistContent
import android.os.Bundle
import android.view.View
-import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
import android.widget.Button
import android.widget.ProgressBar
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.enableEdgeToEdge
-import androidx.core.net.toUri
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
@@ -22,6 +19,7 @@ import im.angry.openeuicc.ui.BaseEuiccAccessActivity
import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import net.typeblog.lpac_jni.LocalProfileAssistant
class DownloadWizardActivity: BaseEuiccAccessActivity() {
@@ -31,12 +29,11 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
var smdp: String,
var matchingId: String?,
var confirmationCode: String?,
+ var preview: Boolean,
var imei: String?,
var downloadStarted: Boolean,
var downloadTaskID: Long,
var downloadError: LocalProfileAssistant.ProfileDownloadException?,
- var skipMethodSelect: Boolean,
- var confirmationCodeRequired: Boolean,
)
private lateinit var state: DownloadWizardState
@@ -65,21 +62,18 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
})
state = DownloadWizardState(
- currentStepFragmentClassName = null,
- selectedLogicalSlot = intent.getIntExtra("selectedLogicalSlot", 0),
- smdp = "",
- matchingId = null,
- confirmationCode = null,
- imei = null,
- downloadStarted = false,
- downloadTaskID = -1,
- downloadError = null,
- skipMethodSelect = false,
- confirmationCodeRequired = false,
+ null,
+ intent.getIntExtra("selectedLogicalSlot", 0),
+ "",
+ null,
+ null,
+ false,
+ null,
+ false,
+ -1,
+ null
)
- handleDeepLink()
-
progressBar = requireViewById(R.id.progress)
nextButton = requireViewById(R.id.download_wizard_next)
prevButton = requireViewById(R.id.download_wizard_back)
@@ -119,35 +113,6 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
}
}
- private fun handleDeepLink() {
- // If we get an LPA string from deep-link intents, extract from there.
- // Note that `onRestoreInstanceState` could override this with user input,
- // but that _is_ the desired behavior.
- val uri = intent.data ?: return
- if (uri.scheme.contentEquals("lpa", ignoreCase = true)) {
- val parsed = LPAString.parse(uri.schemeSpecificPart)
- state.smdp = parsed.address
- state.matchingId = parsed.matchingId
- state.confirmationCodeRequired = parsed.confirmationCodeRequired
- state.skipMethodSelect = true
- }
- }
-
- override fun onProvideAssistContent(outContent: AssistContent?) {
- super.onProvideAssistContent(outContent)
- outContent?.webUri = try {
- val activationCode = LPAString(
- state.smdp,
- state.matchingId,
- null,
- state.confirmationCode != null,
- )
- "LPA:$activationCode".toUri()
- } catch (_: Exception) {
- null
- }
- }
-
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString("currentStepFragmentClassName", state.currentStepFragmentClassName)
@@ -158,7 +123,6 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
outState.putString("imei", state.imei)
outState.putBoolean("downloadStarted", state.downloadStarted)
outState.putLong("downloadTaskID", state.downloadTaskID)
- outState.putBoolean("confirmationCodeRequired", state.confirmationCodeRequired)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
@@ -175,8 +139,6 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
state.downloadStarted =
savedInstanceState.getBoolean("downloadStarted", state.downloadStarted)
state.downloadTaskID = savedInstanceState.getLong("downloadTaskID", state.downloadTaskID)
- state.confirmationCode = savedInstanceState.getString("confirmationCode", state.confirmationCode)
- state.confirmationCodeRequired = savedInstanceState.getBoolean("confirmationCodeRequired", state.confirmationCodeRequired)
}
private fun onPrevPressed() {
@@ -252,14 +214,6 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
supportFragmentManager.beginTransaction().setCustomAnimations(enterAnim, exitAnim)
.replace(R.id.step_fragment_container, nextFrag)
.commit()
-
- // Sync screen on state
- if (nextFrag.keepScreenOn) {
- window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
- } else {
- window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
- }
-
refreshButtons()
}
@@ -289,8 +243,6 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
protected val state: DownloadWizardState
get() = (requireActivity() as DownloadWizardActivity).state
- open val keepScreenOn = false
-
abstract val hasNext: Boolean
abstract val hasPrev: Boolean
abstract fun createNextFragment(): DownloadWizardStepFragment?
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt
index 1c69de5..5fa8002 100644
--- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt
@@ -1,6 +1,7 @@
package im.angry.openeuicc.ui.wizard
import android.os.Bundle
+import android.util.Patterns
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -35,11 +36,7 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF
DownloadWizardProgressFragment()
override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment =
- if (state.skipMethodSelect) {
- DownloadWizardSlotSelectFragment()
- } else {
- DownloadWizardMethodSelectFragment()
- }
+ DownloadWizardMethodSelectFragment()
override fun onCreateView(
inflater: LayoutInflater,
@@ -54,9 +51,6 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF
smdp.editText!!.addTextChangedListener {
updateInputCompleteness()
}
- confirmationCode.editText!!.addTextChangedListener {
- updateInputCompleteness()
- }
return view
}
@@ -67,15 +61,6 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF
confirmationCode.editText!!.setText(state.confirmationCode)
imei.editText!!.setText(state.imei)
updateInputCompleteness()
-
- if (state.confirmationCodeRequired) {
- confirmationCode.editText!!.requestFocus()
- confirmationCode.editText!!.hint =
- getString(R.string.profile_download_confirmation_code_required)
- } else {
- confirmationCode.editText!!.hint =
- getString(R.string.profile_download_confirmation_code)
- }
}
override fun onPause() {
@@ -84,34 +69,7 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF
}
private fun updateInputCompleteness() {
- inputComplete = isValidAddress(smdp.editText!!.text)
- if (state.confirmationCodeRequired) {
- inputComplete = inputComplete && confirmationCode.editText!!.text.isNotEmpty()
- }
+ inputComplete = Patterns.DOMAIN_NAME.matcher(smdp.editText!!.text).matches()
refreshButtons()
}
-}
-
-private fun isValidAddress(input: CharSequence): Boolean {
- if (!input.contains('.')) return false
- var fqdn = input
- var port = 443
- if (input.contains(':')) {
- val portIndex = input.lastIndexOf(':')
- fqdn = input.substring(0, portIndex)
- port = input.substring(portIndex + 1, input.length).toIntOrNull(10) ?: 0
- }
- // see https://en.wikipedia.org/wiki/Port_(computer_networking)
- if (port < 1 || port > 0xffff) return false
- // see https://en.wikipedia.org/wiki/Fully_qualified_domain_name
- if (fqdn.isEmpty() || fqdn.length > 255) return false
- for (part in fqdn.split('.')) {
- if (part.isEmpty() || part.length > 64) return false
- if (part.first() == '-' || part.last() == '-') return false
- for (c in part) {
- if (c.isLetterOrDigit() || c == '-') continue
- return false
- }
- }
- return true
}
\ No newline at end of file
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt
index 3841868..e282196 100644
--- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt
@@ -8,7 +8,6 @@ import android.view.ViewGroup
import android.widget.TextView
import im.angry.openeuicc.common.R
import im.angry.openeuicc.util.*
-import org.json.JSONObject
import java.util.Date
class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardStepFragment() {
@@ -87,10 +86,9 @@ class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardS
ret.appendLine()
val str = resp.data.decodeToString(throwOnInvalidSequence = false)
-
ret.appendLine(
if (str.startsWith('{')) {
- JSONObject(str).toString(2)
+ str.prettyPrintJson()
} else {
str
}
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt
index 4b02b7a..6203364 100644
--- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt
@@ -42,16 +42,21 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
registerForActivityResult(ActivityResultContracts.GetContent()) { result ->
if (result == null) return@registerForActivityResult
- lifecycleScope.launch {
- val decoded = withContext(Dispatchers.IO) {
- runCatching {
- requireContext().contentResolver.openInputStream(result)?.use { input ->
- BitmapFactory.decodeStream(input).use(::decodeQrFromBitmap)
+ lifecycleScope.launch(Dispatchers.IO) {
+ runCatching {
+ requireContext().contentResolver.openInputStream(result)?.let { input ->
+ val bmp = BitmapFactory.decodeStream(input)
+ input.close()
+
+ decodeQrFromBitmap(bmp)?.let {
+ withContext(Dispatchers.Main) {
+ processLpaString(it)
+ }
}
+
+ bmp.recycle()
}
}
-
- decoded.getOrNull()?.let { processLpaString(it) }
}
}
@@ -119,14 +124,9 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
processLpaString(text.toString())
}
- private fun processLpaString(input: String) {
- try {
- val parsed = LPAString.parse(input)
- state.smdp = parsed.address
- state.matchingId = parsed.matchingId
- state.confirmationCodeRequired = parsed.confirmationCodeRequired
- gotoNextFragment(DownloadWizardDetailsFragment())
- } catch (e: IllegalArgumentException) {
+ private fun processLpaString(s: String) {
+ val components = s.split("$")
+ if (components.size < 3 || components[0] != "LPA:1") {
AlertDialog.Builder(requireContext()).apply {
setTitle(R.string.profile_download_incorrect_lpa_string)
setMessage(R.string.profile_download_incorrect_lpa_string_message)
@@ -134,22 +134,21 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
setNegativeButton(android.R.string.cancel, null)
show()
}
+ return
}
+ state.smdp = components[1]
+ state.matchingId = components[2]
+ gotoNextFragment(DownloadWizardDetailsFragment())
}
- private inner class DownloadMethodViewHolder(private val root: View) : ViewHolder(root) {
+ private class DownloadMethodViewHolder(private val root: View) : ViewHolder(root) {
private val icon = root.requireViewById(R.id.download_method_icon)
private val title = root.requireViewById(R.id.download_method_title)
fun bind(item: DownloadMethod) {
icon.setImageResource(item.iconRes)
title.setText(item.titleRes)
- root.setOnClickListener {
- // If the user elected to use another download method, reset the confirmation code flag
- // too
- state.confirmationCodeRequired = false
- item.onClick()
- }
+ root.setOnClickListener { item.onClick() }
}
}
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 0048190..9fd7c21 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,7 +7,6 @@ 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
@@ -43,24 +42,23 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep
}
private data class ProgressItem(
- @StringRes val titleRes: Int,
- var state: ProgressState = ProgressState.NotStarted,
- var errorMessage: SimplifiedErrorMessages? = null,
+ val titleRes: Int,
+ var state: ProgressState
)
private val progressItems = arrayOf(
- 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)
+ 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)
)
private val adapter = ProgressItemAdapter()
- // We don't want to turn off the screen during a download
- override val keepScreenOn = true
-
private var isDone = false
override val hasNext: Boolean
@@ -121,13 +119,8 @@ 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) {
- if (state.downloadError == null) {
- progressItem.state = ProgressState.Done
- } else {
- progressItem.state = ProgressState.Error
- progressItem.errorMessage =
- SimplifiedErrorMessages.fromDownloadError(state.downloadError!!)
- }
+ progressItem.state =
+ if (state.downloadError == null) ProgressState.Done else ProgressState.Error
}
adapter.notifyItemChanged(index)
@@ -137,8 +130,9 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep
refreshButtons()
}
- is EuiccChannelManagerService.ForegroundTaskState.InProgress ->
+ is EuiccChannelManagerService.ForegroundTaskState.InProgress -> {
updateProgress(it.progress)
+ }
else -> {}
}
@@ -166,6 +160,7 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep
state.smdp,
state.matchingId,
state.confirmationCode,
+ state.preview,
state.imei
)
@@ -200,15 +195,9 @@ 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 -> {
@@ -231,15 +220,6 @@ 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 8097058..5510fb0 100644
--- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt
@@ -19,6 +19,7 @@ import im.angry.openeuicc.util.*
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
+import net.typeblog.lpac_jni.LocalProfileInfo
class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardStepFragment() {
companion object {
@@ -48,11 +49,7 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
get() = true
override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment =
- if (state.skipMethodSelect) {
- DownloadWizardDetailsFragment()
- } else {
- DownloadWizardMethodSelectFragment()
- }
+ DownloadWizardMethodSelectFragment()
override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment? = null
@@ -186,12 +183,12 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
}
title.text = if (item.logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
- item.intrinsicChannelName ?: root.context.getString(R.string.channel_type_usb)
+ item.intrinsicChannelName ?: root.context.getString(R.string.usb)
} else {
appContainer.customizableTextProvider.formatInternalChannelName(item.logicalSlotId)
}
eID.text = item.eID
- activeProfile.text = item.enabledProfileName ?: root.context.getString(R.string.profile_no_enabled_profile)
+ activeProfile.text = item.enabledProfileName ?: root.context.getString(R.string.unknown)
freeSpace.text = formatFreeSpace(item.freeSpace)
checkBox.isChecked = adapter.currentSelectedIdx == idx
}
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/SimplifiedErrorMessages.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/SimplifiedErrorMessages.kt
deleted file mode 100644
index 8ce5740..0000000
--- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/SimplifiedErrorMessages.kt
+++ /dev/null
@@ -1,154 +0,0 @@
-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 b44bef8..3f3c4ee 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
@@ -7,65 +7,43 @@ import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.service.EuiccChannelManagerService
import im.angry.openeuicc.ui.BaseEuiccAccessActivity
-private const val FIELD_SLOT_ID = "slotId"
-private const val FIELD_PORT_ID = "portId"
-
-interface EuiccChannelFragmentMarker : OpenEuiccContextMarker
-
-private typealias BundleSetter = Bundle.() -> Unit
+interface EuiccChannelFragmentMarker: OpenEuiccContextMarker
// We must use extension functions because there is no way to add bounds to the type of "self"
// in the definition of an interface, so the only way is to limit where the extension functions
// can be applied.
-fun newInstanceEuicc(clazz: Class, slotId: Int, portId: Int, addArguments: BundleSetter = {}): T
- where T : Fragment, T : EuiccChannelFragmentMarker =
- clazz.getDeclaredConstructor().newInstance().apply {
- arguments = Bundle()
- arguments!!.putInt(FIELD_SLOT_ID, slotId)
- arguments!!.putInt(FIELD_PORT_ID, portId)
- arguments!!.addArguments()
+fun newInstanceEuicc(clazz: Class, slotId: Int, portId: Int, addArguments: Bundle.() -> Unit = {}): T where T: Fragment, T: EuiccChannelFragmentMarker {
+ val instance = clazz.newInstance()
+ instance.arguments = Bundle().apply {
+ putInt("slotId", slotId)
+ putInt("portId", portId)
+ addArguments()
}
+ return instance
+}
// Convenient methods to avoid using `channel` for these
// `channel` requires that the channel actually exists in EuiccChannelManager, which is
// not always the case during operations such as switching
-val T.slotId: Int
- where T : Fragment, T : EuiccChannelFragmentMarker
- get() = requireArguments().getInt(FIELD_SLOT_ID)
-val T.portId: Int
- where T : Fragment, T : EuiccChannelFragmentMarker
- get() = requireArguments().getInt(FIELD_PORT_ID)
-val T.isUsb: Boolean
- where T : Fragment, T : EuiccChannelFragmentMarker
- get() = slotId == EuiccChannelManager.USB_CHANNEL_ID
+val T.slotId: Int where T: Fragment, T: EuiccChannelFragmentMarker
+ get() = requireArguments().getInt("slotId")
+val T.portId: Int where T: Fragment, T: EuiccChannelFragmentMarker
+ get() = requireArguments().getInt("portId")
+val T.isUsb: Boolean where T: Fragment, T: EuiccChannelFragmentMarker
+ get() = requireArguments().getInt("slotId") == EuiccChannelManager.USB_CHANNEL_ID
-private fun T.requireEuiccActivity(): BaseEuiccAccessActivity
- where T : Fragment, T : OpenEuiccContextMarker =
- requireActivity() as BaseEuiccAccessActivity
+val T.euiccChannelManager: EuiccChannelManager where T: Fragment, T: OpenEuiccContextMarker
+ get() = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManager
+val T.euiccChannelManagerService: EuiccChannelManagerService where T: Fragment, T: OpenEuiccContextMarker
+ get() = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManagerService
-val T.euiccChannelManager: EuiccChannelManager
- where T : Fragment, T : OpenEuiccContextMarker
- get() = requireEuiccActivity().euiccChannelManager
-
-val T.euiccChannelManagerService: EuiccChannelManagerService
- where T : Fragment, T : OpenEuiccContextMarker
- get() = requireEuiccActivity().euiccChannelManagerService
-
-suspend fun T.withEuiccChannel(fn: suspend (EuiccChannel) -> R): R
- where T : Fragment, T : EuiccChannelFragmentMarker {
+suspend fun T.withEuiccChannel(fn: suspend (EuiccChannel) -> R): R where T : Fragment, T : EuiccChannelFragmentMarker {
ensureEuiccChannelManager()
return euiccChannelManager.withEuiccChannel(slotId, portId, fn)
}
-suspend fun T.ensureEuiccChannelManager() where T : Fragment, T : OpenEuiccContextMarker =
- requireEuiccActivity().euiccChannelManagerLoaded.await()
-
-fun T.notifyEuiccProfilesChanged() where T : Fragment {
- if (this !is EuiccProfilesChangedListener) return
- // Trigger a refresh in the parent fragment -- it should wait until
- // any foreground task is completed before actually doing a refresh
- this.onEuiccProfilesChanged()
-}
+suspend fun T.ensureEuiccChannelManager() where T: Fragment, T: OpenEuiccContextMarker =
+ (requireActivity() as BaseEuiccAccessActivity).euiccChannelManagerLoaded.await()
interface EuiccProfilesChangedListener {
fun onEuiccProfilesChanged()
diff --git a/app-common/src/main/java/im/angry/openeuicc/util/LPAString.kt b/app-common/src/main/java/im/angry/openeuicc/util/LPAString.kt
deleted file mode 100644
index 63a81f1..0000000
--- a/app-common/src/main/java/im/angry/openeuicc/util/LPAString.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-package im.angry.openeuicc.util
-
-data class LPAString(
- val address: String,
- val matchingId: String?,
- val oid: String?,
- val confirmationCodeRequired: Boolean,
-) {
- companion object {
- fun parse(input: String): LPAString {
- var token = input
- if (token.startsWith("LPA:", ignoreCase = true)) token = token.drop(4)
- val components = token.split('$').map { it.trim().ifBlank { null } }
- require(components.getOrNull(0) == "1") { "Invalid AC_Format" }
- return LPAString(
- requireNotNull(components.getOrNull(1)) { "SM-DP+ is required" },
- components.getOrNull(2),
- components.getOrNull(3),
- components.getOrNull(4) == "1"
- )
- }
- }
-
- override fun toString(): String {
- val parts = arrayOf(
- "1",
- address,
- matchingId ?: "",
- oid ?: "",
- if (confirmationCodeRequired) "1" else ""
- )
- return parts.joinToString("$").trimEnd('$')
- }
-}
\ No newline at end of file
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 2fef3db..f5e3ca2 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,14 +5,11 @@ 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
import im.angry.openeuicc.OpenEuiccApplication
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
-import java.util.Base64
private val Context.dataStore: DataStore by preferencesDataStore(name = "prefs")
@@ -34,42 +31,11 @@ internal object PreferenceKeys {
// ---- Developer Options ----
val DEVELOPER_OPTIONS_ENABLED = booleanPreferencesKey("developer_options_enabled")
- val REFRESH_AFTER_SWITCH = booleanPreferencesKey("refresh_after_switch")
val UNFILTERED_PROFILE_LIST = booleanPreferencesKey("unfiltered_profile_list")
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"
-
-internal object PreferenceConstants {
- val DEFAULT_AID_LIST = """
- # One AID per line. Comment lines start with #.
- # Refs:
-
- # eUICC standard
- $EUICC_DEFAULT_ISDR_AID
-
- # ESTKme AUX (deprecated, use SE0 instead)
- A06573746B6D65FFFFFFFF4953442D52
-
- # ESTKme SE0
- A06573746B6D65FFFF4953442D522030
-
- # eSIM.me
- A0000005591010000000008900000300
-
- # 5ber.eSIM
- A0000005591010FFFFFFFF8900050500
-
- # Xesim
- A0000005591010FFFFFFFF8900000177
- """.trimIndent()
-}
-
-open class PreferenceRepository(private val context: Context) {
+class PreferenceRepository(private val context: Context) {
// Expose flows so that we can also handle default values
// ---- Profile Notifications ----
val notificationDownloadFlow = bindFlow(PreferenceKeys.NOTIFICATION_DOWNLOAD, true)
@@ -81,51 +47,26 @@ open class PreferenceRepository(private val context: Context) {
val verboseLoggingFlow = bindFlow(PreferenceKeys.VERBOSE_LOGGING, false)
// ---- Developer Options ----
- val refreshAfterSwitchFlow = bindFlow(PreferenceKeys.REFRESH_AFTER_SWITCH, true)
val developerOptionsEnabledFlow = bindFlow(PreferenceKeys.DEVELOPER_OPTIONS_ENABLED, false)
val unfilteredProfileListFlow = bindFlow(PreferenceKeys.UNFILTERED_PROFILE_LIST, false)
val ignoreTLSCertificateFlow = bindFlow(PreferenceKeys.IGNORE_TLS_CERTIFICATE, false)
- val euiccMemoryResetFlow = bindFlow(PreferenceKeys.EUICC_MEMORY_RESET, false)
- val isdrAidListFlow = bindFlow(
- PreferenceKeys.ISDR_AID_LIST,
- 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,
- defaultValue: T,
- encoder: (T) -> T = { it },
- decoder: (T) -> T = { it }
- ): PreferenceFlowWrapper =
- PreferenceFlowWrapper(context, key, defaultValue, encoder, decoder)
+ private fun bindFlow(key: Preferences.Key, defaultValue: T): PreferenceFlowWrapper =
+ PreferenceFlowWrapper(context, key, defaultValue)
}
class PreferenceFlowWrapper private constructor(
private val context: Context,
private val key: Preferences.Key,
- inner: Flow,
- private val encoder: (T) -> T,
+ inner: Flow
) : Flow by inner {
- internal constructor(
- context: Context,
- key: Preferences.Key,
- defaultValue: T,
- encoder: (T) -> T,
- decoder: (T) -> T
- ) : this(
+ internal constructor(context: Context, key: Preferences.Key, defaultValue: T) : this(
context,
key,
- context.dataStore.data.map { it[key]?.let(decoder) ?: defaultValue },
- encoder
+ context.dataStore.data.map { it[key] ?: defaultValue }
)
suspend fun updatePreference(value: T) {
- context.dataStore.edit { it[key] = encoder(value) }
+ context.dataStore.edit { it[key] = value }
}
-
- suspend fun removePreference() {
- context.dataStore.edit { it.remove(key) }
- }
-}
+}
\ No newline at end of file
diff --git a/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt
index 57d150b..8d72462 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
@@ -1,7 +1,7 @@
package im.angry.openeuicc.util
fun String.decodeHex(): ByteArray {
- require(length % 2 == 0) { "Must have an even length" }
+ check(length % 2 == 0) { "Must have an even length" }
val decodedLength = length / 2
val out = ByteArray(decodedLength)
@@ -29,15 +29,72 @@ fun formatFreeSpace(size: Int): String =
"$size B"
}
-/**
- * Decode a list of potential ISDR AIDs, one per line. Lines starting with '#' are ignored.
- * If none is found, at least EUICC_DEFAULT_ISDR_AID is returned
- */
-fun parseIsdrAidList(s: String): List =
- s.split('\n')
- .map(String::trim)
- .filter { !it.startsWith('#') }
- .map(String::trim)
- .filter(String::isNotEmpty)
- .mapNotNull { runCatching(it::decodeHex).getOrNull() }
- .ifEmpty { listOf(EUICC_DEFAULT_ISDR_AID.decodeHex()) }
+fun String.prettyPrintJson(): String {
+ val ret = StringBuilder()
+ var inQuotes = false
+ var escaped = false
+ val indentSymbolStack = ArrayDeque()
+
+ val addNewLine = {
+ ret.append('\n')
+ repeat(indentSymbolStack.size) {
+ ret.append('\t')
+ }
+ }
+
+ var lastChar = ' '
+
+ for (c in this) {
+ when {
+ !inQuotes && (c == '{' || c == '[') -> {
+ ret.append(c)
+ indentSymbolStack.addLast(c)
+ addNewLine()
+ }
+
+ !inQuotes && (c == '}' || c == ']') -> {
+ indentSymbolStack.removeLast()
+ if (lastChar != ',') {
+ addNewLine()
+ }
+ ret.append(c)
+ }
+
+ !inQuotes && c == ',' -> {
+ ret.append(c)
+ addNewLine()
+ }
+
+ !inQuotes && c == ':' -> {
+ ret.append(c)
+ ret.append(' ')
+ }
+
+ inQuotes && c == '\\' -> {
+ ret.append(c)
+ escaped = true
+ continue
+ }
+
+ !escaped && c == '"' -> {
+ ret.append(c)
+ inQuotes = !inQuotes
+ }
+
+ !inQuotes && c == ' ' -> {
+ // Do nothing -- we ignore spaces outside of quotes by default
+ // This is to ensure predictable formatting
+ }
+
+ else -> ret.append(c)
+ }
+
+ if (escaped) {
+ escaped = false
+ }
+
+ lastChar = c
+ }
+
+ return ret.toString()
+}
\ No newline at end of file
diff --git a/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt
index c7c859d..a73d7fe 100644
--- a/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt
@@ -102,8 +102,8 @@ fun T.setupLogSaving(
AlertDialog.Builder(context).apply {
setMessage(R.string.logs_saved_message)
- setNegativeButton(android.R.string.cancel) { _, _ -> }
- setPositiveButton(android.R.string.ok) { _, _ ->
+ setNegativeButton(R.string.no) { _, _ -> }
+ setPositiveButton(R.string.yes) { _, _ ->
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
clipData = ClipData.newUri(context.contentResolver, lastFileName, uri)
diff --git a/app-common/src/main/java/im/angry/openeuicc/util/Utils.kt b/app-common/src/main/java/im/angry/openeuicc/util/Utils.kt
index 046657f..444c176 100644
--- a/app-common/src/main/java/im/angry/openeuicc/util/Utils.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/util/Utils.kt
@@ -54,9 +54,6 @@ interface OpenEuiccContextMarker {
val appContainer: AppContainer
get() = openEuiccApplication.appContainer
- val preferenceRepository: PreferenceRepository
- get() = appContainer.preferenceRepository
-
val telephonyManager: TelephonyManager
get() = appContainer.telephonyManager
}
@@ -89,13 +86,6 @@ suspend fun connectSEService(context: Context): SEService = suspendCoroutine { c
}
}
-inline fun Bitmap.use(f: (Bitmap) -> T): T =
- try {
- f(this)
- } finally {
- recycle()
- }
-
fun decodeQrFromBitmap(bmp: Bitmap): String? =
runCatching {
val pixels = IntArray(bmp.width * bmp.height)
diff --git a/app-common/src/main/java/im/angry/openeuicc/util/Vendors.kt b/app-common/src/main/java/im/angry/openeuicc/util/Vendors.kt
deleted file mode 100644
index 529f9ee..0000000
--- a/app-common/src/main/java/im/angry/openeuicc/util/Vendors.kt
+++ /dev/null
@@ -1,112 +0,0 @@
-package im.angry.openeuicc.util
-
-import android.util.Log
-import im.angry.openeuicc.core.ApduInterfaceAtrProvider
-import im.angry.openeuicc.core.EuiccChannel
-import net.typeblog.lpac_jni.Version
-
-data class EuiccVendorInfo(
- val skuName: String?,
- val serialNumber: String?,
- val bootloaderVersion: String?,
- val firmwareVersion: String?,
-)
-
-private val EUICC_VENDORS: Array = arrayOf(EstkMe(), SimLink())
-
-fun EuiccChannel.tryParseEuiccVendorInfo(): EuiccVendorInfo? {
- EUICC_VENDORS.forEach { vendor ->
- vendor.tryParseEuiccVendorInfo(this@tryParseEuiccVendorInfo)?.let {
- return it
- }
- }
-
- return null
-}
-
-interface EuiccVendor {
- fun tryParseEuiccVendorInfo(channel: EuiccChannel): EuiccVendorInfo?
-}
-
-private class EstkMe : EuiccVendor {
- companion object {
- private val PRODUCT_AID = "A06573746B6D65FFFFFFFFFFFF6D6774".decodeHex()
- private val PRODUCT_ATR_FPR = "estk.me".encodeToByteArray()
- }
-
- private fun checkAtr(channel: EuiccChannel): Boolean {
- val iface = channel.apduInterface
- if (iface !is ApduInterfaceAtrProvider) return false
- val atr = iface.atr ?: return false
- for (index in atr.indices) {
- if (atr.size - index < PRODUCT_ATR_FPR.size) break
- if (atr.sliceArray(index until index + PRODUCT_ATR_FPR.size)
- .contentEquals(PRODUCT_ATR_FPR)
- ) return true
- }
- return false
- }
-
- private fun decodeAsn1String(b: ByteArray): String? {
- if (b.size < 2) return null
- if (b[b.size - 2] != 0x90.toByte() || b[b.size - 1] != 0x00.toByte()) return null
- return b.sliceArray(0 until b.size - 2).decodeToString()
- }
-
- override fun tryParseEuiccVendorInfo(channel: EuiccChannel): EuiccVendorInfo? {
- if (!checkAtr(channel)) return null
-
- val iface = channel.apduInterface
- return try {
- iface.withLogicalChannel(PRODUCT_AID) { transmit ->
- fun invoke(p1: Byte) =
- decodeAsn1String(transmit(byteArrayOf(0x00, 0x00, p1, 0x00, 0x00)))
- EuiccVendorInfo(
- skuName = invoke(0x03),
- serialNumber = invoke(0x00),
- bootloaderVersion = invoke(0x01),
- firmwareVersion = invoke(0x02),
- )
- }
- } catch (e: Exception) {
- Log.d(TAG, "Failed to get ESTKmeInfo", e)
- null
- }
- }
-}
-
-private class SimLink : EuiccVendor {
- companion object {
- private val EID_PATTERN = Regex("^89044045(84|21)67274948")
- }
-
- override fun tryParseEuiccVendorInfo(channel: EuiccChannel): EuiccVendorInfo? {
- val eid = channel.lpa.eID
- val version = channel.lpa.euiccInfo2?.euiccFirmwareVersion
- if (version == null || EID_PATTERN.find(eid, 0) == null) return null
- val versionName = when {
- // @formatter:off
- version >= Version(37, 1, 41) -> "v3.1 (beta 1)"
- version >= Version(36, 18, 5) -> "v3 (final)"
- version >= Version(36, 17, 39) -> "v3 (beta)"
- version >= Version(36, 17, 4) -> "v2s"
- version >= Version(36, 9, 3) -> "v2.1"
- version >= Version(36, 7, 2) -> "v2"
- // @formatter:on
- else -> null
- }
-
- val skuName = if (versionName == null) {
- "9eSIM"
- } else {
- "9eSIM $versionName"
- }
-
- return EuiccVendorInfo(
- skuName = skuName,
- serialNumber = null,
- bootloaderVersion = null,
- firmwareVersion = null
- )
- }
-}
\ No newline at end of file
diff --git a/app-common/src/main/res/drawable/ic_euicc_memory_reset.xml b/app-common/src/main/res/drawable/ic_euicc_memory_reset.xml
deleted file mode 100644
index f1ca8c1..0000000
--- a/app-common/src/main/res/drawable/ic_euicc_memory_reset.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
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 c59673b..f1d0852 100644
--- a/app-common/src/main/res/layout/download_progress_item.xml
+++ b/app-common/src/main/res/layout/download_progress_item.xml
@@ -1,32 +1,30 @@
+ android:layout_height="wrap_content"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/download_progress_icon_container"
+ app:layout_constrainedWidth="true"
+ app:layout_constraintHorizontal_bias="0.0" />
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent">
-
-
-
-
\ 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 021c53b..58d55ab 100644
--- a/app-common/src/main/res/layout/euicc_profile.xml
+++ b/app-common/src/main/res/layout/euicc_profile.xml
@@ -54,7 +54,7 @@
-
-
diff --git a/app-common/src/main/res/layout/fragment_euicc.xml b/app-common/src/main/res/layout/fragment_euicc.xml
index c5fde7b..4ae7523 100644
--- a/app-common/src/main/res/layout/fragment_euicc.xml
+++ b/app-common/src/main/res/layout/fragment_euicc.xml
@@ -27,7 +27,6 @@
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
- android:contentDescription="@string/profile_download"
android:src="@drawable/ic_add"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
diff --git a/app-common/src/main/res/menu/activity_isdr_aid_list.xml b/app-common/src/main/res/menu/activity_isdr_aid_list.xml
deleted file mode 100644
index 99492d6..0000000
--- a/app-common/src/main/res/menu/activity_isdr_aid_list.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
\ No newline at end of file
diff --git a/app-common/src/main/res/menu/activity_main.xml b/app-common/src/main/res/menu/activity_main.xml
index c15663f..0e00292 100644
--- a/app-common/src/main/res/menu/activity_main.xml
+++ b/app-common/src/main/res/menu/activity_main.xml
@@ -3,7 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
diff --git a/app-common/src/main/res/menu/activity_notifications.xml b/app-common/src/main/res/menu/activity_notifications.xml
index b80e06e..87f96a6 100644
--- a/app-common/src/main/res/menu/activity_notifications.xml
+++ b/app-common/src/main/res/menu/activity_notifications.xml
@@ -4,6 +4,6 @@
\ No newline at end of file
diff --git a/app-common/src/main/res/menu/fragment_euicc.xml b/app-common/src/main/res/menu/fragment_euicc.xml
index 6e2dfbe..b54eaf1 100644
--- a/app-common/src/main/res/menu/fragment_euicc.xml
+++ b/app-common/src/main/res/menu/fragment_euicc.xml
@@ -10,9 +10,4 @@
android:id="@+id/euicc_info"
android:title="@string/euicc_info"
app:showAsAction="never" />
-
-
\ No newline at end of file
diff --git a/app-common/src/main/res/menu/fragment_profile_rename.xml b/app-common/src/main/res/menu/fragment_profile_rename.xml
index f55c56c..bde850f 100644
--- a/app-common/src/main/res/menu/fragment_profile_rename.xml
+++ b/app-common/src/main/res/menu/fragment_profile_rename.xml
@@ -4,6 +4,6 @@
\ No newline at end of file
diff --git a/app-common/src/main/res/menu/profile_options.xml b/app-common/src/main/res/menu/profile_options.xml
index 60722d6..6add53d 100644
--- a/app-common/src/main/res/menu/profile_options.xml
+++ b/app-common/src/main/res/menu/profile_options.xml
@@ -2,18 +2,18 @@
\ 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 b171972..b592ec3 100644
--- a/app-common/src/main/res/values-ja/strings.xml
+++ b/app-common/src/main/res/values-ja/strings.xml
@@ -2,30 +2,23 @@
このアプリでアクセスできるリムーバブル eUICC カードがデバイス上で検出されていません。互換性のあるカード挿入または USB リーダーを接続してください。
この eSIM にはプロファイルがありません。
- ヘルプ
- スロットを再読み込み
- 不明
+ 不明
+ 情報なし
+ ヘルプ
+ スロットを再読み込み
論理スロット %d
-
- 有効化済み
- 無効化済み
- プロバイダー:
- クラス:
- テスト中
- プロビジョニング
- 稼働中
- 有効化
- 無効化
- 削除
- 名前を変更
- eSIM チップがプロファイルの切り替えの待機中にタイムアウトしました。これはデバイスのモデムファームウェアのバグの可能性があります。機内モードに切り替えるかアプリを再起動、デバイスを再起動してください。
- 操作は成功しましたが、デバイスのモデムが更新を拒否しました。新しいプロファイルを使用するには機内モードに切り替えるか、再起動する必要があります。
+ 有効済み
+ 無効済み
+ プロバイダー:
+ 有効化
+ 無効化
+ 削除
+ 名前を変更
+ eSIM チップがプロファイルの切り替えの待機中にタイムアウトしました。これはデバイスのモデムファームウェアのバグの可能性があります。機内モードに切り替えるかアプリを再起動、デバイスを再起動してください。
+ 操作は成功しましたが、デバイスのモデムが更新を拒否しました。新しいプロファイルを使用するには機内モードに切り替えるか、再起動する必要があります。
新しい eSIM プロファイルに切り替えることができません。
- 確認文字列が一致しません
- 確認文字列が一致しません
- このチップは消去されました
+ 入力した確認用テキストは一致していません
ICCID をクリップボードにコピーしました
- シリアル番号をクリップボードにコピーしました
EID をクリップボードにコピーしました
ATR をクリップボードにコピーしました
USB の権限を許可
@@ -40,23 +33,20 @@
eSIM プロファイルの削除に失敗しました
eSIM プロファイルを切り替え中
eSIM プロファイルの切り替えに失敗しました
- eSIM チップを消去中
- eSIM チップの消去に失敗しました
新しい eSIM
サーバー (RSP / SM-DP+)
アクティベーションコード
確認コード (オプション)
- 確認コード (必須)
IMEI (オプション)
- 残りの容量が少量です
- 残り容量が少ないため、このプロファイルのダウンロードに失敗する可能性があります。
- クリップボードに LPA コードがありません
- 解析できません
- QR コードまたはクリップボードの内容を LPA コードとして解析できませんでした。
+ ダウンロードに失敗する可能性があります
+ 残り容量が少ないため、ダウンロードに失敗する可能性があります。
+ クリップボードに LPA コードが見つかりません
+ LPA コードを解析できません
+ クリップボードまたは QR コードの内容を LPA コードとして解析できません
ダウンロードウィザード
戻る
次へ
- 選択した SIM が削除されました
+ 選択された SIM が取り外されました
ダウンロードする eSIM を選択または確認:
タイプ:
リムーバブル
@@ -86,33 +76,12 @@
最終の APDU レスポンス (SIM) は失敗しました
最終の APDU 例外:
保存
- 「%s」での診断
- この eSIM プロファイルはすでに eSIM チップに存在します。
- eSIM チップにはプロファイルをダウンロードするのに必要なメモリが残っていません。
- この eSIM プロファイルは、eSIM チップではサポートされていません。
- eSIM チップでエラーが発生しました。
- 使用しているデバイスまたは eSIM チップの EID は、通信事業者によってサポートされていません。
- この eSIM プロファイルは、別のデバイスにダウンロードされています。
- この eSIM プロファイルは取り消されました。
- アクティベーションコードが無効です。
- eSIM プロファイルのダウンロード試行回数の上限を超えました。
- このプロファイルをダウンロードするには確認コードが必要です。
- 入力した確認コードは無効です。
- この eSIM プロファイルは有効期限が切れています。
- 確認コードのダウンロード試行回数の上限を超えました。
- 不明な SM-DP+ アドレス
- ネットワークにアクセスできません
- TLS 証明書エラー、この eSIM プロファイルはサポートされていません
- ダウンロード済みの eSIM プロファイルを再インストールしようとしています
- 未使用の eSIM プロファイルをいくつか削除して、再度お試しください
- サポートについては、通信事業者にお問い合わせください。
- この eSIM プロファイルを再発行するには、通信事業者にお問い合わせください。
- 別のネットワークに接続後 (例: Wi-Fi とデータを切り替え) を行った後に再度お試しください。
- ログは共有したパスに保存されました。別のアプリで共有しますか?
+ %s のエラー診断
+ ログは指定されたパスに保存しました。他のアプリにシェアしますか?
新しいニックネーム
- ニックネームを UTF-8 にエンコードできませんでした
- ニックネームが 64 文字を超えています
- プロファイルの名前変更時に不明なエラーが発生しました
+ ニックネームを UTF-8 にエンコードできません
+ ニックネームは 64 文字以内にしてください
+ ニックネームの変更で予期せぬエラーが発生しました
%s のプロファイルを削除してもよろしいですか?この操作は元に戻せません。
削除を確認するには「%s」を入力してください
通知
@@ -121,82 +90,58 @@
eSIM プロファイルはダウンロードや削除、有効化や無効化されたときに通信事業者に通知を送信できます。送信されるこれらの通知のキューはここにリストされます。\n\n設定では、各タイプの通知を自動的に送信するかどうかを指定できます。通知が送信された場合でもキューのスペースが不足していない限り、記録から自動的に削除されることはありません。\n\nここでは保留中の各通知を手動で送信または削除できます。
ダウンロードしました
削除しました
- 有効化済み
- 無効化済み
+ 有効化しました
+ 無効化しました
処理
削除
eUICC 情報
eUICC 情報 (%s)
アクセスモード
リムーバブル
- 製品名
- 製品シリアル番号
- 製品ブートローダーバージョン
- 製品ファームウェアバージョン
SGP.22 バージョン
- eUICC OS バージョン
- グローバルプラットフォームのバージョン
+ eUICC OS のバージョン
+ グローバルプラットフォームのバージョン
SAS 認定番号
- 保護されたプロファイルのバージョン
+ Protected Profileのバージョン
NVRAM の空き容量 (eSIM プロファイルストレージ)
- (参照用)
- 証明書発行者 (CI)
- GSMA ライブ CI
+ 証明書の発行者 (CI)
+ GSMA プロダクション CI
GSMA テスト CI
- 不明な eSIM CI
- eUICC を消去
- eUICC を消去
- このチップ上のすべてのプロファイルを削除することを確認してください。この操作は元に戻せないことを理解してください。\n\nEID: %1$s\n\n%2$s
- 確認のために「%s」を入力してください
- EID が %s で終わるチップを消去することを確認し、この操作は元に戻せないことを理解してください
- 消去
-
- はい
- いいえ
- 不明
- 情報がありません
+ 未知の eSIM CI
+ はい
+ いいえ
保存
%s のログ
開発者になるまであと %d ステップです。
あなたは開発者になりました!
- ISD-R AID リスト
- カスタム ISD-R AID リストを保存しました。
- リセット
設定
通知
- eSIM のプロファイル操作により、通信事業者に通知が送信されます。必要に応じてこの動作を微調整できます。
+ eSIM のプロファイル操作により、通信事業者に通知が送信されます。ここでは、どのタイプの通知を送信するのかを微調整できます。
ダウンロード
- プロファイルをダウンロード中の通知を送信します
+ プロファイルのダウンロード済みの通知を送信します
削除
- プロファイルを削除中の通知を送信します
- 切り替え中
- プロファイルを切り替え中の通知を送信します\nこのタイプの通知は信頼できないことに注意してください。
+ プロファイルの削除済みの通知を送信します
+ 切り替え
+ プロファイルの切り替え済みの通知を送信します\nこのタイプの通知は有効化しても必ず送信するとは限らないことに注意してください。
高度な設定
- プロファイルの無効化と削除を許可
- デフォルトでは、このアプリでデバイスに挿入されたリムーバブル eSIM の有効なプロファイルを無効化することを防いでいます。なぜなのかというと時々アクセスができなくなるからです。\nこのチェックボックスを ON にすることで、この保護機能を解除します。
+ 有効なプロファイルの無効化と削除を許可する
+ デフォルトでは、このアプリでデバイスに挿入された取り外し可能な eSIM の有効なプロファイルを無効化することを防いでいます。なぜなのかというと時々アクセスができなくなるからです。\nこのチェックボックスを ON にすることで、この保護機能を解除します。
詳細ログ
詳細ログを有効化します。これには個人的な情報が含まれている可能性があります。この機能を ON にした後は、信頼できるユーザーとのみログを共有してください。
- 言語
- アプリの言語
ログ
アプリの最新デバッグログを表示します
開発者オプション
- モデムに更新コマンドを送信
- プロファイルを切り替えた後にモデムに更新コマンドを送信するかどうかを設定します。クラッシュが発生する場合は、この機能を無効化してください。
- フィルタリングされていないプロファイル一覧を表示
- 非運用のプロファイルも含めます
SM-DP+ TLS 証明書を無視する
- RSP サーバーで使用される TLS 証明書を受け入れます
- eUICC の消去を許可
- これは危険な操作であり、デフォルトでは非表示になっています。代わりとしてすべてのプロファイルを手動で削除することもできます。
- グローバル ES10x MSS
-
- - 高速
- - 互換モード
-
- ISD-R AID リストをカスタマイズ
- 一部ブランドのリムーバブル eUICC は独自の非標準な ISD-R AID を使用しているため、サードパーティー製アプリからアクセスできない場合があります。このリストに追加された非標準な AID の使用を試みますが、動作の保証はできません。
+ SM-DP+ TLS 証明書を無視して任意の RSP を許可します
情報
アプリバージョン
ソースコード
+ 言語
+ アプリの言語を選択
+ すべてのプロファイルを表示
+ プロダクション以外のプロファイルも表示する
+ タイプ:
+ テスティング
+ 準備中
+ 動作中
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 7303d2d..cf51734 100644
--- a/app-common/src/main/res/values-zh-rCN/strings.xml
+++ b/app-common/src/main/res/values-zh-rCN/strings.xml
@@ -2,21 +2,20 @@
在此设备上未检测到此应用程序可访问的可插拔 eUICC 卡。请插入兼容卡或 USB 读卡器。
此 eSIM 上还没有配置文件
- 未知
- 帮助
- 重新加载卡槽
- 未知
+ 未知
+ 帮助
+ 重新加载卡槽
逻辑卡槽 %d
- 已启用
- 已禁用
- 提供商:
+ 已启用
+ 已禁用
+ 提供商:
类型:
- 启用
- 禁用
- 删除
- 重命名
- 等待 eSIM 芯片切换配置文件时超时。这可能是您手机基带固件中的一个错误。请尝试切换飞行模式、重新启动应用程序或重新启动手机
- 操作成功, 但是您手机的基带拒绝刷新。您可能需要切换飞行模式或重新启动,以便使用新的配置文件。
+ 启用
+ 禁用
+ 删除
+ 重命名
+ 等待 eSIM 芯片切换配置文件时超时。这可能是您手机基带固件中的一个错误。请尝试切换飞行模式、重新启动应用程序或重新启动手机
+ 操作成功, 但是您手机的基带拒绝刷新。您可能需要切换飞行模式或重新启动,以便使用新的配置文件。
无法切换到新的 eSIM 配置文件。
输入的确认文本不匹配
已复制 ICCID 到剪贴板
@@ -38,16 +37,9 @@
服务器 (RSP / SM-DP+)
激活码
确认码 (可选)
- 已复制序列号到剪贴板
- 产品名称
- 产品序列号
- 产品 Bootloader 版本
- 产品固件版本
- 确认码 (必需)
IMEI (可选)
- 剩余空间不足
+ 本次下载可能会失败
当前芯片的剩余空间不足,可能导致配置下载失败。\n是否继续下载?
- 请连接到其他网络(例如在 Wi-Fi 和数据之间切换)后重试。
日志已保存到指定路径。需要通过其他 App 分享吗?
新昵称
无法将昵称编码为 UTF-8
@@ -67,7 +59,6 @@
删除
保存日志
%s 的日志
- 自定义 ISD-R AID 列表已保存
设置
通知
操作 eSIM 配置文件会向运营商发送通知。根据需要在此处微调此行为。
@@ -84,12 +75,6 @@
详细日志中包含敏感信息,开启此功能后请仅与你信任的人共享你的日志。
日志
查看应用程序的最新调试日志
- 某些品牌的可移除 eUICC 可能会使用自己的非标准 ISD-R AID,导致第三方应用无法访问。此 App 可以尝试使用此列表中添加的非标准 AID,但不能保证它们一定有效。
- 全局 ES10x MSS
-
- - 最佳效率
- - 最佳兼容性
-
信息
App 版本
源码
@@ -139,62 +124,24 @@
可插拔
SGP.22 版本
eUICC OS 版本
- GlobalPlatform 版本
+ GlobalPlatform 版本
SAS 认证号码
Protected Profile 版本
NVRAM 剩余空间 (eSIM 存储容量)
- (仅供参考)
证书签发者 (CI)
GSMA 生产环境 CI
GSMA 测试 CI
未知 eSIM CI
- 是
- 否
+ 是
+ 否
还有 %d 步成为开发者
你现在是开发者了!
语言
选择 App 语言
开发者选项
- 切换配置文件后是否向基带发送刷新命令。如果发现崩溃,请尝试禁用此功能。
显示未经过滤的配置文件列表
在配置文件列表中包括非生产环境的配置文件
无视 SM-DP+ 的 TLS 证书
允许 RSP 服务器使用任意证书
- 无信息
- 输入的确认文本不匹配
- 此芯片已被擦除
- 正在擦除 eSIM 芯片
- eSIM 芯片擦除失败
- 擦除 eSIM 芯片
- 擦除 eSIM 芯片
- 请确认删除此芯片上的所有配置文件,并了解此操作不可逆。\n\nEID: %1$s\n\n%2$s
- 请在此处输入「%s」以确认
- 我确认擦除 EID 以 %s 结尾的芯片,并了解此操作不可逆
- 擦除
- 允许擦除 eUICC
- 此操作是默认隐藏的危险操作。作为替代方案,您可以手动删除所有配置文件。
- 向基带发送刷新命令
- 自定义 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
deleted file mode 100644
index ef6c842..0000000
--- a/app-common/src/main/res/values-zh-rTW/strings.xml
+++ /dev/null
@@ -1,196 +0,0 @@
-
-
- 在此裝置上未檢測到此應用程式可訪問的可插拔 eUICC 卡。請插入相容卡或 USB 晶片讀卡機。
- 此 eSIM 上還沒有設定檔
- 未知
- 幫助
- 重新載入卡槽
- 未知
- 虛擬卡槽 %d
- 已啟用
- 已停用
- 電信業者:
- 類型:
- 啟用
- 停用
- 刪除
- 重新命名
- 等待 eSIM 切換設定檔時逾時。這可能是您手機基頻處理器韌體中的一個錯誤。請嘗試切換飛航模式、重新啟動應用程式或重新啟動手機
- 操作成功, 但是您手機的基頻處理器沒有重新整理。您可能需要切換飛航模式或重新啟動,以便使用新的設定檔。
- 無法切換到新的 eSIM 設定檔。
- 輸入的確認文字不匹配
- 已複製 ICCID 到剪貼簿
- 已複製 EID 到剪貼簿
- 已複製 ATR 到剪貼簿
- 授予 USB 權限
- 需要獲得訪問 USB 晶片讀卡機的權限。
- 無法透過 USB 晶片讀卡機連線到 eSIM。
- 長時間運行的背景作業
- 正在下載 eSIM 設定檔
- 無法下載 eSIM 設定檔
- 正在重新命名 eSIM 設定檔
- 無法重新命名 eSIM 設定檔
- 正在刪除 eSIM 設定檔
- 無法刪除 eSIM 設定檔
- 正在切換 eSIM 設定檔
- 無法切換 eSIM 設定檔
- 新增新 eSIM
- 伺服器 (RSP / SM-DP+)
- 啟用碼
- 確認碼 (可選)
- 已複製序號到剪貼簿
- 產品名稱
- 產品序號
- 產品引導程式版本
- 產品韌體版本
- 確認碼 (必需)
- IMEI (可選)
- 剩餘空間不足
- 目前晶片的剩餘空間不足,可能導致配置下載失敗。\n是否繼續下載?
- 請連接到其他網路(例如在 Wi-Fi 和資料之間切換)後重試。
- 日誌已儲存到指定路徑。需要透過其他 App 分享嗎?
- 新名稱
- 無法將名稱編碼為 UTF-8
- 名稱長於 64 字元
- 重新命名設定檔時發生了未知錯誤
- 您確定要刪除 %s 嗎?此動作無法還原。
- 請輸入\'%s\'以確認刪除
- 通知列表
- 通知列表 (%s)
- 管理通知
- eSIM 設定檔可以在下載、刪除、啟用或停用時向電信業者傳送通知。此處列出了要傳送的這些通知的佇列。\n\n在\"設定\"中,您可以指定是否自動傳送每種型別的通知。請注意,即使通知已傳送,也不會自動從記錄中刪除,除非佇列空間不足。\n\n在這裡,您可以手動傳送或刪除每個待處理的通知。
- 已下載
- 已刪除
- 已啟用
- 已停用
- 處理
- 刪除
- 儲存日誌
- %s 的日誌
- 自訂 ISD-R AID 列表已儲存
- 設定
- 通知
- 變更 eSIM 設定檔會向電信業者傳送通知。根據需要在此處微調此行為。
- 下載
- 傳送 下載 設定檔的通知
- 刪除
- 傳送 刪除 設定檔的通知
- 切換
- 記錄詳細日誌
- 詳細日誌中包含敏感資訊,開啟此功能後請僅與你信任的人共享你的日誌。
- 日誌
- 檢視應用程式的最新除錯日誌
- 傳送 切換 設定檔的通知\n注意,這種型別的通知是不可靠的。
- 進階
- 允許 停用/刪除 已啟用的設定檔
- 預設情況下,此應用程式會阻止您停用可插拔 eSIM 中已啟用的設定檔。\n因為這樣做 有時 會導致無法存取。\n勾選此框以 移除 此保護措施。
- 某些品牌的可移除 eUICC 可能會使用自己的非標準 ISD-R AID,導致第三方應用程式無法存取。此 App 可以嘗試使用此清單中新增的非標準 AID,但不能保證它們一定有效。
- 全局 ES10x MSS
- 資訊
- App 版本
- 原始碼
- 測試
- 準備中
- 可用
- 未在剪貼簿上發現 LPA 碼
- LPA 碼解析錯誤
- 無法將二維碼或剪貼簿內容解析為 LPA 碼
- 下載精靈
- 返回
- 下一步
- 您選擇的 SIM 卡已被移除
- 請選擇或確認下載目標 eSIM 卡槽:
- 型別:
- 可插拔
- 內建
- 內建, 埠 %d
- 目前設定檔:
- 剩餘空間:
- 您想要如何下載 eSIM 設定檔?
- 用相機掃描二維碼
- 從相簿選擇二維碼
- 從剪貼簿讀取
- 手動輸入
- 請輸入或確認下載 eSIM 的詳細資訊:
- 正在下載您的 eSIM...
- 準備中
- 正在連線到伺服器
- 正在向伺服器驗證您的裝置
- 正在下載 eSIM 設定檔
- 正在寫入 eSIM 設定檔
- 錯誤診斷
- 錯誤代碼: %s
- 上次 HTTP 狀態碼 (來自伺服器): %d
- 上次 HTTP 應答 (來自伺服器):
- 上次 HTTP 錯誤:
- 上次 APDU 應答 (來自 SIM): %s
- 上次 APDU 應答 (來自 SIM) 是成功的
- 上次 APDU 應答 (來自 SIM) 是失敗的
- 上次 APDU 錯誤:
- 儲存
- %s 的錯誤診斷
- eUICC 詳情
- eUICC 詳情 (%s)
- 訪問方式
- 可插拔
- SGP.22 版本
- eUICC OS 版本
- GlobalPlatform 版本
- SAS 認證號碼
- Protected Profile 版本
- NVRAM 剩餘空間 (eSIM 儲存容量)
- (僅供參考)
- 證書簽發者 (CI)
- GSMA 生產環境 CI
- GSMA 測試 CI
- 未知 eSIM CI
- 是
- 否
- 還有 %d 步成為開發者
- 您現在是開發者了!
- 語言
- 選擇 App 語言
- 開發人員選項
- 切換設定檔後是否向基帶發送刷新命令。如果發現崩潰,請嘗試停用此功能。
- 顯示未經過濾的設定檔列表
- 在設定檔列表中包括非生產環境的設定檔
- 忽略 SM-DP+ 的 TLS 證書
- 允許 RSP 伺服器使用任意證書
- 無資訊
- 輸入的確認文字不匹配
- 此晶片已被擦除
- 正在擦除 eSIM 晶片
- eSIM 晶片擦除失敗
- 擦除 eSIM 晶片
- 擦除 eSIM 晶片
- 請確認刪除此晶片上的所有配置文件,並了解此操作不可逆。\n\nEID: %1$s\n\n%2$s
- 請在此輸入「%s」以確認
- 我確認擦除 EID 以 %s 結尾的晶片,並了解此操作不可逆
- 擦除
- 允許擦除 eUICC
- 此操作是預設隱藏的危險操作。作為替代方案,您可以手動刪除所有設定檔。
- 向基帶發送刷新命令
- 自訂 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 39a762f..d3bce00 100644
--- a/app-common/src/main/res/values/strings.xml
+++ b/app-common/src/main/res/values/strings.xml
@@ -2,41 +2,35 @@
No removable eUICC card accessible by this app is detected on this device. Insert a compatible card or a USB reader.
No profiles (yet) on this eSIM.
-
- Help
-
- Reload Slots
- Unknown
+ Unknown
+ Information Unavailable
+ Help
+ Reload Slots
Logical Slot %d
- USB
- OpenMobile API (OMAPI)
+ USB
+ OpenMobile API (OMAPI)
-
- Enabled
- Disabled
- Provider:
+ Enabled
+ Disabled
+ Provider:
Class:
Testing
Provisioning
Operational
- ICCID:
- #%d
+ ICCID:
- Enable
- Disable
- Delete
- Rename
+ Enable
+ Disable
+ Delete
+ Rename
- Timed out waiting for the eSIM chip to switch profiles. This may be a bug in your phone\'s modem firmware. Try toggling airplane mode, restarting the application, or rebooting the phone.
- The operation was successful, but your phone\'s modem refused to refresh. You might need to toggle airplane mode or reboot in order to use the new profile.
+ Timed out waiting for the eSIM chip to switch profiles. This may be a bug in your phone\'s modem firmware. Try toggling airplane mode, restarting the application, or rebooting the phone.
+ The operation was successful, but your phone\'s modem refused to refresh. You might need to toggle airplane mode or reboot in order to use the new profile.
Cannot switch to new eSIM profile.
Confirmation string mismatch
- Confirmation string mismatch
- This chip has been erased
ICCID copied to clipboard
- Serial number copied to clipboard
EID copied to clipboard
ATR copied to clipboard
@@ -53,18 +47,15 @@
Failed to delete eSIM profile
Switching eSIM profile
Failed to switch eSIM profile
- Erasing eSIM chip
- Failed to erase eSIM chip
New eSIM
Server (RSP / SM-DP+)
Activation Code
Confirmation Code (Optional)
- Confirmation Code (Required)
IMEI (Optional)
- Low remaining capacity
- This profile may fail to download due to low remaining capacity.
+ This download may fail
+ This download may fail due to low remaining capacity.
No LPA code found in clipboard
Unable to parse
Could not parse QR code or clipboard content as a LPA code.
@@ -104,27 +95,6 @@
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?
@@ -153,37 +123,21 @@
eUICC Info (%s)
Access Mode
Removable
- Product Name
- Product Serial Number
- Product Bootloader Version
- Product Firmware Version
EID
- 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
Unknown eSIM CI
Answer To Reset (ATR)
- Erase eUICC
- Erase eUICC
- Please confirm to delete all profiles on this chip and understand that this operation is irreversible.\n\nEID: %1$s\n\n%2$s
- Type \'%s\' here to confirm
- I CONFIRM TO ERASE THE CHIP WHOSE EID ENDS WITH %s AND UNDERSTAND THAT THIS IS IRREVERSIBLE
- Erase
-
-
- Yes
- No
- Unknown
- Information Unavailable
+ Yes
+ No
Save
Logs at %s
@@ -191,10 +145,6 @@
You are %d steps away from being a developer.
You are now a developer!
- ISD-R AID List
- Saved custom ISD-R AID list.
- Reset
-
Settings
Notifications
eSIM profile operations send notifications to the carrier. Fine-tune this behavior as needed here.
@@ -214,26 +164,10 @@
Logs
View recent debug logs of the application
Developer Options
- Send refresh command to modem
- Whether to send a refresh command to the modem after switching profiles. Try disabling this if you see crashes.
Show unfiltered profile list
Include non-production profiles in the list
Ignore SM-DP+ TLS certificate
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
App Version
Source Code
diff --git a/app-common/src/main/res/xml/locale_config.xml b/app-common/src/main/res/xml/locale_config.xml
index 6d7f076..e1a13f8 100644
--- a/app-common/src/main/res/xml/locale_config.xml
+++ b/app-common/src/main/res/xml/locale_config.xml
@@ -3,5 +3,4 @@
-
\ No newline at end of file
diff --git a/app-common/src/main/res/xml/pref_settings.xml b/app-common/src/main/res/xml/pref_settings.xml
index 831b04d..bb5bd50 100644
--- a/app-common/src/main/res/xml/pref_settings.xml
+++ b/app-common/src/main/res/xml/pref_settings.xml
@@ -52,17 +52,11 @@
-
-
-
-
-
-
-
-
-
-
+
+ android:label="@string/compatibility_check" />
by lazy { getCompatibilityChecks(this) }
+ private val adapter = CompatibilityChecksAdapter()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ enableEdgeToEdge()
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_compatibility_check)
+ setSupportActionBar(requireViewById(im.angry.openeuicc.common.R.id.toolbar))
+ setupToolbarInsets()
+ supportActionBar!!.setDisplayHomeAsUpEnabled(true)
+
+ compatibilityCheckList = requireViewById(R.id.recycler_view).also {
+ it.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
+ it.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
+ it.adapter = adapter
+ }
+
+ setupRootViewInsets(compatibilityCheckList)
+ }
+
+ @SuppressLint("NotifyDataSetChanged")
+ override fun onStart() {
+ super.onStart()
+ lifecycleScope.launch {
+ compatibilityChecks.executeAll { adapter.notifyDataSetChanged() }
+ }
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean =
+ when (item.itemId) {
+ android.R.id.home -> {
+ finish()
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+
+ inner class ViewHolder(private val root: View): RecyclerView.ViewHolder(root) {
+ private val titleView: TextView = root.requireViewById(R.id.compatibility_check_title)
+ private val descView: TextView = root.requireViewById(R.id.compatibility_check_desc)
+ private val statusContainer: ViewGroup = root.requireViewById(R.id.compatibility_check_status_container)
+
+ fun bindItem(item: CompatibilityCheck) {
+ titleView.text = item.title
+ descView.text = Html.fromHtml(item.description, Html.FROM_HTML_MODE_COMPACT)
+
+ statusContainer.children.forEach {
+ it.isVisible = false
+ }
+
+ val viewId = when (item.state) {
+ CompatibilityCheck.State.SUCCESS -> R.id.compatibility_check_checkmark
+ CompatibilityCheck.State.FAILURE -> R.id.compatibility_check_error
+ CompatibilityCheck.State.FAILURE_UNKNOWN -> R.id.compatibility_check_unknown
+ else -> R.id.compatibility_check_progress_bar
+ }
+ root.requireViewById(viewId).isVisible = true
+ }
+ }
+
+ inner class CompatibilityChecksAdapter: RecyclerView.Adapter() {
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
+ ViewHolder(layoutInflater.inflate(R.layout.compatibility_check_item, parent, false))
+
+ override fun getItemCount(): Int =
+ compatibilityChecks.indexOfLast { it.state != CompatibilityCheck.State.NOT_STARTED } + 1
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ holder.bindItem(compatibilityChecks[position])
+ }
+ }
+}
\ No newline at end of file
diff --git a/app-unpriv/src/main/java/im/angry/openeuicc/ui/QuickCompatibilityActivity.kt b/app-unpriv/src/main/java/im/angry/openeuicc/ui/QuickCompatibilityActivity.kt
deleted file mode 100644
index d5e599f..0000000
--- a/app-unpriv/src/main/java/im/angry/openeuicc/ui/QuickCompatibilityActivity.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package im.angry.openeuicc.ui
-
-import android.os.Bundle
-import androidx.activity.enableEdgeToEdge
-import androidx.appcompat.app.AppCompatActivity
-import im.angry.easyeuicc.R
-import im.angry.openeuicc.di.UnprivilegedUiComponentFactory
-import im.angry.openeuicc.util.OpenEuiccContextMarker
-
-class QuickCompatibilityActivity : AppCompatActivity(), OpenEuiccContextMarker {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- enableEdgeToEdge()
- setContentView(R.layout.activity_quick_compatibility)
-
- val quickCompatibilityFragment =
- (appContainer.uiComponentFactory as UnprivilegedUiComponentFactory)
- .createQuickCompatibilityFragment()
-
- supportFragmentManager.beginTransaction()
- .replace(R.id.quick_compatibility_container, quickCompatibilityFragment)
- .commit()
- }
-}
diff --git a/app-unpriv/src/main/java/im/angry/openeuicc/ui/QuickCompatibilityFragment.kt b/app-unpriv/src/main/java/im/angry/openeuicc/ui/QuickCompatibilityFragment.kt
deleted file mode 100644
index 9b41730..0000000
--- a/app-unpriv/src/main/java/im/angry/openeuicc/ui/QuickCompatibilityFragment.kt
+++ /dev/null
@@ -1,186 +0,0 @@
-package im.angry.openeuicc.ui
-
-import android.content.pm.PackageManager
-import android.icu.text.ListFormatter
-import android.os.Build
-import android.os.Bundle
-import android.se.omapi.Reader
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.Button
-import android.widget.CheckBox
-import android.widget.TextView
-import androidx.core.view.isVisible
-import androidx.fragment.app.Fragment
-import androidx.lifecycle.lifecycleScope
-import im.angry.easyeuicc.R
-import im.angry.openeuicc.util.EUICC_DEFAULT_ISDR_AID
-import im.angry.openeuicc.util.UnprivilegedEuiccContextMarker
-import im.angry.openeuicc.util.connectSEService
-import im.angry.openeuicc.util.decodeHex
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.withContext
-
-open class QuickCompatibilityFragment : Fragment(), UnprivilegedEuiccContextMarker {
- companion object {
- enum class Compatibility {
- COMPATIBLE,
- NOT_COMPATIBLE,
- }
-
- data class CompatibilityResult(
- val compatibility: Compatibility,
- val slotsOmapi: List = emptyList(),
- val slotsIsdr: List = emptyList()
- )
- }
-
- private val conclusion: TextView by lazy {
- requireView().requireViewById(R.id.quick_compatibility_conclusion)
- }
-
- private val resultSlots: TextView by lazy {
- requireView().requireViewById(R.id.quick_compatibility_result_slots)
- }
-
- private val resultSlotsIsdr: TextView by lazy {
- requireView().requireViewById(R.id.quick_compatibility_result_slots_isdr)
- }
-
- private val resultNotes: TextView by lazy {
- requireView().requireViewById(R.id.quick_compatibility_result_notes)
- }
-
- private val skipCheckBox: CheckBox by lazy {
- requireView().requireViewById(R.id.quick_compatibility_skip)
- }
-
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View = inflater.inflate(R.layout.fragment_quick_compatibility, container, false).apply {
- requireViewById(R.id.quick_compatibility_device_information)
- .text = formatDeviceInformation()
- requireViewById