diff --git a/.forgejo/workflows/build-debug.yml b/.forgejo/workflows/build-debug.yml
index 660dabc..0818b8b 100644
--- a/.forgejo/workflows/build-debug.yml
+++ b/.forgejo/workflows/build-debug.yml
@@ -33,14 +33,23 @@ jobs:
uses: https://gitea.angry.im/actions/setup-android@v3
- name: Build Debug APKs
- run: ./gradlew --no-daemon assembleDebug
+ run: ./gradlew --no-daemon assembleDebug :app:assembleDebugMagiskModuleDir
- name: Copy Artifacts
- run: find . -name 'app*-debug.apk' -exec cp {} . \;
+ run: |
+ find . -name 'app*-debug.apk' -exec cp {} . \;
+ cp -r app/build/magisk/debug ./magisk-debug
- - name: Upload Artifacts
+ - name: Upload APK Artifacts
uses: https://gitea.angry.im/actions/upload-artifact@v3
with:
name: Debug APKs
compression-level: 0
path: app*-debug.apk
+
+ - name: Upload Magisk Artifacts
+ uses: https://gitea.angry.im/actions/upload-artifact@v3
+ with:
+ name: magisk-debug
+ compression-level: 0
+ path: magisk-debug
diff --git a/.gitmodules b/.gitmodules
index f888959..863f185 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,3 @@
[submodule "libs/lpac-jni/src/main/jni/lpac"]
path = libs/lpac-jni/src/main/jni/lpac
- url = https://github.com/estkme/lpac
+ url = https://github.com/estkme-group/lpac.git
diff --git a/.idea/.gitignore b/.idea/.gitignore
index b7c2402..d2293f6 100644
--- a/.idea/.gitignore
+++ b/.idea/.gitignore
@@ -1,14 +1,7 @@
-/shelf
-/caches
-/libraries
-/assetWizardSettings.xml
-/deploymentTargetDropDown.xml
-/gradle.xml
-/misc.xml
-/modules.xml
-/navEditor.xml
-/runConfigurations.xml
-/workspace.xml
-/AndroidProjectSystem.xml
-
-**/*.iml
\ No newline at end of file
+*
+!/codeStyles/Project.xml
+!/codeStyles/codeStyleConfig.xml
+!/vcs.xml
+!/kotlinc.xml
+!/compiler.xml
+!/migrations.xml
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
deleted file mode 100644
index e40be60..0000000
--- a/.idea/deploymentTargetSelector.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/README.md b/README.md
index f953f9e..8a67e41 100644
--- a/README.md
+++ b/README.md
@@ -4,19 +4,23 @@ A fully free and open-source Local Profile Assistant implementation for Android
There are two variants of this project, OpenEUICC and EasyEUICC:
-| | OpenEUICC | EasyEUICC |
-|:------------------------------|:-----------------------------------------------:|:-----------------:|
-| Privileged | Must be installed as system app | No |
-| Internal eSIM | Supported | Unsupported |
-| External (Removable) eSIM | Supported | Supported |
-| USB Readers | Supported | Supported |
-| Requires allowlisting by eSIM | No | Yes -- except USB |
-| System Integration | Partial (carrier partner API unimplemented yet) | No |
+| | OpenEUICC | EasyEUICC |
+| :---------------------------- | :-----------------------------: | :-----------------: |
+| Privileged | Must be installed as system app | No |
+| Internal eSIM | Supported | Unsupported |
+| External eSIM [^1] | Supported | Supported |
+| USB Readers | Supported | Supported |
+| Requires allowlisting by eSIM | No | Yes -- except USB |
+| System Integration | Partial [^2] | No |
+| Minimum Android Version | Android 11 or higher | Android 9 or higher |
+
+[^1]: Also known as "Removable eSIM"
+[^2]: Carrier Partner API unimplemented yet
Some side notes:
1. When privileged, OpenEUICC supports any eUICC chip that implements the SGP.22 standard, internal or external. However, there is __no guarantee__ that external (removable) eSIMs actually follow the standard. Please __DO NOT__ submit bug reports for non-functioning removable eSIMs. They are __NOT__ officially supported unless they also support / are supported by EasyEUICC, the unprivileged variant.
2. Both variants support accessing eUICC chips through USB CCID readers, regardless of whether the chip contains the correct ARA-M hash to allow for unprivileged access. However, only `T=0` readers that use the standard [USB CCID protocol](https://en.wikipedia.org/wiki/CCID_(protocol)) are supported.
-3. Prebuilt release-mode EasyEUICC apks can be downloaded [here](https://gitea.angry.im/PeterCxy/OpenEUICC/releases). For OpenEUICC, no official release is currently provided and only debug mode APKs can be found in the CI page.
+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`.
__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.
@@ -74,10 +78,7 @@ FAQs
===
- Q: Do you provide prebuilt binaries for OpenEUICC?
-- A: Debug-mode APKs are available continuously as an artifact of the [Actions](https://gitea.angry.im/PeterCxy/OpenEUICC/actions) CI used by this project. However, these debug-mode APKs are **not** intended for inclusion inside system images, nor are they supported by the developer in any sense. If you are a custom ROM developer, either include the entire OpenEUICC repository in your AOSP source tree, or generate an APK using `gradle` and import that as a prebuilt system app. Note that you might want `privapp_whitelist_im.angry.openeuicc.xml` as well.
-
-- 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.
+- 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.
- 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 b0324dc..44c82c0 100644
--- a/app-common/src/main/AndroidManifest.xml
+++ b/app-common/src/main/AndroidManifest.xml
@@ -45,8 +45,9 @@
-
-
+
+
+
diff --git a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt
index b975313..78a8c3f 100644
--- a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt
@@ -20,9 +20,8 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
override suspend fun tryOpenEuiccChannel(
port: UiccPortInfoCompat,
- isdrAid: ByteArray,
- seId: EuiccChannel.SecureElementId,
- ): EuiccChannel? {
+ isdrAid: ByteArray
+ ): EuiccChannel? = try {
if (port.portIndex != 0) {
Log.w(
DefaultEuiccChannelManager.TAG,
@@ -36,61 +35,52 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
DefaultEuiccChannelManager.TAG,
"Trying OMAPI for physical slot ${port.card.physicalSlotIndex}"
)
- try {
- return EuiccChannelImpl(
- context.getString(R.string.omapi),
+ EuiccChannelImpl(
+ context.getString(R.string.channel_type_omapi),
+ port,
+ intrinsicChannelName = null,
+ OmapiApduInterface(
+ seService!!,
port,
- intrinsicChannelName = null,
- OmapiApduInterface(
- seService!!,
- port,
- context.preferenceRepository.verboseLoggingFlow
- ),
- isdrAid,
- seId,
- context.preferenceRepository.verboseLoggingFlow,
- context.preferenceRepository.ignoreTLSCertificateFlow,
- ).also {
- Log.i(DefaultEuiccChannelManager.TAG, "Is OMAPI channel, setting MSS to 60")
- it.lpa.setEs10xMss(60)
- }
- } catch (_: IllegalArgumentException) {
- // Failed
- Log.w(
- DefaultEuiccChannelManager.TAG,
- "OMAPI APDU interface unavailable for physical slot ${port.card.physicalSlotIndex} with ISD-R AID: ${isdrAid.encodeHex()}."
- )
- }
-
- return null
+ context.preferenceRepository.verboseLoggingFlow
+ ),
+ isdrAid,
+ context.preferenceRepository.verboseLoggingFlow,
+ context.preferenceRepository.ignoreTLSCertificateFlow,
+ context.preferenceRepository.es10xMssFlow,
+ )
+ } catch (_: IllegalArgumentException) {
+ // Failed
+ Log.w(
+ DefaultEuiccChannelManager.TAG,
+ "OMAPI APDU interface unavailable for physical slot ${port.card.physicalSlotIndex} with ISD-R AID: ${isdrAid.encodeHex()}."
+ )
+ null
}
override fun tryOpenUsbEuiccChannel(
ccidCtx: UsbCcidContext,
- isdrAid: ByteArray,
- seId: EuiccChannel.SecureElementId
- ): EuiccChannel? {
- try {
- return EuiccChannelImpl(
- context.getString(R.string.usb),
- FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)),
- intrinsicChannelName = ccidCtx.productName,
- UsbApduInterface(
- ccidCtx
- ),
- isdrAid,
- seId,
- context.preferenceRepository.verboseLoggingFlow,
- context.preferenceRepository.ignoreTLSCertificateFlow,
- )
- } catch (_: IllegalArgumentException) {
- // Failed
- Log.w(
- DefaultEuiccChannelManager.TAG,
- "USB APDU interface unavailable for ISD-R AID: ${isdrAid.encodeHex()}."
- )
- }
- return null
+ isdrAid: ByteArray
+ ): EuiccChannel? = try {
+ EuiccChannelImpl(
+ context.getString(R.string.channel_type_usb),
+ FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)),
+ intrinsicChannelName = ccidCtx.productName,
+ UsbApduInterface(
+ ccidCtx
+ ),
+ isdrAid,
+ context.preferenceRepository.verboseLoggingFlow,
+ context.preferenceRepository.ignoreTLSCertificateFlow,
+ context.preferenceRepository.es10xMssFlow,
+ )
+ } catch (_: IllegalArgumentException) {
+ // Failed
+ Log.w(
+ DefaultEuiccChannelManager.TAG,
+ "USB APDU interface unavailable for ISD-R AID: ${isdrAid.encodeHex()}."
+ )
+ null
}
override fun cleanup() {
diff --git a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt
index 2d0493b..6b336cd 100644
--- a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt
@@ -32,7 +32,7 @@ open class DefaultEuiccChannelManager(
private val channelCache = mutableListOf()
- private var usbChannels = mutableListOf()
+ private var usbChannel: EuiccChannel? = null
private val lock = Mutex()
@@ -51,20 +51,15 @@ open class DefaultEuiccChannelManager(
protected open val uiccCards: Collection
get() = (0.. EuiccChannel?): List {
+ private suspend inline fun tryOpenChannelFirstValidAid(openFn: (ByteArray) -> EuiccChannel?): EuiccChannel? {
val isdrAidList =
parseIsdrAidList(appContainer.preferenceRepository.isdrAidListFlow.first())
- var seId = 0
- return isdrAidList.mapNotNull {
- Log.i(
- TAG,
- "Opening channel, trying ISDR AID ${it.encodeHex()}, this will be seId $seId"
- )
+ return isdrAidList.firstNotNullOfOrNull {
+ Log.i(TAG, "Opening channel, trying ISDR AID ${it.encodeHex()}")
- openFn(it, EuiccChannel.SecureElementId.createFromInt(seId))?.let { channel ->
+ openFn(it)?.let { channel ->
if (channel.valid) {
- seId += 1
channel
} else {
channel.close()
@@ -74,18 +69,19 @@ open class DefaultEuiccChannelManager(
}
}
- private suspend fun tryOpenEuiccChannel(
- port: UiccPortInfoCompat,
- seId: EuiccChannel.SecureElementId = EuiccChannel.SecureElementId.DEFAULT
- ): EuiccChannel? {
+ private suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel? {
lock.withLock {
if (port.card.physicalSlotIndex == EuiccChannelManager.USB_CHANNEL_ID) {
- // We only compare seId because we assume we can only open 1 card from USB
- return usbChannels.find { it.seId == seId }
+ return if (usbChannel != null && usbChannel!!.valid) {
+ usbChannel
+ } else {
+ usbChannel = null
+ null
+ }
}
val existing =
- channelCache.find { it.slotId == port.card.physicalSlotIndex && it.portId == port.portIndex && it.seId == seId }
+ channelCache.find { it.slotId == port.card.physicalSlotIndex && it.portId == port.portIndex }
if (existing != null) {
if (existing.valid && port.logicalSlotIndex == existing.logicalSlotId) {
return existing
@@ -100,18 +96,12 @@ open class DefaultEuiccChannelManager(
return null
}
- val channels =
- tryOpenChannelWithKnownAids { isdrAid, seId ->
- euiccChannelFactory.tryOpenEuiccChannel(
- port,
- isdrAid,
- seId
- )
- }
+ val channel =
+ tryOpenChannelFirstValidAid { euiccChannelFactory.tryOpenEuiccChannel(port, it) }
- if (channels.isNotEmpty()) {
- channelCache.addAll(channels)
- return channels.find { it.seId == seId }
+ if (channel != null) {
+ channelCache.add(channel)
+ return channel
} else {
Log.i(
TAG,
@@ -122,13 +112,10 @@ open class DefaultEuiccChannelManager(
}
}
- protected suspend fun findEuiccChannelByLogicalSlot(
- logicalSlotId: Int,
- seId: EuiccChannel.SecureElementId = EuiccChannel.SecureElementId.DEFAULT
- ): EuiccChannel? =
+ protected suspend fun findEuiccChannelByLogicalSlot(logicalSlotId: Int): EuiccChannel? =
withContext(Dispatchers.IO) {
if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
- return@withContext usbChannels.find { it.seId == seId }
+ return@withContext usbChannel
}
for (card in uiccCards) {
@@ -144,7 +131,7 @@ open class DefaultEuiccChannelManager(
private suspend fun findAllEuiccChannelsByPhysicalSlot(physicalSlotId: Int): List? {
if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
- return usbChannels.ifEmpty { null }
+ return usbChannel?.let { listOf(it) }
}
for (card in uiccCards) {
@@ -155,18 +142,14 @@ open class DefaultEuiccChannelManager(
return null
}
- private suspend fun findEuiccChannelByPort(
- physicalSlotId: Int,
- portId: Int,
- seId: EuiccChannel.SecureElementId = EuiccChannel.SecureElementId.DEFAULT
- ): EuiccChannel? =
+ private suspend fun findEuiccChannelByPort(physicalSlotId: Int, portId: Int): EuiccChannel? =
withContext(Dispatchers.IO) {
if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
- return@withContext usbChannels.find { it.seId == seId }
+ return@withContext usbChannel
}
uiccCards.find { it.physicalSlotIndex == physicalSlotId }?.let { card ->
- card.ports.find { it.portIndex == portId }?.let { tryOpenEuiccChannel(it, seId) }
+ card.ports.find { it.portIndex == portId }?.let { tryOpenEuiccChannel(it) }
}
}
@@ -185,17 +168,15 @@ open class DefaultEuiccChannelManager(
return@withContext listOf(0)
}
- findAllEuiccChannelsByPhysicalSlot(physicalSlotId)?.map { it.portId }?.toSet()?.toList()
- ?: listOf()
+ findAllEuiccChannelsByPhysicalSlot(physicalSlotId)?.map { it.portId } ?: listOf()
}
override suspend fun withEuiccChannel(
physicalSlotId: Int,
portId: Int,
- seId: EuiccChannel.SecureElementId,
fn: suspend (EuiccChannel) -> R
): R {
- val channel = findEuiccChannelByPort(physicalSlotId, portId, seId)
+ val channel = findEuiccChannelByPort(physicalSlotId, portId)
?: throw EuiccChannelManager.EuiccChannelNotFoundException()
val wrapper = EuiccChannelWrapper(channel)
try {
@@ -209,10 +190,9 @@ open class DefaultEuiccChannelManager(
override suspend fun withEuiccChannel(
logicalSlotId: Int,
- seId: EuiccChannel.SecureElementId,
fn: suspend (EuiccChannel) -> R
): R {
- val channel = findEuiccChannelByLogicalSlot(logicalSlotId, seId)
+ val channel = findEuiccChannelByLogicalSlot(logicalSlotId)
?: throw EuiccChannelManager.EuiccChannelNotFoundException()
val wrapper = EuiccChannelWrapper(channel)
try {
@@ -226,8 +206,8 @@ open class DefaultEuiccChannelManager(
override suspend fun waitForReconnect(physicalSlotId: Int, portId: Int, timeoutMillis: Long) {
if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
- usbChannels.forEach { it.close() }
- usbChannels.clear()
+ usbChannel?.close()
+ usbChannel = null
} else {
// If there is already a valid channel, we close it proactively
// Sometimes the current channel can linger on for a bit even after it should have become invalid
@@ -243,7 +223,7 @@ open class DefaultEuiccChannelManager(
// tryOpenUsbEuiccChannel() will always try to reopen the channel, even if
// a USB channel already exists
tryOpenUsbEuiccChannel()
- usbChannels.getOrNull(0)!!
+ usbChannel!!
} else {
// tryOpenEuiccChannel() will automatically dispose of invalid channels
// and recreate when needed
@@ -284,20 +264,6 @@ open class DefaultEuiccChannelManager(
}
})
- override fun flowEuiccSecureElements(
- slotId: Int,
- portId: Int
- ): Flow = flow {
- // Emit the "default" channel first
- // TODO: This function below should really return a list, not just one SE
- findEuiccChannelByPort(slotId, portId, seId = EuiccChannel.SecureElementId.DEFAULT)?.let {
- emit(EuiccChannel.SecureElementId.DEFAULT)
-
- channelCache.filter { it.slotId == slotId && it.portId == portId && it.seId != EuiccChannel.SecureElementId.DEFAULT }
- .forEach { emit(it.seId) }
- }
- }
-
override suspend fun tryOpenUsbEuiccChannel(): Pair =
withContext(Dispatchers.IO) {
usbManager.deviceList.values.forEach { device ->
@@ -311,17 +277,15 @@ open class DefaultEuiccChannelManager(
"Found CCID interface on ${device.deviceId}:${device.vendorId}, and has permission; trying to open channel"
)
- val ccidCtx =
- UsbCcidContext.createFromUsbDevice(context, device, iface) ?: return@forEach
+ val ccidCtx = UsbCcidContext.createFromUsbDevice(context, device, iface) ?: return@forEach
try {
- val channels = tryOpenChannelWithKnownAids { isdrAid, seId ->
- euiccChannelFactory.tryOpenUsbEuiccChannel(ccidCtx, isdrAid, seId)
+ val channel = tryOpenChannelFirstValidAid {
+ euiccChannelFactory.tryOpenUsbEuiccChannel(ccidCtx, it)
}
- if (channels.isNotEmpty() && channels[0].valid) {
+ if (channel != null && channel.lpa.valid) {
ccidCtx.allowDisconnect = true
- usbChannels.clear()
- usbChannels.addAll(channels)
+ usbChannel = channel
return@withContext Pair(device, true)
}
} catch (e: Exception) {
@@ -345,8 +309,8 @@ open class DefaultEuiccChannelManager(
channel.close()
}
- usbChannels.forEach { it.close() }
- usbChannels.clear()
+ usbChannel?.close()
+ usbChannel = null
channelCache.clear()
euiccChannelFactory.cleanup()
}
diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt
index 19dd682..b20932f 100644
--- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt
@@ -1,7 +1,5 @@
package im.angry.openeuicc.core
-import android.os.Parcel
-import android.os.Parcelable
import im.angry.openeuicc.util.*
import net.typeblog.lpac_jni.ApduInterface
import net.typeblog.lpac_jni.LocalProfileAssistant
@@ -15,56 +13,6 @@ interface EuiccChannel {
val logicalSlotId: Int
val portId: Int
- /**
- * A semi-obscure wrapper over the integer ID of a secure element on a card.
- *
- * Because the ID is arbitrary, this is intended to discourage the use of the
- * integer value directly. Additionally, it prevents accidentally calling the
- * wrong function in EuiccChannelManager with a ton of integer parameters.
- */
- class SecureElementId private constructor(val id: Int) : Parcelable {
- companion object {
- val DEFAULT = SecureElementId(0)
-
- /**
- * Create a SecureElementId from an integer ID. You should not
- */
- fun createFromInt(id: Int): SecureElementId =
- SecureElementId(id)
-
- @Suppress("unused")
- @JvmField
- val CREATOR = object : Parcelable.Creator {
- override fun createFromParcel(parcel: Parcel): SecureElementId =
- createFromInt(parcel.readInt())
-
- override fun newArray(size: Int): Array = arrayOfNulls(size)
- }
- }
-
- override fun hashCode(): Int =
- id.hashCode()
-
- override fun equals(other: Any?): Boolean =
- if (other is SecureElementId) {
- this.id == other.id
- } else {
- super.equals(other)
- }
-
- override fun describeContents(): Int = id
-
- override fun writeToParcel(parcel: Parcel, flags: Int) {
- parcel.writeInt(id)
- }
- }
-
- /**
- * Some chips support multiple SEs on one chip. The seId here is intended
- * to distinguish channels opened from these different SEs.
- */
- val seId: SecureElementId
-
val lpa: LocalProfileAssistant
val valid: Boolean
diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelFactory.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelFactory.kt
index a8051af..ba587a6 100644
--- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelFactory.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelFactory.kt
@@ -6,12 +6,11 @@ import im.angry.openeuicc.util.*
// This class is here instead of inside DI because it contains a bit more logic than just
// "dumb" dependency injection.
interface EuiccChannelFactory {
- suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat, isdrAid: ByteArray, seId: EuiccChannel.SecureElementId): EuiccChannel?
+ suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat, isdrAid: ByteArray): EuiccChannel?
fun tryOpenUsbEuiccChannel(
ccidCtx: UsbCcidContext,
- isdrAid: ByteArray,
- seId: EuiccChannel.SecureElementId
+ isdrAid: ByteArray
): EuiccChannel?
/**
diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt
index 491f6d2..eaec522 100644
--- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt
@@ -1,8 +1,9 @@
package im.angry.openeuicc.core
import im.angry.openeuicc.util.UiccPortInfoCompat
-import im.angry.openeuicc.util.decodeHex
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.runBlocking
import net.typeblog.lpac_jni.ApduInterface
import net.typeblog.lpac_jni.LocalProfileAssistant
import net.typeblog.lpac_jni.impl.HttpInterfaceImpl
@@ -14,9 +15,9 @@ class EuiccChannelImpl(
override val intrinsicChannelName: String?,
override val apduInterface: ApduInterface,
override val isdrAid: ByteArray,
- override val seId: EuiccChannel.SecureElementId,
verboseLoggingFlow: Flow,
- ignoreTLSCertificateFlow: Flow
+ ignoreTLSCertificateFlow: Flow,
+ es10xMssFlow: Flow,
) : EuiccChannel {
override val slotId = port.card.physicalSlotIndex
override val logicalSlotId = port.logicalSlotIndex
@@ -26,8 +27,10 @@ class EuiccChannelImpl(
LocalProfileAssistantImpl(
isdrAid,
apduInterface,
- HttpInterfaceImpl(verboseLoggingFlow, ignoreTLSCertificateFlow)
- )
+ HttpInterfaceImpl(verboseLoggingFlow, ignoreTLSCertificateFlow),
+ ).also {
+ it.setEs10xMss(runBlocking { es10xMssFlow.first().toByte() })
+ }
override val atr: ByteArray?
get() = (apduInterface as? ApduInterfaceAtrProvider)?.atr
diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt
index eb8e87c..17f3130 100644
--- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt
@@ -37,14 +37,6 @@ interface EuiccChannelManager {
*/
fun flowAllOpenEuiccPorts(): Flow>
- /**
- * Iterate over all the Secure Elements available on one eUICC.
- *
- * This is going to almost always return only 1 result, except in the case where
- * a card has multiple SEs.
- */
- fun flowEuiccSecureElements(slotId: Int, portId: Int): Flow
-
/**
* Scan all possible USB devices for CCID readers that may contain eUICC cards.
* If found, try to open it for access, and add it to the internal EuiccChannel cache
@@ -89,16 +81,14 @@ interface EuiccChannelManager {
suspend fun withEuiccChannel(
physicalSlotId: Int,
portId: Int,
- seId: EuiccChannel.SecureElementId = EuiccChannel.SecureElementId.DEFAULT,
fn: suspend (EuiccChannel) -> R
): R
/**
- * Same as withEuiccChannel(Int, Int, SecureElementId, (EuiccChannel) -> R) but instead uses logical slot ID
+ * Same as withEuiccChannel(Int, Int, (EuiccChannel) -> R) but instead uses logical slot ID
*/
suspend fun withEuiccChannel(
logicalSlotId: Int,
- seId: EuiccChannel.SecureElementId = EuiccChannel.SecureElementId.DEFAULT,
fn: suspend (EuiccChannel) -> R
): R
diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt
index a36c1b6..361a943 100644
--- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt
@@ -26,8 +26,6 @@ class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel {
get() = channel.logicalSlotId
override val portId: Int
get() = channel.portId
- override val seId: EuiccChannel.SecureElementId
- get() = channel.seId
private val lpaDelegate = lazy {
LocalProfileAssistantWrapper(channel.lpa)
}
diff --git a/app-common/src/main/java/im/angry/openeuicc/di/DefaultCustomizableTextProvider.kt b/app-common/src/main/java/im/angry/openeuicc/di/DefaultCustomizableTextProvider.kt
index b493611..76227fd 100644
--- a/app-common/src/main/java/im/angry/openeuicc/di/DefaultCustomizableTextProvider.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/di/DefaultCustomizableTextProvider.kt
@@ -8,7 +8,7 @@ open class DefaultCustomizableTextProvider(private val context: Context) : Custo
get() = context.getString(R.string.no_euicc)
override val profileSwitchingTimeoutMessage: String
- get() = context.getString(R.string.enable_disable_timeout)
+ get() = context.getString(R.string.profile_switch_timeout)
override fun formatInternalChannelName(logicalSlotId: Int): String =
context.getString(R.string.channel_name_format, logicalSlotId)
diff --git a/app-common/src/main/java/im/angry/openeuicc/di/DefaultUiComponentFactory.kt b/app-common/src/main/java/im/angry/openeuicc/di/DefaultUiComponentFactory.kt
index d268da8..52a501a 100644
--- a/app-common/src/main/java/im/angry/openeuicc/di/DefaultUiComponentFactory.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/di/DefaultUiComponentFactory.kt
@@ -2,18 +2,13 @@ package im.angry.openeuicc.di
import androidx.fragment.app.Fragment
import androidx.preference.PreferenceFragmentCompat
-import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.ui.EuiccManagementFragment
import im.angry.openeuicc.ui.NoEuiccPlaceholderFragment
import im.angry.openeuicc.ui.SettingsFragment
open class DefaultUiComponentFactory : UiComponentFactory {
- override fun createEuiccManagementFragment(
- slotId: Int,
- portId: Int,
- seId: EuiccChannel.SecureElementId
- ): EuiccManagementFragment =
- EuiccManagementFragment.newInstance(slotId, portId, seId)
+ override fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment =
+ EuiccManagementFragment.newInstance(slotId, portId)
override fun createNoEuiccPlaceholderFragment(): Fragment = NoEuiccPlaceholderFragment()
diff --git a/app-common/src/main/java/im/angry/openeuicc/di/UiComponentFactory.kt b/app-common/src/main/java/im/angry/openeuicc/di/UiComponentFactory.kt
index 6a4d13f..2c3c72b 100644
--- a/app-common/src/main/java/im/angry/openeuicc/di/UiComponentFactory.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/di/UiComponentFactory.kt
@@ -2,16 +2,10 @@ package im.angry.openeuicc.di
import androidx.fragment.app.Fragment
import androidx.preference.PreferenceFragmentCompat
-import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.ui.EuiccManagementFragment
interface UiComponentFactory {
- fun createEuiccManagementFragment(
- slotId: Int,
- portId: Int,
- seId: EuiccChannel.SecureElementId
- ): EuiccManagementFragment
-
+ fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment
fun createNoEuiccPlaceholderFragment(): Fragment
fun createSettingsFragment(): Fragment
}
\ No newline at end of file
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt
index bd869ca..248afaf 100644
--- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt
@@ -36,14 +36,13 @@ private val RE_SAS = Regex(
class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
companion object {
- private val YES_NO = Pair(R.string.yes, R.string.no)
+ private val YES_NO = Pair(R.string.euicc_info_yes, R.string.euicc_info_no)
}
private lateinit var swipeRefresh: SwipeRefreshLayout
private lateinit var infoList: RecyclerView
private var logicalSlotId: Int = -1
- private var seId: EuiccChannel.SecureElementId = EuiccChannel.SecureElementId.DEFAULT
data class Item(
@StringRes
@@ -68,15 +67,9 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
}
logicalSlotId = intent.getIntExtra("logicalSlotId", 0)
- seId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- intent.getParcelableExtra("seId", EuiccChannel.SecureElementId::class.java)
- } else {
- @Suppress("DEPRECATION")
- intent.getParcelableExtra("seId")!!
- } ?: EuiccChannel.SecureElementId.DEFAULT
val channelTitle = if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
- getString(R.string.usb)
+ getString(R.string.channel_type_usb)
} else {
appContainer.customizableTextProvider.formatInternalChannelName(logicalSlotId)
}
@@ -106,7 +99,7 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
lifecycleScope.launch {
(infoList.adapter!! as EuiccInfoAdapter).euiccInfoItems =
- euiccChannelManager.withEuiccChannel(logicalSlotId, fn = ::buildEuiccInfoItems)
+ euiccChannelManager.withEuiccChannel(logicalSlotId, ::buildEuiccInfoItems)
swipeRefresh.isRefreshing = false
}
@@ -114,42 +107,29 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
private fun buildEuiccInfoItems(channel: EuiccChannel) = buildList {
add(Item(R.string.euicc_info_access_mode, channel.type))
- add(
- Item(
- R.string.euicc_info_removable,
- formatByBoolean(channel.port.card.isRemovable, YES_NO)
- )
- )
- add(
- Item(
- R.string.euicc_info_eid,
- channel.lpa.eID,
- copiedToastResId = R.string.toast_eid_copied
- )
- )
+ add(Item(R.string.euicc_info_removable, formatByBoolean(channel.port.card.isRemovable, YES_NO)))
+ add(Item(R.string.euicc_info_eid, channel.lpa.eID, copiedToastResId = R.string.toast_eid_copied))
add(Item(R.string.euicc_info_isdr_aid, channel.isdrAid.encodeHex()))
channel.tryParseEuiccVendorInfo()?.let { vendorInfo ->
vendorInfo.skuName?.let { add(Item(R.string.euicc_info_sku, it)) }
- vendorInfo.serialNumber?.let {
- add(
- Item(
- R.string.euicc_info_sn,
- it,
- copiedToastResId = R.string.toast_sn_copied
- )
- )
- }
+ vendorInfo.serialNumber?.let { add(Item(R.string.euicc_info_sn, it, copiedToastResId = R.string.toast_sn_copied)) }
vendorInfo.firmwareVersion?.let { add(Item(R.string.euicc_info_fw_ver, it)) }
vendorInfo.bootloaderVersion?.let { add(Item(R.string.euicc_info_bl_ver, it)) }
}
channel.lpa.euiccInfo2?.let { info ->
add(Item(R.string.euicc_info_sgp22_version, info.sgp22Version.toString()))
add(Item(R.string.euicc_info_firmware_version, info.euiccFirmwareVersion.toString()))
- add(Item(R.string.euicc_info_globalplatform_version, info.globalPlatformVersion.toString()))
+ add(Item(R.string.euicc_info_gp_version, info.globalPlatformVersion.toString()))
add(Item(R.string.euicc_info_pp_version, info.ppVersion.toString()))
info.sasAccreditationNumber.trim().takeIf(RE_SAS::matches)
?.let { add(Item(R.string.euicc_info_sas_accreditation_number, it.uppercase())) }
- add(Item(R.string.euicc_info_free_nvram, info.freeNvram.let(::formatFreeSpace)))
+
+ val nvramText = buildString {
+ append(formatFreeSpace(info.freeNvram))
+ append(' ')
+ append(getString(R.string.euicc_info_free_nvram_hint))
+ }
+ add(Item(R.string.euicc_info_free_nvram, nvramText))
}
channel.lpa.euiccInfo2?.euiccCiPKIdListForSigning.orEmpty().let { signers ->
// SGP.28 v1.0, eSIM CI Registration Criteria (Page 5 of 9, 2019-10-24)
@@ -157,14 +137,14 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
// FS.27 v2.0, Security Guidelines for UICC Profiles (Page 25 of 27, 2024-01-30)
// https://www.gsma.com/solutions-and-impact/technologies/security/wp-content/uploads/2024/01/FS.27-Security-Guidelines-for-UICC-Credentials-v2.0-FINAL-23-July.pdf#page=25
val resId = when {
- signers.isEmpty() -> R.string.unknown // the case is not mp, but it's is not common
+ signers.isEmpty() -> R.string.euicc_info_unknown // the case is not mp, but it's is not common
PKID_GSMA_LIVE_CI.any(signers::contains) -> R.string.euicc_info_ci_gsma_live
PKID_GSMA_TEST_CI.any(signers::contains) -> R.string.euicc_info_ci_gsma_test
else -> R.string.euicc_info_ci_unknown
}
add(Item(R.string.euicc_info_ci_type, getString(resId)))
}
- val atr = channel.atr?.encodeHex() ?: getString(R.string.information_unavailable)
+ val atr = channel.atr?.encodeHex() ?: getString(R.string.euicc_info_unavailable)
add(Item(R.string.euicc_info_atr, atr, copiedToastResId = R.string.toast_atr_copied))
}
@@ -197,7 +177,7 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
fun bind(item: Item) {
copiedToastResId = item.copiedToastResId
title.setText(item.titleResId)
- content.text = item.content ?: getString(R.string.unknown)
+ content.text = item.content ?: getString(R.string.euicc_info_unknown)
}
}
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt
index 4be3ebc..016e96f 100644
--- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt
@@ -31,7 +31,6 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.material.floatingactionbutton.FloatingActionButton
import net.typeblog.lpac_jni.LocalProfileInfo
import im.angry.openeuicc.common.R
-import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.service.EuiccChannelManagerService
import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
import im.angry.openeuicc.ui.wizard.DownloadWizardActivity
@@ -50,12 +49,8 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
companion object {
const val TAG = "EuiccManagementFragment"
- fun newInstance(
- slotId: Int,
- portId: Int,
- seId: EuiccChannel.SecureElementId
- ): EuiccManagementFragment =
- newInstanceEuicc(EuiccManagementFragment::class.java, slotId, portId, seId)
+ fun newInstance(slotId: Int, portId: Int): EuiccManagementFragment =
+ newInstanceEuicc(EuiccManagementFragment::class.java, slotId, portId)
}
private lateinit var swipeRefresh: SwipeRefreshLayout
@@ -153,7 +148,6 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
R.id.show_notifications -> {
Intent(requireContext(), NotificationsActivity::class.java).apply {
putExtra("logicalSlotId", logicalSlotId)
- putExtra("seId", seId)
startActivity(this)
}
true
@@ -162,14 +156,13 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
R.id.euicc_info -> {
Intent(requireContext(), EuiccInfoActivity::class.java).apply {
putExtra("logicalSlotId", logicalSlotId)
- putExtra("seId", seId)
startActivity(this)
}
true
}
R.id.euicc_memory_reset -> {
- EuiccMemoryResetFragment.newInstance(slotId, portId, seId, eid)
+ EuiccMemoryResetFragment.newInstance(slotId, portId, eid)
.show(childFragmentManager, EuiccMemoryResetFragment.TAG)
true
}
@@ -260,7 +253,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
if (!isUsb) {
withContext(Dispatchers.Main) {
AlertDialog.Builder(requireContext()).apply {
- setMessage(R.string.switch_did_not_refresh)
+ setMessage(R.string.profile_switch_did_not_refresh)
setPositiveButton(android.R.string.ok) { dialog, _ ->
dialog.dismiss()
requireActivity().finish()
@@ -301,10 +294,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
}
}
- protected open fun populatePopupWithProfileActions(
- popup: PopupMenu,
- profile: LocalProfileInfo
- ) {
+ protected open fun populatePopupWithProfileActions(popup: PopupMenu, profile: LocalProfileInfo) {
popup.inflate(R.menu.profile_options)
if (profile.isEnabled) {
popup.menu.findItem(R.id.enable).isVisible = false
@@ -331,7 +321,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
}
}
- inner class FooterViewHolder : ViewHolder(FrameLayout(requireContext())) {
+ inner class FooterViewHolder: ViewHolder(FrameLayout(requireContext())) {
init {
itemView.layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
@@ -357,6 +347,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
private val profileClassLabel: TextView = root.requireViewById(R.id.profile_class_label)
private val profileClass: TextView = root.requireViewById(R.id.profile_class)
private val profileMenu: ImageButton = root.requireViewById(R.id.profile_menu)
+ private val profileSeqNumber: TextView = root.requireViewById(R.id.profile_sequence_number)
init {
iccid.setOnClickListener {
@@ -376,7 +367,9 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
true
}
- profileMenu.setOnClickListener { showOptionsMenu() }
+ profileMenu.setOnClickListener {
+ showOptionsMenu()
+ }
}
private lateinit var profile: LocalProfileInfo
@@ -387,9 +380,9 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
state.setText(
if (profile.isEnabled) {
- R.string.enabled
+ R.string.profile_state_enabled
} else {
- R.string.disabled
+ R.string.profile_state_disabled
}
)
provider.text = profile.providerName
@@ -406,6 +399,13 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
iccid.transformationMethod = PasswordTransformationMethod.getInstance()
}
+ fun setProfileSequenceNumber(index: Int) {
+ profileSeqNumber.text = root.context.getString(
+ R.string.profile_sequence_number_format,
+ index,
+ )
+ }
+
private fun showOptionsMenu() {
// Prevent users from doing multiple things at once
if (invalid || swipeRefresh.isRefreshing) return
@@ -423,36 +423,20 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
enableOrDisableProfile(profile.iccid, true)
true
}
-
R.id.disable -> {
enableOrDisableProfile(profile.iccid, false)
true
}
-
R.id.rename -> {
- ProfileRenameFragment.newInstance(
- slotId,
- portId,
- seId,
- profile.iccid,
- profile.displayName
- )
+ ProfileRenameFragment.newInstance(slotId, portId, profile.iccid, profile.displayName)
.show(childFragmentManager, ProfileRenameFragment.TAG)
true
}
-
R.id.delete -> {
- ProfileDeleteFragment.newInstance(
- slotId,
- portId,
- seId,
- profile.iccid,
- profile.displayName
- )
+ ProfileDeleteFragment.newInstance(slotId, portId, profile.iccid, profile.displayName)
.show(childFragmentManager, ProfileDeleteFragment.TAG)
true
}
-
else -> false
}
}
@@ -464,11 +448,9 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
when (ViewHolder.Type.fromInt(viewType)) {
ViewHolder.Type.PROFILE -> {
- val view = LayoutInflater.from(parent.context)
- .inflate(R.layout.euicc_profile, parent, false)
+ val view = LayoutInflater.from(parent.context).inflate(R.layout.euicc_profile, parent, false)
ProfileViewHolder(view)
}
-
ViewHolder.Type.FOOTER -> {
FooterViewHolder()
}
@@ -479,11 +461,9 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
position < profiles.size -> {
ViewHolder.Type.PROFILE.value
}
-
position >= profiles.size && position < profiles.size + footerViews.size -> {
ViewHolder.Type.FOOTER.value
}
-
else -> -1
}
@@ -491,8 +471,8 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
when (holder) {
is ProfileViewHolder -> {
holder.setProfile(profiles[position])
+ holder.setProfileSequenceNumber(position + 1)
}
-
is FooterViewHolder -> {
holder.attach(footerViews[position - profiles.size])
}
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccMemoryResetFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccMemoryResetFragment.kt
index 79a8f0a..086a849 100644
--- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccMemoryResetFragment.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccMemoryResetFragment.kt
@@ -11,7 +11,6 @@ import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import im.angry.openeuicc.common.R
-import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
import im.angry.openeuicc.util.EuiccChannelFragmentMarker
import im.angry.openeuicc.util.EuiccProfilesChangedListener
@@ -30,8 +29,8 @@ class EuiccMemoryResetFragment : DialogFragment(), EuiccChannelFragmentMarker {
private const val FIELD_EID = "eid"
- fun newInstance(slotId: Int, portId: Int, seId: EuiccChannel.SecureElementId, eid: String) =
- newInstanceEuicc(EuiccMemoryResetFragment::class.java, slotId, portId, seId) {
+ fun newInstance(slotId: Int, portId: Int, eid: String) =
+ newInstanceEuicc(EuiccMemoryResetFragment::class.java, slotId, portId) {
putString(FIELD_EID, eid)
}
}
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt
index 93f51dd..b42f4cf 100644
--- a/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt
@@ -23,7 +23,6 @@ import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import im.angry.openeuicc.common.R
-import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers
@@ -113,12 +112,10 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
startActivity(Intent(this, SettingsActivity::class.java))
true
}
-
R.id.reload -> {
refresh()
true
}
-
else -> super.onOptionsItemSelected(item)
}
@@ -157,33 +154,27 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
euiccChannelManager.flowInternalEuiccPorts().onEach { (slotId, portId) ->
Log.d(TAG, "slot $slotId port $portId")
- euiccChannelManager.flowEuiccSecureElements(slotId, portId).onEach { seId ->
- euiccChannelManager.withEuiccChannel(slotId, portId, seId) { channel ->
- if (preferenceRepository.verboseLoggingFlow.first()) {
- Log.d(TAG, channel.lpa.eID)
- }
- // Request the system to refresh the list of profiles every time we start
- // Note that this is currently supposed to be no-op when unprivileged,
- // but it could change in the future
- euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId)
-
- val channelName =
- appContainer.customizableTextProvider.formatInternalChannelName(channel.logicalSlotId)
- newPages.add(Page(channel.logicalSlotId, channelName) {
- appContainer.uiComponentFactory.createEuiccManagementFragment(
- slotId,
- portId,
- seId
- )
- })
+ euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
+ if (preferenceRepository.verboseLoggingFlow.first()) {
+ Log.d(TAG, channel.lpa.eID)
}
- }.collect()
+ // Request the system to refresh the list of profiles every time we start
+ // Note that this is currently supposed to be no-op when unprivileged,
+ // but it could change in the future
+ euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId)
+
+ val channelName =
+ appContainer.customizableTextProvider.formatInternalChannelName(channel.logicalSlotId)
+ newPages.add(Page(channel.logicalSlotId, channelName) {
+ appContainer.uiComponentFactory.createEuiccManagementFragment(slotId, portId)
+ })
+ }
}.collect()
// If USB readers exist, add them at the very last
// We use a wrapper fragment to handle logic specific to USB readers
usbDevice?.let {
- val productName = it.productName ?: getString(R.string.usb)
+ val productName = it.productName ?: getString(R.string.channel_type_usb)
newPages.add(Page(EuiccChannelManager.USB_CHANNEL_ID, productName) {
UsbCcidReaderFragment()
})
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt
index 594fdd1..07d5f13 100644
--- a/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt
@@ -1,7 +1,6 @@
package im.angry.openeuicc.ui
import android.annotation.SuppressLint
-import android.os.Build
import android.os.Bundle
import android.text.Html
import android.view.ContextMenu
@@ -21,7 +20,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import im.angry.openeuicc.common.R
-import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers
@@ -29,13 +27,12 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.typeblog.lpac_jni.LocalProfileNotification
-class NotificationsActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
+class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
private lateinit var swipeRefresh: SwipeRefreshLayout
private lateinit var notificationList: RecyclerView
private val notificationAdapter = NotificationAdapter()
private var logicalSlotId = -1
- private var seId = EuiccChannel.SecureElementId.DEFAULT
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
@@ -54,27 +51,16 @@ class NotificationsActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker
override fun onInit() {
notificationList.layoutManager =
LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
- notificationList.addItemDecoration(
- DividerItemDecoration(
- this,
- LinearLayoutManager.VERTICAL
- )
- )
+ notificationList.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
notificationList.adapter = notificationAdapter
registerForContextMenu(notificationList)
logicalSlotId = intent.getIntExtra("logicalSlotId", 0)
- seId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- intent.getParcelableExtra("seId", EuiccChannel.SecureElementId::class.java)
- } else {
- @Suppress("DEPRECATION")
- intent.getParcelableExtra("seId")!!
- } ?: EuiccChannel.SecureElementId.DEFAULT
// This is slightly different from the MainActivity logic
// due to the length (we don't want to display the full USB product name)
val channelTitle = if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
- getString(R.string.usb)
+ getString(R.string.channel_type_usb)
} else {
appContainer.customizableTextProvider.formatInternalChannelName(logicalSlotId)
}
@@ -100,7 +86,6 @@ class NotificationsActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker
finish()
true
}
-
R.id.help -> {
AlertDialog.Builder(this, R.style.AlertDialogTheme).apply {
setMessage(R.string.profile_notifications_help)
@@ -111,7 +96,6 @@ class NotificationsActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker
}
true
}
-
else -> super.onOptionsItemSelected(item)
}
@@ -130,20 +114,20 @@ class NotificationsActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker
}
private fun refresh() {
- launchTask {
- notificationAdapter.notifications =
- euiccChannelManager.withEuiccChannel(logicalSlotId) { channel ->
- val nameMap = buildMap {
- for (profile in channel.lpa.profiles) {
- put(profile.iccid, profile.displayName)
- }
- }
+ launchTask {
+ notificationAdapter.notifications =
+ euiccChannelManager.withEuiccChannel(logicalSlotId) { channel ->
+ val nameMap = buildMap {
+ for (profile in channel.lpa.profiles) {
+ put(profile.iccid, profile.displayName)
+ }
+ }
- channel.lpa.notifications.map {
- LocalProfileNotificationWrapper(it, nameMap[it.iccid] ?: "???")
- }
- }
- }
+ channel.lpa.notifications.map {
+ LocalProfileNotificationWrapper(it, nameMap[it.iccid] ?: "???")
+ }
+ }
+ }
}
data class LocalProfileNotificationWrapper(
@@ -152,7 +136,7 @@ class NotificationsActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker
)
@SuppressLint("ClickableViewAccessibility")
- inner class NotificationViewHolder(private val root: View) :
+ inner class NotificationViewHolder(private val root: View):
RecyclerView.ViewHolder(root), View.OnCreateContextMenuListener, OnMenuItemClickListener {
private val address: TextView = root.requireViewById(R.id.notification_address)
private val sequenceNumber: TextView =
@@ -186,8 +170,7 @@ class NotificationsActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker
LocalProfileNotification.Operation.Delete -> R.string.profile_notification_operation_delete
LocalProfileNotification.Operation.Enable -> R.string.profile_notification_operation_enable
LocalProfileNotification.Operation.Disable -> R.string.profile_notification_operation_disable
- }
- )
+ })
fun updateNotification(value: LocalProfileNotificationWrapper) {
notification = value
@@ -198,13 +181,10 @@ class NotificationsActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker
value.inner.seqNumber
)
profileName.text = Html.fromHtml(
- root.context.getString(
- R.string.profile_notification_name_format,
+ root.context.getString(R.string.profile_notification_name_format,
operationToLocalizedText(value.inner.profileManagementOperation),
- value.profileName, value.inner.iccid
- ),
- Html.FROM_HTML_MODE_COMPACT
- )
+ value.profileName, value.inner.iccid),
+ Html.FROM_HTML_MODE_COMPACT)
}
override fun onCreateContextMenu(
@@ -233,7 +213,6 @@ class NotificationsActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker
}
true
}
-
R.id.notification_delete -> {
launchTask {
withContext(Dispatchers.IO) {
@@ -246,12 +225,11 @@ class NotificationsActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker
}
true
}
-
else -> false
}
}
- inner class NotificationAdapter : RecyclerView.Adapter() {
+ inner class NotificationAdapter: RecyclerView.Adapter() {
var notifications: List = listOf()
@SuppressLint("NotifyDataSetChanged")
set(value) {
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt
index d5cf496..38d1bc6 100644
--- a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt
@@ -9,7 +9,6 @@ import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import im.angry.openeuicc.common.R
-import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
import im.angry.openeuicc.util.*
import kotlinx.coroutines.flow.onStart
@@ -21,8 +20,8 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
private const val FIELD_ICCID = "iccid"
private const val FIELD_NAME = "name"
- fun newInstance(slotId: Int, portId: Int, seId: EuiccChannel.SecureElementId, iccid: String, name: String) =
- newInstanceEuicc(ProfileDeleteFragment::class.java, slotId, portId, seId) {
+ fun newInstance(slotId: Int, portId: Int, iccid: String, name: String) =
+ newInstanceEuicc(ProfileDeleteFragment::class.java, slotId, portId) {
putString(FIELD_ICCID, iccid)
putString(FIELD_NAME, name)
}
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt
index e5f8251..281e625 100644
--- a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt
@@ -12,7 +12,6 @@ import androidx.appcompat.widget.Toolbar
import androidx.lifecycle.lifecycleScope
import com.google.android.material.textfield.TextInputLayout
import im.angry.openeuicc.common.R
-import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
import im.angry.openeuicc.util.*
import kotlinx.coroutines.launch
@@ -25,8 +24,8 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
const val TAG = "ProfileRenameFragment"
- fun newInstance(slotId: Int, portId: Int, seId: EuiccChannel.SecureElementId, iccid: String, currentName: String) =
- newInstanceEuicc(ProfileRenameFragment::class.java, slotId, portId, seId) {
+ fun newInstance(slotId: Int, portId: Int, iccid: String, currentName: String) =
+ newInstanceEuicc(ProfileRenameFragment::class.java, slotId, portId) {
putString(FIELD_ICCID, iccid)
putString(FIELD_CURRENT_NAME, currentName)
}
@@ -66,7 +65,7 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
super.onViewCreated(view, savedInstanceState)
profileRenameNewName.editText!!.setText(currentName)
toolbar.apply {
- setTitle(R.string.rename)
+ setTitle(R.string.profile_rename)
setNavigationOnClickListener {
if (!renaming) dismiss()
}
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt
index 6554142..7a717ac 100644
--- a/app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt
@@ -8,6 +8,7 @@ import android.provider.Settings
import android.widget.Toast
import androidx.lifecycle.lifecycleScope
import androidx.preference.CheckBoxPreference
+import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
@@ -16,7 +17,6 @@ import im.angry.openeuicc.util.*
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
open class SettingsFragment: PreferenceFragmentCompat() {
private lateinit var developerPref: PreferenceCategory
@@ -34,7 +34,7 @@ open class SettingsFragment: PreferenceFragmentCompat() {
// Show / hide developer preference based on whether it is enabled
lifecycleScope.launch {
preferenceRepository.developerOptionsEnabledFlow
- .onEach { developerPref.isVisible = it }
+ .onEach(developerPref::setVisible)
.collect()
}
@@ -84,6 +84,9 @@ open class SettingsFragment: PreferenceFragmentCompat() {
requirePreference("pref_developer_euicc_memory_reset")
.bindBooleanFlow(preferenceRepository.euiccMemoryResetFlow)
+ requirePreference("pref_developer_es10x_mss")
+ .bindIntFlow(preferenceRepository.es10xMssFlow, 63)
+
requirePreference("pref_developer_isdr_aid_list").apply {
intent = Intent(requireContext(), IsdrAidListActivity::class.java)
}
@@ -100,51 +103,53 @@ open class SettingsFragment: PreferenceFragmentCompat() {
@Suppress("UNUSED_PARAMETER")
private fun onAppVersionClicked(pref: Preference): Boolean {
if (developerPref.isVisible) return false
+
val now = System.currentTimeMillis()
- if (now - lastClickTimestamp >= 1000) {
- numClicks = 1
- } else {
- numClicks++
- }
+ numClicks = if (now - lastClickTimestamp >= 1000) 1 else numClicks + 1
lastClickTimestamp = now
- if (numClicks == 7) {
- lifecycleScope.launch {
- preferenceRepository.developerOptionsEnabledFlow.updatePreference(true)
-
- lastToast?.cancel()
- Toast.makeText(
- requireContext(),
- R.string.developer_options_enabled,
- Toast.LENGTH_SHORT
- ).show()
- }
- } else if (numClicks > 1) {
- lastToast?.cancel()
- lastToast = Toast.makeText(
- requireContext(),
- getString(R.string.developer_options_steps, 7 - numClicks),
- Toast.LENGTH_SHORT
- )
- lastToast!!.show()
+ lifecycleScope.launch {
+ preferenceRepository.developerOptionsEnabledFlow.updatePreference(numClicks >= 7)
}
+ val toastText = when {
+ numClicks == 7 -> getString(R.string.developer_options_enabled)
+ numClicks > 1 -> getString(R.string.developer_options_steps, 7 - numClicks)
+ else -> return true
+ }
+
+ lastToast?.cancel()
+ lastToast = Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT)
+ lastToast!!.show()
return true
}
protected fun CheckBoxPreference.bindBooleanFlow(flow: PreferenceFlowWrapper) {
lifecycleScope.launch {
- flow.collect { isChecked = it }
+ flow.collect(::setChecked)
}
setOnPreferenceChangeListener { _, newValue ->
- runBlocking {
+ lifecycleScope.launch {
flow.updatePreference(newValue as Boolean)
}
true
}
}
+ private fun ListPreference.bindIntFlow(flow: PreferenceFlowWrapper, defaultValue: Int) {
+ lifecycleScope.launch {
+ flow.collect { value = it.toString() }
+ }
+
+ setOnPreferenceChangeListener { _, newValue ->
+ lifecycleScope.launch {
+ flow.updatePreference((newValue as String).toIntOrNull() ?: defaultValue)
+ }
+ true
+ }
+ }
+
protected fun mergePreferenceOverlay(overlayKey: String, targetKey: String) {
val overlayCat = requirePreference(overlayKey)
val targetCat = requirePreference(targetKey)
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/UsbCcidReaderFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/UsbCcidReaderFragment.kt
index a14e47d..7a52ca0 100644
--- a/app-common/src/main/java/im/angry/openeuicc/ui/UsbCcidReaderFragment.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/UsbCcidReaderFragment.kt
@@ -20,7 +20,6 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope
import im.angry.openeuicc.common.R
-import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers
@@ -157,9 +156,7 @@ class UsbCcidReaderFragment : Fragment(), OpenEuiccContextMarker {
R.id.child_container,
appContainer.uiComponentFactory.createEuiccManagementFragment(
slotId = EuiccChannelManager.USB_CHANNEL_ID,
- portId = 0,
- // TODO: What if a USB card has multiple SEs?
- seId = EuiccChannel.SecureElementId.DEFAULT
+ portId = 0
)
)
}
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt
index 9e312d4..6574645 100644
--- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt
@@ -123,8 +123,8 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
// If we get an LPA string from deep-link intents, extract from there.
// Note that `onRestoreInstanceState` could override this with user input,
// but that _is_ the desired behavior.
- val uri = intent.data
- if (uri?.scheme == "lpa") {
+ val uri = intent.data ?: return
+ if (uri.scheme.contentEquals("lpa", ignoreCase = true)) {
val parsed = LPAString.parse(uri.schemeSpecificPart)
state.smdp = parsed.address
state.matchingId = parsed.matchingId
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt
index e282196..3841868 100644
--- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt
@@ -8,6 +8,7 @@ import android.view.ViewGroup
import android.widget.TextView
import im.angry.openeuicc.common.R
import im.angry.openeuicc.util.*
+import org.json.JSONObject
import java.util.Date
class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardStepFragment() {
@@ -86,9 +87,10 @@ class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardS
ret.appendLine()
val str = resp.data.decodeToString(throwOnInvalidSequence = false)
+
ret.appendLine(
if (str.startsWith('{')) {
- str.prettyPrintJson()
+ JSONObject(str).toString(2)
} else {
str
}
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt
index 342a687..0048190 100644
--- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt
@@ -7,6 +7,7 @@ import android.view.ViewGroup
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
+import androidx.annotation.StringRes
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
@@ -42,19 +43,17 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep
}
private data class ProgressItem(
- val titleRes: Int,
- var state: ProgressState
+ @StringRes val titleRes: Int,
+ var state: ProgressState = ProgressState.NotStarted,
+ var errorMessage: SimplifiedErrorMessages? = null,
)
private val progressItems = arrayOf(
- ProgressItem(R.string.download_wizard_progress_step_preparing, ProgressState.NotStarted),
- ProgressItem(R.string.download_wizard_progress_step_connecting, ProgressState.NotStarted),
- ProgressItem(
- R.string.download_wizard_progress_step_authenticating,
- ProgressState.NotStarted
- ),
- ProgressItem(R.string.download_wizard_progress_step_downloading, ProgressState.NotStarted),
- ProgressItem(R.string.download_wizard_progress_step_finalizing, ProgressState.NotStarted)
+ ProgressItem(R.string.download_wizard_progress_step_preparing),
+ ProgressItem(R.string.download_wizard_progress_step_connecting),
+ ProgressItem(R.string.download_wizard_progress_step_authenticating),
+ ProgressItem(R.string.download_wizard_progress_step_downloading),
+ ProgressItem(R.string.download_wizard_progress_step_finalizing)
)
private val adapter = ProgressItemAdapter()
@@ -122,8 +121,13 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep
// Change the state of the last InProgress item to success (or error)
progressItems.forEachIndexed { index, progressItem ->
if (progressItem.state == ProgressState.InProgress) {
- progressItem.state =
- if (state.downloadError == null) ProgressState.Done else ProgressState.Error
+ if (state.downloadError == null) {
+ progressItem.state = ProgressState.Done
+ } else {
+ progressItem.state = ProgressState.Error
+ progressItem.errorMessage =
+ SimplifiedErrorMessages.fromDownloadError(state.downloadError!!)
+ }
}
adapter.notifyItemChanged(index)
@@ -133,9 +137,8 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep
refreshButtons()
}
- is EuiccChannelManagerService.ForegroundTaskState.InProgress -> {
+ is EuiccChannelManagerService.ForegroundTaskState.InProgress ->
updateProgress(it.progress)
- }
else -> {}
}
@@ -197,9 +200,15 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep
private val progressBar =
root.requireViewById(R.id.download_progress_icon_progress)
private val icon = root.requireViewById(R.id.download_progress_icon)
+ private val errorTitle =
+ root.requireViewById(R.id.download_progress_item_error_title)
+ private val errorSuggestion =
+ root.requireViewById(R.id.download_progress_item_error_suggestion)
fun bind(item: ProgressItem) {
title.text = getString(item.titleRes)
+ errorTitle.visibility = View.GONE
+ errorSuggestion.visibility = View.GONE
when (item.state) {
ProgressState.NotStarted -> {
@@ -222,6 +231,15 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep
progressBar.visibility = View.GONE
icon.setImageResource(R.drawable.ic_error_outline)
icon.visibility = View.VISIBLE
+
+ item.errorMessage?.titleResId?.let {
+ errorTitle.visibility = View.VISIBLE
+ errorTitle.text = getString(it)
+ }
+ item.errorMessage?.suggestResId?.let {
+ errorSuggestion.visibility = View.VISIBLE
+ errorSuggestion.text = getString(it)
+ }
}
}
}
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt
index 28bc9f0..8097058 100644
--- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt
@@ -19,7 +19,6 @@ import im.angry.openeuicc.util.*
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
-import net.typeblog.lpac_jni.LocalProfileInfo
class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardStepFragment() {
companion object {
@@ -187,12 +186,12 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
}
title.text = if (item.logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
- item.intrinsicChannelName ?: root.context.getString(R.string.usb)
+ item.intrinsicChannelName ?: root.context.getString(R.string.channel_type_usb)
} else {
appContainer.customizableTextProvider.formatInternalChannelName(item.logicalSlotId)
}
eID.text = item.eID
- activeProfile.text = item.enabledProfileName ?: root.context.getString(R.string.unknown)
+ activeProfile.text = item.enabledProfileName ?: root.context.getString(R.string.profile_no_enabled_profile)
freeSpace.text = formatFreeSpace(item.freeSpace)
checkBox.isChecked = adapter.currentSelectedIdx == idx
}
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/SimplifiedErrorMessages.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/SimplifiedErrorMessages.kt
new file mode 100644
index 0000000..8ce5740
--- /dev/null
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/SimplifiedErrorMessages.kt
@@ -0,0 +1,154 @@
+package im.angry.openeuicc.ui.wizard
+
+import androidx.annotation.StringRes
+import im.angry.openeuicc.common.R
+import net.typeblog.lpac_jni.LocalProfileAssistant
+import org.json.JSONObject
+import java.net.NoRouteToHostException
+import java.net.PortUnreachableException
+import java.net.SocketException
+import java.net.SocketTimeoutException
+import java.net.UnknownHostException
+import javax.net.ssl.SSLException
+
+enum class SimplifiedErrorMessages(
+ @StringRes val titleResId: Int,
+ @StringRes val suggestResId: Int?
+) {
+ ICCIDAlreadyInUse(
+ R.string.download_wizard_error_iccid_already,
+ R.string.download_wizard_error_suggest_profile_installed
+ ),
+ InsufficientMemory(
+ R.string.download_wizard_error_insufficient_memory,
+ R.string.download_wizard_error_suggest_insufficient_memory
+ ),
+ UnsupportedProfile(
+ R.string.download_wizard_error_unsupported_profile,
+ null
+ ),
+ CardInternalError(
+ R.string.download_wizard_error_card_internal_error,
+ null
+ ),
+ EIDNotSupported(
+ R.string.download_wizard_error_eid_not_supported,
+ R.string.download_wizard_error_suggest_contact_carrier
+ ),
+ EIDMismatch(
+ R.string.download_wizard_error_eid_mismatch,
+ R.string.download_wizard_error_suggest_contact_reissue
+ ),
+ UnreleasedProfile(
+ R.string.download_wizard_error_profile_unreleased,
+ R.string.download_wizard_error_suggest_contact_reissue
+ ),
+ MatchingIDRefused(
+ R.string.download_wizard_error_matching_id_refused,
+ R.string.download_wizard_error_suggest_contact_carrier
+ ),
+ ProfileRetriesExceeded(
+ R.string.download_wizard_error_profile_retries_exceeded,
+ R.string.download_wizard_error_suggest_contact_carrier
+ ),
+ ConfirmationCodeMissing(
+ R.string.download_wizard_error_confirmation_code_missing,
+ R.string.download_wizard_error_suggest_contact_carrier
+ ),
+ ConfirmationCodeRefused(
+ R.string.download_wizard_error_confirmation_code_refused,
+ R.string.download_wizard_error_suggest_contact_carrier
+ ),
+ ConfirmationCodeRetriesExceeded(
+ R.string.download_wizard_error_confirmation_code_retries_exceeded,
+ R.string.download_wizard_error_suggest_contact_carrier
+ ),
+ ProfileExpired(
+ R.string.download_wizard_error_profile_expired,
+ R.string.download_wizard_error_suggest_contact_carrier
+ ),
+ UnknownHost(
+ R.string.download_wizard_error_unknown_hostname,
+ null
+ ),
+ NetworkUnreachable(
+ R.string.download_wizard_error_network_unreachable,
+ R.string.download_wizard_error_suggest_network_unreachable
+ ),
+ TLSError(
+ R.string.download_wizard_error_tls_certificate,
+ null
+ );
+
+ companion object {
+ private val httpErrors = buildMap {
+ // Stage: AuthenticateClient
+ put("8.1" to "4.8", InsufficientMemory)
+ put("8.1.1" to "2.1", EIDNotSupported)
+ put("8.1.1" to "3.8", EIDMismatch)
+ put("8.2" to "1.2", UnreleasedProfile)
+ put("8.2.6" to "3.8", MatchingIDRefused)
+ put("8.8.5" to "6.4", ProfileRetriesExceeded)
+
+ // Stage: GetBoundProfilePackage
+ put("8.2.7" to "2.2", ConfirmationCodeMissing)
+ put("8.2.7" to "3.8", ConfirmationCodeRefused)
+ put("8.2.7" to "6.4", ConfirmationCodeRetriesExceeded)
+
+ // Stage: AuthenticateClient, GetBoundProfilePackage
+ put("8.8.5" to "4.10", ProfileExpired)
+ }
+
+ fun fromDownloadError(exc: LocalProfileAssistant.ProfileDownloadException) = when {
+ exc.lpaErrorReason != "ES10B_ERROR_REASON_UNDEFINED" -> fromLPAErrorReason(exc.lpaErrorReason)
+ exc.lastHttpResponse?.rcode == 200 -> fromHTTPResponse(exc.lastHttpResponse!!)
+ exc.lastHttpException != null -> fromHTTPException(exc.lastHttpException!!)
+ exc.lastApduResponse != null -> fromAPDUResponse(exc.lastApduResponse!!)
+ else -> null
+ }
+
+ private fun fromLPAErrorReason(reason: String) = when (reason) {
+ "ES10B_ERROR_REASON_UNSUPPORTED_CRT_VALUES" -> UnsupportedProfile
+ "ES10B_ERROR_REASON_UNSUPPORTED_REMOTE_OPERATION_TYPE" -> UnsupportedProfile
+ "ES10B_ERROR_REASON_UNSUPPORTED_PROFILE_CLASS" -> UnsupportedProfile
+ "ES10B_ERROR_REASON_INSTALL_FAILED_DUE_TO_ICCID_ALREADY_EXISTS_ON_EUICC" -> ICCIDAlreadyInUse
+ "ES10B_ERROR_REASON_INSTALL_FAILED_DUE_TO_INSUFFICIENT_MEMORY_FOR_PROFILE" -> InsufficientMemory
+ "ES10B_ERROR_REASON_INSTALL_FAILED_DUE_TO_INTERRUPTION" -> CardInternalError
+ "ES10B_ERROR_REASON_INSTALL_FAILED_DUE_TO_PE_PROCESSING_ERROR" -> CardInternalError
+ else -> null
+ }
+
+ private fun fromHTTPResponse(httpResponse: net.typeblog.lpac_jni.HttpInterface.HttpResponse): SimplifiedErrorMessages? {
+ if (httpResponse.data.first().toInt() != '{'.code) return null
+ val response = JSONObject(httpResponse.data.decodeToString())
+ val statusCodeData = response.optJSONObject("header")
+ ?.optJSONObject("functionExecutionStatus")
+ ?.optJSONObject("statusCodeData")
+ ?: return null
+ val subjectCode = statusCodeData.optString("subjectCode")
+ val reasonCode = statusCodeData.optString("reasonCode")
+ return httpErrors[subjectCode to reasonCode]
+ }
+
+ private fun fromHTTPException(exc: Exception) = when (exc) {
+ is SSLException -> TLSError
+ is UnknownHostException -> UnknownHost
+ is NoRouteToHostException -> NetworkUnreachable
+ is PortUnreachableException -> NetworkUnreachable
+ is SocketTimeoutException -> NetworkUnreachable
+ is SocketException -> exc.message
+ ?.contains("Connection reset", ignoreCase = true)
+ ?.let { if (it) NetworkUnreachable else null }
+
+ else -> null
+ }
+
+ private fun fromAPDUResponse(resp: ByteArray): SimplifiedErrorMessages? {
+ val isSuccess = resp.size >= 2 &&
+ resp[resp.size - 2] == 0x90.toByte() &&
+ resp[resp.size - 1] == 0x00.toByte()
+ if (isSuccess) return null
+ return CardInternalError
+ }
+ }
+}
diff --git a/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt
index d2d8c4b..b44bef8 100644
--- a/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt
@@ -1,6 +1,5 @@
package im.angry.openeuicc.util
-import android.os.Build
import android.os.Bundle
import androidx.fragment.app.Fragment
import im.angry.openeuicc.core.EuiccChannel
@@ -10,7 +9,6 @@ import im.angry.openeuicc.ui.BaseEuiccAccessActivity
private const val FIELD_SLOT_ID = "slotId"
private const val FIELD_PORT_ID = "portId"
-private const val FIELD_SE_ID = "seId"
interface EuiccChannelFragmentMarker : OpenEuiccContextMarker
@@ -19,19 +17,12 @@ private typealias BundleSetter = Bundle.() -> Unit
// We must use extension functions because there is no way to add bounds to the type of "self"
// in the definition of an interface, so the only way is to limit where the extension functions
// can be applied.
-fun newInstanceEuicc(
- clazz: Class,
- slotId: Int,
- portId: Int,
- seId: EuiccChannel.SecureElementId,
- addArguments: BundleSetter = {}
-): T
+fun newInstanceEuicc(clazz: Class, slotId: Int, portId: Int, addArguments: BundleSetter = {}): T
where T : Fragment, T : EuiccChannelFragmentMarker =
clazz.getDeclaredConstructor().newInstance().apply {
arguments = Bundle()
arguments!!.putInt(FIELD_SLOT_ID, slotId)
arguments!!.putInt(FIELD_PORT_ID, portId)
- arguments!!.putParcelable(FIELD_SE_ID, seId)
arguments!!.addArguments()
}
@@ -44,18 +35,6 @@ val T.slotId: Int
val T.portId: Int
where T : Fragment, T : EuiccChannelFragmentMarker
get() = requireArguments().getInt(FIELD_PORT_ID)
-val T.seId: EuiccChannel.SecureElementId
- where T : Fragment, T : EuiccChannelFragmentMarker
- get() =
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- requireArguments().getParcelable(
- FIELD_SE_ID,
- EuiccChannel.SecureElementId::class.java
- )!!
- } else {
- @Suppress("DEPRECATION")
- requireArguments().getParcelable(FIELD_SE_ID)!!
- }
val T.isUsb: Boolean
where T : Fragment, T : EuiccChannelFragmentMarker
get() = slotId == EuiccChannelManager.USB_CHANNEL_ID
@@ -75,12 +54,7 @@ val T.euiccChannelManagerService: EuiccChannelManagerService
suspend fun T.withEuiccChannel(fn: suspend (EuiccChannel) -> R): R
where T : Fragment, T : EuiccChannelFragmentMarker {
ensureEuiccChannelManager()
- return euiccChannelManager.withEuiccChannel(
- slotId,
- portId,
- seId,
- fn
- )
+ return euiccChannelManager.withEuiccChannel(slotId, portId, fn)
}
suspend fun T.ensureEuiccChannelManager() where T : Fragment, T : OpenEuiccContextMarker =
diff --git a/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt
index 5f4aec4..2fef3db 100644
--- a/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt
@@ -5,6 +5,7 @@ import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import androidx.fragment.app.Fragment
@@ -38,6 +39,7 @@ internal object PreferenceKeys {
val IGNORE_TLS_CERTIFICATE = booleanPreferencesKey("ignore_tls_certificate")
val EUICC_MEMORY_RESET = booleanPreferencesKey("euicc_memory_reset")
val ISDR_AID_LIST = stringPreferencesKey("isdr_aid_list")
+ val ES10X_MSS = intPreferencesKey("es10x_mss")
}
const val EUICC_DEFAULT_ISDR_AID = "A0000005591010FFFFFFFF8900000100"
@@ -50,9 +52,12 @@ internal object PreferenceConstants {
# eUICC standard
$EUICC_DEFAULT_ISDR_AID
- # eSTK.me
+ # ESTKme AUX (deprecated, use SE0 instead)
A06573746B6D65FFFFFFFF4953442D52
+ # ESTKme SE0
+ A06573746B6D65FFFF4953442D522030
+
# eSIM.me
A0000005591010000000008900000300
@@ -86,6 +91,7 @@ open class PreferenceRepository(private val context: Context) {
PreferenceConstants.DEFAULT_AID_LIST,
{ Base64.getEncoder().encodeToString(it.encodeToByteArray()) },
{ Base64.getDecoder().decode(it).decodeToString() })
+ val es10xMssFlow = bindFlow(PreferenceKeys.ES10X_MSS, 63)
protected fun bindFlow(
key: Preferences.Key,
diff --git a/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt
index 079853e..57d150b 100644
--- a/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt
@@ -41,73 +41,3 @@ fun parseIsdrAidList(s: String): List =
.filter(String::isNotEmpty)
.mapNotNull { runCatching(it::decodeHex).getOrNull() }
.ifEmpty { listOf(EUICC_DEFAULT_ISDR_AID.decodeHex()) }
-
-fun String.prettyPrintJson(): String {
- val ret = StringBuilder()
- var inQuotes = false
- var escaped = false
- val indentSymbolStack = ArrayDeque()
-
- val addNewLine = {
- ret.append('\n')
- repeat(indentSymbolStack.size) {
- ret.append('\t')
- }
- }
-
- var lastChar = ' '
-
- for (c in this) {
- when {
- !inQuotes && (c == '{' || c == '[') -> {
- ret.append(c)
- indentSymbolStack.addLast(c)
- addNewLine()
- }
-
- !inQuotes && (c == '}' || c == ']') -> {
- indentSymbolStack.removeLast()
- if (lastChar != ',') {
- addNewLine()
- }
- ret.append(c)
- }
-
- !inQuotes && c == ',' -> {
- ret.append(c)
- addNewLine()
- }
-
- !inQuotes && c == ':' -> {
- ret.append(c)
- ret.append(' ')
- }
-
- inQuotes && c == '\\' -> {
- ret.append(c)
- escaped = true
- continue
- }
-
- !escaped && c == '"' -> {
- ret.append(c)
- inQuotes = !inQuotes
- }
-
- !inQuotes && c == ' ' -> {
- // Do nothing -- we ignore spaces outside of quotes by default
- // This is to ensure predictable formatting
- }
-
- else -> ret.append(c)
- }
-
- if (escaped) {
- escaped = false
- }
-
- lastChar = c
- }
-
- return ret.toString()
-}
\ No newline at end of file
diff --git a/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt
index a73d7fe..c7c859d 100644
--- a/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt
@@ -102,8 +102,8 @@ fun T.setupLogSaving(
AlertDialog.Builder(context).apply {
setMessage(R.string.logs_saved_message)
- setNegativeButton(R.string.no) { _, _ -> }
- setPositiveButton(R.string.yes) { _, _ ->
+ setNegativeButton(android.R.string.cancel) { _, _ -> }
+ setPositiveButton(android.R.string.ok) { _, _ ->
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
clipData = ClipData.newUri(context.contentResolver, lastFileName, uri)
diff --git a/app-common/src/main/res/layout/download_progress_item.xml b/app-common/src/main/res/layout/download_progress_item.xml
index f1d0852..c59673b 100644
--- a/app-common/src/main/res/layout/download_progress_item.xml
+++ b/app-common/src/main/res/layout/download_progress_item.xml
@@ -1,30 +1,32 @@
+ android:layout_height="wrap_content">
+ app:layout_constraintBottom_toBottomOf="@id/download_progress_icon_container"
+ app:layout_constraintEnd_toStartOf="@id/download_progress_icon_container"
+ app:layout_constraintHorizontal_bias="0.0"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="@id/download_progress_icon_container"
+ app:layout_constraintVertical_bias="0.5" />
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_bias="0.0">
+
+
+
+
\ No newline at end of file
diff --git a/app-common/src/main/res/layout/euicc_profile.xml b/app-common/src/main/res/layout/euicc_profile.xml
index 58d55ab..021c53b 100644
--- a/app-common/src/main/res/layout/euicc_profile.xml
+++ b/app-common/src/main/res/layout/euicc_profile.xml
@@ -54,7 +54,7 @@
+
+
diff --git a/app-common/src/main/res/layout/fragment_euicc.xml b/app-common/src/main/res/layout/fragment_euicc.xml
index 4ae7523..c5fde7b 100644
--- a/app-common/src/main/res/layout/fragment_euicc.xml
+++ b/app-common/src/main/res/layout/fragment_euicc.xml
@@ -27,6 +27,7 @@
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
+ android:contentDescription="@string/profile_download"
android:src="@drawable/ic_add"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
diff --git a/app-common/src/main/res/menu/activity_isdr_aid_list.xml b/app-common/src/main/res/menu/activity_isdr_aid_list.xml
index 32f178a..99492d6 100644
--- a/app-common/src/main/res/menu/activity_isdr_aid_list.xml
+++ b/app-common/src/main/res/menu/activity_isdr_aid_list.xml
@@ -9,7 +9,7 @@
\ No newline at end of file
diff --git a/app-common/src/main/res/menu/activity_main.xml b/app-common/src/main/res/menu/activity_main.xml
index 0e00292..c15663f 100644
--- a/app-common/src/main/res/menu/activity_main.xml
+++ b/app-common/src/main/res/menu/activity_main.xml
@@ -3,7 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
diff --git a/app-common/src/main/res/menu/activity_notifications.xml b/app-common/src/main/res/menu/activity_notifications.xml
index 87f96a6..b80e06e 100644
--- a/app-common/src/main/res/menu/activity_notifications.xml
+++ b/app-common/src/main/res/menu/activity_notifications.xml
@@ -4,6 +4,6 @@
\ No newline at end of file
diff --git a/app-common/src/main/res/menu/fragment_profile_rename.xml b/app-common/src/main/res/menu/fragment_profile_rename.xml
index bde850f..f55c56c 100644
--- a/app-common/src/main/res/menu/fragment_profile_rename.xml
+++ b/app-common/src/main/res/menu/fragment_profile_rename.xml
@@ -4,6 +4,6 @@
\ No newline at end of file
diff --git a/app-common/src/main/res/menu/profile_options.xml b/app-common/src/main/res/menu/profile_options.xml
index 6add53d..60722d6 100644
--- a/app-common/src/main/res/menu/profile_options.xml
+++ b/app-common/src/main/res/menu/profile_options.xml
@@ -2,18 +2,18 @@
\ No newline at end of file
diff --git a/app-common/src/main/res/values-ja/strings.xml b/app-common/src/main/res/values-ja/strings.xml
index d51e2c7..b171972 100644
--- a/app-common/src/main/res/values-ja/strings.xml
+++ b/app-common/src/main/res/values-ja/strings.xml
@@ -2,26 +2,28 @@
このアプリでアクセスできるリムーバブル eUICC カードがデバイス上で検出されていません。互換性のあるカード挿入または USB リーダーを接続してください。
この eSIM にはプロファイルがありません。
- 不明
- 情報がありません
- ヘルプ
- スロットを再読み込み
+ ヘルプ
+ スロットを再読み込み
+ 不明
論理スロット %d
- 有効済み
- 無効済み
- プロバイダー:
+
+ 有効化済み
+ 無効化済み
+ プロバイダー:
クラス:
テスト中
プロビジョニング
稼働中
- 有効化
- 無効化
- 削除
- 名前を変更
- eSIM チップがプロファイルの切り替えの待機中にタイムアウトしました。これはデバイスのモデムファームウェアのバグの可能性があります。機内モードに切り替えるかアプリを再起動、デバイスを再起動してください。
- 操作は成功しましたが、デバイスのモデムが更新を拒否しました。新しいプロファイルを使用するには機内モードに切り替えるか、再起動する必要があります。
+ 有効化
+ 無効化
+ 削除
+ 名前を変更
+ eSIM チップがプロファイルの切り替えの待機中にタイムアウトしました。これはデバイスのモデムファームウェアのバグの可能性があります。機内モードに切り替えるかアプリを再起動、デバイスを再起動してください。
+ 操作は成功しましたが、デバイスのモデムが更新を拒否しました。新しいプロファイルを使用するには機内モードに切り替えるか、再起動する必要があります。
新しい eSIM プロファイルに切り替えることができません。
確認文字列が一致しません
+ 確認文字列が一致しません
+ このチップは消去されました
ICCID をクリップボードにコピーしました
シリアル番号をクリップボードにコピーしました
EID をクリップボードにコピーしました
@@ -38,14 +40,16 @@
eSIM プロファイルの削除に失敗しました
eSIM プロファイルを切り替え中
eSIM プロファイルの切り替えに失敗しました
+ eSIM チップを消去中
+ eSIM チップの消去に失敗しました
新しい eSIM
サーバー (RSP / SM-DP+)
アクティベーションコード
確認コード (オプション)
確認コード (必須)
IMEI (オプション)
- 残り容量が少ない
- 残り容量が少ないため、ダウンロードに失敗する可能性があります。
+ 残りの容量が少量です
+ 残り容量が少ないため、このプロファイルのダウンロードに失敗する可能性があります。
クリップボードに LPA コードがありません
解析できません
QR コードまたはクリップボードの内容を LPA コードとして解析できませんでした。
@@ -83,6 +87,27 @@
最終の APDU 例外:
保存
「%s」での診断
+ この eSIM プロファイルはすでに eSIM チップに存在します。
+ eSIM チップにはプロファイルをダウンロードするのに必要なメモリが残っていません。
+ この eSIM プロファイルは、eSIM チップではサポートされていません。
+ eSIM チップでエラーが発生しました。
+ 使用しているデバイスまたは eSIM チップの EID は、通信事業者によってサポートされていません。
+ この eSIM プロファイルは、別のデバイスにダウンロードされています。
+ この eSIM プロファイルは取り消されました。
+ アクティベーションコードが無効です。
+ eSIM プロファイルのダウンロード試行回数の上限を超えました。
+ このプロファイルをダウンロードするには確認コードが必要です。
+ 入力した確認コードは無効です。
+ この eSIM プロファイルは有効期限が切れています。
+ 確認コードのダウンロード試行回数の上限を超えました。
+ 不明な SM-DP+ アドレス
+ ネットワークにアクセスできません
+ TLS 証明書エラー、この eSIM プロファイルはサポートされていません
+ ダウンロード済みの eSIM プロファイルを再インストールしようとしています
+ 未使用の eSIM プロファイルをいくつか削除して、再度お試しください
+ サポートについては、通信事業者にお問い合わせください。
+ この eSIM プロファイルを再発行するには、通信事業者にお問い合わせください。
+ 別のネットワークに接続後 (例: Wi-Fi とデータを切り替え) を行った後に再度お試しください。
ログは共有したパスに保存されました。別のアプリで共有しますか?
新しいニックネーム
ニックネームを UTF-8 にエンコードできませんでした
@@ -96,8 +121,8 @@
eSIM プロファイルはダウンロードや削除、有効化や無効化されたときに通信事業者に通知を送信できます。送信されるこれらの通知のキューはここにリストされます。\n\n設定では、各タイプの通知を自動的に送信するかどうかを指定できます。通知が送信された場合でもキューのスペースが不足していない限り、記録から自動的に削除されることはありません。\n\nここでは保留中の各通知を手動で送信または削除できます。
ダウンロードしました
削除しました
- 有効化しました
- 無効化しました
+ 有効化済み
+ 無効化済み
処理
削除
eUICC 情報
@@ -110,21 +135,33 @@
製品ファームウェアバージョン
SGP.22 バージョン
eUICC OS バージョン
- グローバルプラットフォームのバージョン
+ グローバルプラットフォームのバージョン
SAS 認定番号
保護されたプロファイルのバージョン
NVRAM の空き容量 (eSIM プロファイルストレージ)
+ (参照用)
証明書発行者 (CI)
GSMA ライブ CI
GSMA テスト CI
不明な eSIM CI
- はい
- いいえ
+ eUICC を消去
+ eUICC を消去
+ このチップ上のすべてのプロファイルを削除することを確認してください。この操作は元に戻せないことを理解してください。\n\nEID: %1$s\n\n%2$s
+ 確認のために「%s」を入力してください
+ EID が %s で終わるチップを消去することを確認し、この操作は元に戻せないことを理解してください
+ 消去
+
+ はい
+ いいえ
+ 不明
+ 情報がありません
保存
%s のログ
開発者になるまであと %d ステップです。
あなたは開発者になりました!
- カスタム ISD-R AID リストが保存されました
+ ISD-R AID リスト
+ カスタム ISD-R AID リストを保存しました。
+ リセット
設定
通知
eSIM のプロファイル操作により、通信事業者に通知が送信されます。必要に応じてこの動作を微調整できます。
@@ -136,7 +173,7 @@
プロファイルを切り替え中の通知を送信します\nこのタイプの通知は信頼できないことに注意してください。
高度な設定
プロファイルの無効化と削除を許可
- デフォルトでは、このアプリでデバイスに挿入された取り外し可能な eSIM の有効なプロファイルを無効化することを防いでいます。なぜなのかというと時々アクセスができなくなるからです。\nこのチェックボックスを ON にすることで、この保護機能を解除します。
+ デフォルトでは、このアプリでデバイスに挿入されたリムーバブル eSIM の有効なプロファイルを無効化することを防いでいます。なぜなのかというと時々アクセスができなくなるからです。\nこのチェックボックスを ON にすることで、この保護機能を解除します。
詳細ログ
詳細ログを有効化します。これには個人的な情報が含まれている可能性があります。この機能を ON にした後は、信頼できるユーザーとのみログを共有してください。
言語
@@ -144,29 +181,22 @@
ログ
アプリの最新デバッグログを表示します
開発者オプション
- プロファイルを切り替えた後にモデムに更新コマンドを送信するかどうか。クラッシュが発生する場合は、これを無効にしてみてください。
+ モデムに更新コマンドを送信
+ プロファイルを切り替えた後にモデムに更新コマンドを送信するかどうかを設定します。クラッシュが発生する場合は、この機能を無効化してください。
フィルタリングされていないプロファイル一覧を表示
非運用のプロファイルも含めます
SM-DP+ TLS 証明書を無視する
RSP サーバーで使用される TLS 証明書を受け入れます
- 一部のブランドの取り外し可能な eUICC では、独自の非標準 ISD-R AID が使用されている場合があり、サードパーティ アプリからアクセスできなくなります。アプリはこのリストに追加された非標準の AID の使用を試みる可能性がありますが、動作することは保証されません。
+ eUICC の消去を許可
+ これは危険な操作であり、デフォルトでは非表示になっています。代わりとしてすべてのプロファイルを手動で削除することもできます。
+ グローバル ES10x MSS
+
+ - 高速
+ - 互換モード
+
+ ISD-R AID リストをカスタマイズ
+ 一部ブランドのリムーバブル eUICC は独自の非標準な ISD-R AID を使用しているため、サードパーティー製アプリからアクセスできない場合があります。このリストに追加された非標準な AID の使用を試みますが、動作の保証はできません。
情報
アプリバージョン
ソースコード
- 確認文字列が一致しません
- このチップは消去されました
- eSIM チップを消去しています
- eSIM チップの消去は失敗しました
- eSIM を消去する
- eSIM を消去する
- このチップ内のすべてのプロファイルを削除することをご確認してください。この操作は元に戻せないことをご理解してください。\n\nEID: %1$s\n\n%2$s
- 確認のため、ここに「%s」を入力してください
- EID が %s で終わるチップを消去することに同意します。これは元に戻せないことを理解しています。
- 消去する
- eUICC の消去を可能にする
- この操作は、デフォルトでは非表示になっている危険な操作です。代わりに、すべての構成ファイルを手動で削除することもできます。
- モデムに更新コマンドを送信
- ISD-R AID リストのカスタマイズ
- リセット
- ISD-R AID リスト
diff --git a/app-common/src/main/res/values-zh-rCN/strings.xml b/app-common/src/main/res/values-zh-rCN/strings.xml
index 32ced90..7303d2d 100644
--- a/app-common/src/main/res/values-zh-rCN/strings.xml
+++ b/app-common/src/main/res/values-zh-rCN/strings.xml
@@ -2,20 +2,21 @@
在此设备上未检测到此应用程序可访问的可插拔 eUICC 卡。请插入兼容卡或 USB 读卡器。
此 eSIM 上还没有配置文件
- 未知
- 帮助
- 重新加载卡槽
+ 未知
+ 帮助
+ 重新加载卡槽
+ 未知
逻辑卡槽 %d
- 已启用
- 已禁用
- 提供商:
+ 已启用
+ 已禁用
+ 提供商:
类型:
- 启用
- 禁用
- 删除
- 重命名
- 等待 eSIM 芯片切换配置文件时超时。这可能是您手机基带固件中的一个错误。请尝试切换飞行模式、重新启动应用程序或重新启动手机
- 操作成功, 但是您手机的基带拒绝刷新。您可能需要切换飞行模式或重新启动,以便使用新的配置文件。
+ 启用
+ 禁用
+ 删除
+ 重命名
+ 等待 eSIM 芯片切换配置文件时超时。这可能是您手机基带固件中的一个错误。请尝试切换飞行模式、重新启动应用程序或重新启动手机
+ 操作成功, 但是您手机的基带拒绝刷新。您可能需要切换飞行模式或重新启动,以便使用新的配置文件。
无法切换到新的 eSIM 配置文件。
输入的确认文本不匹配
已复制 ICCID 到剪贴板
@@ -46,6 +47,7 @@
IMEI (可选)
剩余空间不足
当前芯片的剩余空间不足,可能导致配置下载失败。\n是否继续下载?
+ 请连接到其他网络(例如在 Wi-Fi 和数据之间切换)后重试。
日志已保存到指定路径。需要通过其他 App 分享吗?
新昵称
无法将昵称编码为 UTF-8
@@ -83,6 +85,11 @@
日志
查看应用程序的最新调试日志
某些品牌的可移除 eUICC 可能会使用自己的非标准 ISD-R AID,导致第三方应用无法访问。此 App 可以尝试使用此列表中添加的非标准 AID,但不能保证它们一定有效。
+ 全局 ES10x MSS
+
+ - 最佳效率
+ - 最佳兼容性
+
信息
App 版本
源码
@@ -132,16 +139,17 @@
可插拔
SGP.22 版本
eUICC OS 版本
- GlobalPlatform 版本
+ GlobalPlatform 版本
SAS 认证号码
Protected Profile 版本
NVRAM 剩余空间 (eSIM 存储容量)
+ (仅供参考)
证书签发者 (CI)
GSMA 生产环境 CI
GSMA 测试 CI
未知 eSIM CI
- 是
- 否
+ 是
+ 否
还有 %d 步成为开发者
你现在是开发者了!
语言
@@ -152,7 +160,7 @@
在配置文件列表中包括非生产环境的配置文件
无视 SM-DP+ 的 TLS 证书
允许 RSP 服务器使用任意证书
- 无信息
+ 无信息
输入的确认文本不匹配
此芯片已被擦除
正在擦除 eSIM 芯片
@@ -167,6 +175,26 @@
此操作是默认隐藏的危险操作。作为替代方案,您可以手动删除所有配置文件。
向基带发送刷新命令
自定义 ISD-R AID 列表
- 重置
+ 重置
ISD-R AID 列表
+ 此 eSIM 配置文件已存在于您的 eSIM 芯片上。
+ 您的 eSIM 芯片没有足够的空间来下载配置文件。
+ 您的 eSIM 芯片不支持此 eSIM 配置文件。
+ eSIM 芯片错误。
+ 您的设备或 eSIM 芯片的 EID 不受您的运营商支持。
+ 此 eSIM 配置文件已被下载到另一台设备上。
+ 此 eSIM 配置文件已被撤销。
+ 激活码无效。
+ 已超出 eSIM 配置文件的最大下载尝试次数。
+ 下载此配置文件需要确认码。
+ 您输入的确认码无效。
+ 此 eSIM 配置文件已过期。
+ 已超出确认码的最大下载尝试次数。
+ 未知的 SM-DP+ 地址
+ 网络不可达
+ TLS 证书错误,不支持此 eSIM 配置文件
+ 您正在尝试重新安装已下载的 eSIM 配置文件
+ 请删除一些未使用的 eSIM 配置文件,然后重试
+ 请联系您的运营商寻求帮助。
+ 请联系您的运营商重新签发此 eSIM 配置文件。
\ No newline at end of file
diff --git a/app-common/src/main/res/values-zh-rTW/strings.xml b/app-common/src/main/res/values-zh-rTW/strings.xml
index 5136bf7..ef6c842 100644
--- a/app-common/src/main/res/values-zh-rTW/strings.xml
+++ b/app-common/src/main/res/values-zh-rTW/strings.xml
@@ -2,20 +2,21 @@
在此裝置上未檢測到此應用程式可訪問的可插拔 eUICC 卡。請插入相容卡或 USB 晶片讀卡機。
此 eSIM 上還沒有設定檔
- 未知
- 幫助
- 重新載入卡槽
+ 未知
+ 幫助
+ 重新載入卡槽
+ 未知
虛擬卡槽 %d
- 已啟用
- 已停用
- 電信業者:
+ 已啟用
+ 已停用
+ 電信業者:
類型:
- 啟用
- 停用
- 刪除
- 重新命名
- 等待 eSIM 切換設定檔時逾時。這可能是您手機基頻處理器韌體中的一個錯誤。請嘗試切換飛航模式、重新啟動應用程式或重新啟動手機
- 操作成功, 但是您手機的基頻處理器沒有重新整理。您可能需要切換飛航模式或重新啟動,以便使用新的設定檔。
+ 啟用
+ 停用
+ 刪除
+ 重新命名
+ 等待 eSIM 切換設定檔時逾時。這可能是您手機基頻處理器韌體中的一個錯誤。請嘗試切換飛航模式、重新啟動應用程式或重新啟動手機
+ 操作成功, 但是您手機的基頻處理器沒有重新整理。您可能需要切換飛航模式或重新啟動,以便使用新的設定檔。
無法切換到新的 eSIM 設定檔。
輸入的確認文字不匹配
已複製 ICCID 到剪貼簿
@@ -46,6 +47,7 @@
IMEI (可選)
剩餘空間不足
目前晶片的剩餘空間不足,可能導致配置下載失敗。\n是否繼續下載?
+ 請連接到其他網路(例如在 Wi-Fi 和資料之間切換)後重試。
日誌已儲存到指定路徑。需要透過其他 App 分享嗎?
新名稱
無法將名稱編碼為 UTF-8
@@ -83,6 +85,7 @@
允許 停用/刪除 已啟用的設定檔
預設情況下,此應用程式會阻止您停用可插拔 eSIM 中已啟用的設定檔。\n因為這樣做 有時 會導致無法存取。\n勾選此框以 移除 此保護措施。
某些品牌的可移除 eUICC 可能會使用自己的非標準 ISD-R AID,導致第三方應用程式無法存取。此 App 可以嘗試使用此清單中新增的非標準 AID,但不能保證它們一定有效。
+ 全局 ES10x MSS
資訊
App 版本
原始碼
@@ -132,16 +135,17 @@
可插拔
SGP.22 版本
eUICC OS 版本
- GlobalPlatform 版本
+ GlobalPlatform 版本
SAS 認證號碼
Protected Profile 版本
NVRAM 剩餘空間 (eSIM 儲存容量)
+ (僅供參考)
證書簽發者 (CI)
GSMA 生產環境 CI
GSMA 測試 CI
未知 eSIM CI
- 是
- 否
+ 是
+ 否
還有 %d 步成為開發者
您現在是開發者了!
語言
@@ -152,7 +156,7 @@
在設定檔列表中包括非生產環境的設定檔
忽略 SM-DP+ 的 TLS 證書
允許 RSP 伺服器使用任意證書
- 無資訊
+ 無資訊
輸入的確認文字不匹配
此晶片已被擦除
正在擦除 eSIM 晶片
@@ -167,6 +171,26 @@
此操作是預設隱藏的危險操作。作為替代方案,您可以手動刪除所有設定檔。
向基帶發送刷新命令
自訂 ISD-R AID 列表
- 重置
+ 重置
ISD-R AID 列表
+ 此 eSIM 設定檔已存在於您的 eSIM 晶片上。
+ 您的 eSIM 晶片沒有足夠的空間來下載設定檔。
+ 您的 eSIM 晶片不支援此 eSIM 設定檔。
+ eSIM 晶片錯誤。
+ 您的裝置或 eSIM 晶片的 EID 不受您的電信業者支援。
+ 此 eSIM 設定檔已被下載到另一台裝置上。
+ 此 eSIM 設定檔已被撤銷。
+ 啟用碼無效。
+ 已超出 eSIM 設定檔的最大下載嘗試次數。
+ 下載此設定檔需要確認碼。
+ 您輸入的確認碼無效。
+ 此 eSIM 設定檔已過期。
+ 已超出確認碼的最大下載嘗試次數。
+ 未知的 SM-DP+ 位址
+ 網路不可達
+ TLS 憑證錯誤,不支援此 eSIM 設定檔
+ 您正在嘗試重新安裝已下載的 eSIM 設定文件
+ 請刪除一些未使用的 eSIM 設定文件,然後重試
+ 請聯絡您的電信業者尋求協助。
+ 請聯絡您的電信業者重新簽發此 eSIM 設定檔。
\ No newline at end of file
diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml
index 05ca15a..39a762f 100644
--- a/app-common/src/main/res/values/strings.xml
+++ b/app-common/src/main/res/values/strings.xml
@@ -2,31 +2,34 @@
No removable eUICC card accessible by this app is detected on this device. Insert a compatible card or a USB reader.
No profiles (yet) on this eSIM.
- Unknown
- Information Unavailable
- Help
- Reload Slots
+
+ Help
+
+ Reload Slots
+ Unknown
Logical Slot %d
- USB
- OpenMobile API (OMAPI)
+ USB
+ OpenMobile API (OMAPI)
- Enabled
- Disabled
- Provider:
+
+ Enabled
+ Disabled
+ Provider:
Class:
Testing
Provisioning
Operational
- ICCID:
+ ICCID:
+ #%d
- Enable
- Disable
- Delete
- Rename
+ Enable
+ Disable
+ Delete
+ Rename
- Timed out waiting for the eSIM chip to switch profiles. This may be a bug in your phone\'s modem firmware. Try toggling airplane mode, restarting the application, or rebooting the phone.
- The operation was successful, but your phone\'s modem refused to refresh. You might need to toggle airplane mode or reboot in order to use the new profile.
+ Timed out waiting for the eSIM chip to switch profiles. This may be a bug in your phone\'s modem firmware. Try toggling airplane mode, restarting the application, or rebooting the phone.
+ The operation was successful, but your phone\'s modem refused to refresh. You might need to toggle airplane mode or reboot in order to use the new profile.
Cannot switch to new eSIM profile.
Confirmation string mismatch
@@ -101,6 +104,27 @@
Last APDU exception:
Save
Diagnostics at %s
+ This eSIM profile is already present on your eSIM chip.
+ Your eSIM chip does not have sufficient memory left to download the profile.
+ This eSIM profile is unsupported by your eSIM chip.
+ An error occurred in your eSIM chip.
+ The EID of your device or eSIM chip is unsupported by your carrier.
+ This eSIM profile has been downloaded on another device.
+ This eSIM profile has been revoked.
+ The activation code is invalid.
+ The maximum number of download attempts for the eSIM profile has been exceeded.
+ Confirmation code is required to download this profile.
+ The confirmation code you entered is invalid.
+ This eSIM profile has expired.
+ The maximum number of download attempts for the confirmation code has been exceeded.
+ Unknown SM-DP+ address
+ Network is unreachable
+ TLS certificate error, this eSIM profile is not supported
+ You are trying to reinstall an already downloaded eSIM profile
+ Please delete some unused eSIM profiles and try again
+ Please contact your carrier for assistance.
+ Please contact your carrier to reissue this eSIM profile.
+ Please try again after connecting to a different network (e.g. switching between Wi-Fi and data).
Logs have been saved to the selected path. Would you like to share the log through another app?
@@ -137,10 +161,11 @@
ISD-R AID
SGP.22 Version
eUICC OS Version
- GlobalPlatform Version
+ GlobalPlatform Version
SAS Accreditation Number
Protected Profile Version
Free NVRAM (eSIM profile storage)
+ (for reference only)
Certificate Issuer (CI)
GSMA Live CI
GSMA Test CI
@@ -154,8 +179,11 @@
I CONFIRM TO ERASE THE CHIP WHOSE EID ENDS WITH %s AND UNDERSTAND THAT THIS IS IRREVERSIBLE
Erase
- Yes
- No
+
+ Yes
+ No
+ Unknown
+ Information Unavailable
Save
Logs at %s
@@ -163,10 +191,9 @@
You are %d steps away from being a developer.
You are now a developer!
- Reset
-
ISD-R AID List
Saved custom ISD-R AID list.
+ Reset
Settings
Notifications
@@ -195,6 +222,16 @@
Accept any TLS certificate used by the RSP server
Allow erasing eUICC
This is a dangerous operation and hidden by default. As an alternative, you can delete all profiles manually.
+ ES10x MSS
+ Global ES10x MSS
+
+ - High Efficiency
+ - Most Compatible
+
+
+ - 255
+ - 63
+
Customize ISD-R AID list
Some brands of removable eUICCs may use their own non-standard ISD-R AID, rendering them inaccessible to third-party apps. We can attempt to use non-standard AIDs added in this list, but there is no guarantee that they will work.
Info
diff --git a/app-common/src/main/res/xml/pref_settings.xml b/app-common/src/main/res/xml/pref_settings.xml
index 690a120..831b04d 100644
--- a/app-common/src/main/res/xml/pref_settings.xml
+++ b/app-common/src/main/res/xml/pref_settings.xml
@@ -81,6 +81,14 @@
app:summary="@string/pref_developer_euicc_memory_reset_desc"
app:title="@string/pref_developer_euicc_memory_reset" />
+
+
+ android:label="@string/quick_compatibility" />
by lazy { getCompatibilityChecks(this) }
- private val adapter = CompatibilityChecksAdapter()
-
- override fun onCreate(savedInstanceState: Bundle?) {
- enableEdgeToEdge()
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_compatibility_check)
- setSupportActionBar(requireViewById(im.angry.openeuicc.common.R.id.toolbar))
- setupToolbarInsets()
- supportActionBar!!.setDisplayHomeAsUpEnabled(true)
-
- compatibilityCheckList = requireViewById(R.id.recycler_view).also {
- it.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
- it.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
- it.adapter = adapter
- }
-
- setupRootViewInsets(compatibilityCheckList)
- }
-
- @SuppressLint("NotifyDataSetChanged")
- override fun onStart() {
- super.onStart()
- lifecycleScope.launch {
- compatibilityChecks.executeAll { adapter.notifyDataSetChanged() }
- }
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean =
- when (item.itemId) {
- android.R.id.home -> {
- finish()
- true
- }
- else -> super.onOptionsItemSelected(item)
- }
-
- inner class ViewHolder(private val root: View): RecyclerView.ViewHolder(root) {
- private val titleView: TextView = root.requireViewById(R.id.compatibility_check_title)
- private val descView: TextView = root.requireViewById(R.id.compatibility_check_desc)
- private val statusContainer: ViewGroup = root.requireViewById(R.id.compatibility_check_status_container)
-
- fun bindItem(item: CompatibilityCheck) {
- titleView.text = item.title
- descView.text = Html.fromHtml(item.description, Html.FROM_HTML_MODE_COMPACT)
-
- statusContainer.children.forEach {
- it.isVisible = false
- }
-
- val viewId = when (item.state) {
- CompatibilityCheck.State.SUCCESS -> R.id.compatibility_check_checkmark
- CompatibilityCheck.State.FAILURE -> R.id.compatibility_check_error
- CompatibilityCheck.State.FAILURE_UNKNOWN -> R.id.compatibility_check_unknown
- else -> R.id.compatibility_check_progress_bar
- }
- root.requireViewById(viewId).isVisible = true
- }
- }
-
- inner class CompatibilityChecksAdapter: RecyclerView.Adapter() {
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
- ViewHolder(layoutInflater.inflate(R.layout.compatibility_check_item, parent, false))
-
- override fun getItemCount(): Int =
- compatibilityChecks.indexOfLast { it.state != CompatibilityCheck.State.NOT_STARTED } + 1
-
- override fun onBindViewHolder(holder: ViewHolder, position: Int) {
- holder.bindItem(compatibilityChecks[position])
- }
- }
-}
\ No newline at end of file
diff --git a/app-unpriv/src/main/java/im/angry/openeuicc/ui/QuickCompatibilityActivity.kt b/app-unpriv/src/main/java/im/angry/openeuicc/ui/QuickCompatibilityActivity.kt
new file mode 100644
index 0000000..d5e599f
--- /dev/null
+++ b/app-unpriv/src/main/java/im/angry/openeuicc/ui/QuickCompatibilityActivity.kt
@@ -0,0 +1,24 @@
+package im.angry.openeuicc.ui
+
+import android.os.Bundle
+import androidx.activity.enableEdgeToEdge
+import androidx.appcompat.app.AppCompatActivity
+import im.angry.easyeuicc.R
+import im.angry.openeuicc.di.UnprivilegedUiComponentFactory
+import im.angry.openeuicc.util.OpenEuiccContextMarker
+
+class QuickCompatibilityActivity : AppCompatActivity(), OpenEuiccContextMarker {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ setContentView(R.layout.activity_quick_compatibility)
+
+ val quickCompatibilityFragment =
+ (appContainer.uiComponentFactory as UnprivilegedUiComponentFactory)
+ .createQuickCompatibilityFragment()
+
+ supportFragmentManager.beginTransaction()
+ .replace(R.id.quick_compatibility_container, quickCompatibilityFragment)
+ .commit()
+ }
+}
diff --git a/app-unpriv/src/main/java/im/angry/openeuicc/ui/QuickCompatibilityFragment.kt b/app-unpriv/src/main/java/im/angry/openeuicc/ui/QuickCompatibilityFragment.kt
new file mode 100644
index 0000000..9b41730
--- /dev/null
+++ b/app-unpriv/src/main/java/im/angry/openeuicc/ui/QuickCompatibilityFragment.kt
@@ -0,0 +1,186 @@
+package im.angry.openeuicc.ui
+
+import android.content.pm.PackageManager
+import android.icu.text.ListFormatter
+import android.os.Build
+import android.os.Bundle
+import android.se.omapi.Reader
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.CheckBox
+import android.widget.TextView
+import androidx.core.view.isVisible
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.lifecycleScope
+import im.angry.easyeuicc.R
+import im.angry.openeuicc.util.EUICC_DEFAULT_ISDR_AID
+import im.angry.openeuicc.util.UnprivilegedEuiccContextMarker
+import im.angry.openeuicc.util.connectSEService
+import im.angry.openeuicc.util.decodeHex
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+
+open class QuickCompatibilityFragment : Fragment(), UnprivilegedEuiccContextMarker {
+ companion object {
+ enum class Compatibility {
+ COMPATIBLE,
+ NOT_COMPATIBLE,
+ }
+
+ data class CompatibilityResult(
+ val compatibility: Compatibility,
+ val slotsOmapi: List = emptyList(),
+ val slotsIsdr: List = emptyList()
+ )
+ }
+
+ private val conclusion: TextView by lazy {
+ requireView().requireViewById(R.id.quick_compatibility_conclusion)
+ }
+
+ private val resultSlots: TextView by lazy {
+ requireView().requireViewById(R.id.quick_compatibility_result_slots)
+ }
+
+ private val resultSlotsIsdr: TextView by lazy {
+ requireView().requireViewById(R.id.quick_compatibility_result_slots_isdr)
+ }
+
+ private val resultNotes: TextView by lazy {
+ requireView().requireViewById(R.id.quick_compatibility_result_notes)
+ }
+
+ private val skipCheckBox: CheckBox by lazy {
+ requireView().requireViewById(R.id.quick_compatibility_skip)
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View = inflater.inflate(R.layout.fragment_quick_compatibility, container, false).apply {
+ requireViewById(R.id.quick_compatibility_device_information)
+ .text = formatDeviceInformation()
+ requireViewById
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index b9c2100..4012227 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,3 +1,4 @@
+import com.android.build.gradle.internal.api.ApkVariantOutputImpl
import im.angry.openeuicc.build.*
plugins {
@@ -23,6 +24,9 @@ android {
}
buildTypes {
+ defaultConfig {
+ versionNameSuffix = "-priv"
+ }
release {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
@@ -45,4 +49,63 @@ dependencies {
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
+}
+
+val modulePropsTemplate = mutableMapOf(
+ "id" to android.defaultConfig.applicationId!!,
+ "name" to "OpenEUICC",
+ "version" to android.defaultConfig.versionName!!,
+ "versionCode" to "${android.defaultConfig.versionCode}",
+ "author" to "OpenEUICC authors",
+ "description" to "OpenEUICC is an open-source app that provides system-level eSIM integration."
+)
+
+val moduleCustomizeScript = project.file("magisk/customize.sh").readText()
+ .replace("{APK_NAME}", "OpenEUICC")
+ .replace("{PKG_NAME}", android.defaultConfig.applicationId!!)
+
+val moduleUninstallScript = project.file("magisk/uninstall.sh").readText()
+ .replace("{PKG_NAME}", android.defaultConfig.applicationId!!)
+
+tasks.register("assembleDebugMagiskModuleDir") {
+ variant = "debug"
+ appName = "OpenEUICC"
+ permsFile = project.rootProject.file("privapp_whitelist_im.angry.openeuicc.xml")
+ moduleInstaller = project.file("magisk/module_installer.sh")
+ moduleCustomizeScriptText = moduleCustomizeScript
+ moduleUninstallScriptText = moduleUninstallScript
+ moduleProp = modulePropsTemplate.let {
+ it["description"] = "(debug build) ${it["description"]}"
+ it["versionCode"] = "${(android.applicationVariants.find { it.name == "debug" }!!.outputs.first() as ApkVariantOutputImpl).versionCodeOverride}"
+ it["updateJson"] = "https://openeuicc.com/magisk/magisk-debug.json"
+ it
+ }
+ dependsOn("assembleDebug")
+}
+
+tasks.register("assembleDebugMagiskModule") {
+ dependsOn("assembleDebugMagiskModuleDir")
+ from((tasks.getByName("assembleDebugMagiskModuleDir") as MagiskModuleDirTask).outputDir)
+ archiveFileName = "magisk-debug.zip"
+ destinationDirectory = project.layout.buildDirectory.dir("magisk")
+ entryCompression = ZipEntryCompression.STORED
+}
+
+tasks.register("assembleReleaseMagiskModuleDir") {
+ variant = "release"
+ appName = "OpenEUICC"
+ permsFile = project.rootProject.file("privapp_whitelist_im.angry.openeuicc.xml")
+ moduleInstaller = project.file("magisk/module_installer.sh")
+ moduleCustomizeScriptText = moduleCustomizeScript
+ moduleUninstallScriptText = moduleUninstallScript
+ moduleProp = modulePropsTemplate
+ dependsOn("assembleRelease")
+}
+
+tasks.register("assembleReleaseMagiskModule") {
+ dependsOn("assembleReleaseMagiskModuleDir")
+ from((tasks.getByName("assembleReleaseMagiskModuleDir") as MagiskModuleDirTask).outputDir)
+ archiveFileName = "magisk-release.zip"
+ destinationDirectory = project.layout.buildDirectory.dir("magisk")
+ entryCompression = ZipEntryCompression.STORED
}
\ No newline at end of file
diff --git a/app/magisk/customize.sh b/app/magisk/customize.sh
new file mode 100644
index 0000000..3b57a55
--- /dev/null
+++ b/app/magisk/customize.sh
@@ -0,0 +1,9 @@
+TMP_FILE="$TMPDIR/{APK_NAME}"
+
+chmod u+x "$MODPATH/uninstall.sh"
+cp "$MODPATH/system/system_ext/priv-app/{APK_NAME}/{APK_NAME}.apk" "$TMP_FILE"
+
+pm install -r "$TMP_FILE"
+rm -f "$TMP_FILE"
+
+pm grant "{PKG_NAME}" android.permission.READ_PHONE_STATE
\ No newline at end of file
diff --git a/app/magisk/module_installer.sh b/app/magisk/module_installer.sh
new file mode 100644
index 0000000..28b48e5
--- /dev/null
+++ b/app/magisk/module_installer.sh
@@ -0,0 +1,33 @@
+#!/sbin/sh
+
+#################
+# Initialization
+#################
+
+umask 022
+
+# echo before loading util_functions
+ui_print() { echo "$1"; }
+
+require_new_magisk() {
+ ui_print "*******************************"
+ ui_print " Please install Magisk v20.4+! "
+ ui_print "*******************************"
+ exit 1
+}
+
+#########################
+# Load util_functions.sh
+#########################
+
+OUTFD=$2
+ZIPFILE=$3
+
+mount /data 2>/dev/null
+
+[ -f /data/adb/magisk/util_functions.sh ] || require_new_magisk
+. /data/adb/magisk/util_functions.sh
+[ $MAGISK_VER_CODE -lt 20400 ] && require_new_magisk
+
+install_module
+exit 0
diff --git a/app/magisk/uninstall.sh b/app/magisk/uninstall.sh
new file mode 100644
index 0000000..1eb0200
--- /dev/null
+++ b/app/magisk/uninstall.sh
@@ -0,0 +1 @@
+pm uninstall "{PKG_NAME}"
\ No newline at end of file
diff --git a/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt b/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt
index dcf2328..876387f 100644
--- a/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt
+++ b/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt
@@ -2,7 +2,6 @@ package im.angry.openeuicc.core
import android.content.Context
import android.util.Log
-import im.angry.openeuicc.OpenEuiccApplication
import im.angry.openeuicc.R
import im.angry.openeuicc.util.*
import kotlinx.coroutines.flow.first
@@ -16,14 +15,13 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto
@Suppress("NAME_SHADOWING")
override suspend fun tryOpenEuiccChannel(
port: UiccPortInfoCompat,
- isdrAid: ByteArray,
- seId: EuiccChannel.SecureElementId,
+ isdrAid: ByteArray
): EuiccChannel? {
val port = port as RealUiccPortInfoCompat
if (port.card.isRemovable) {
// Attempt unprivileged (OMAPI) before TelephonyManager
// but still try TelephonyManager in case OMAPI is broken
- super.tryOpenEuiccChannel(port, isdrAid, seId)?.let { return it }
+ super.tryOpenEuiccChannel(port, isdrAid)?.let { return it }
}
if (port.card.isEuicc || preferenceRepository.removableTelephonyManagerFlow.first()) {
@@ -33,7 +31,7 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto
)
try {
return EuiccChannelImpl(
- context.getString(R.string.telephony_manager),
+ context.getString(R.string.channel_type_telephony_manager),
port,
intrinsicChannelName = null,
TelephonyManagerApduInterface(
@@ -42,9 +40,9 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto
context.preferenceRepository.verboseLoggingFlow
),
isdrAid,
- seId,
context.preferenceRepository.verboseLoggingFlow,
context.preferenceRepository.ignoreTLSCertificateFlow,
+ context.preferenceRepository.es10xMssFlow,
)
} catch (_: IllegalArgumentException) {
// Failed
@@ -55,6 +53,6 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto
}
}
- return super.tryOpenEuiccChannel(port, isdrAid, seId)
+ return super.tryOpenEuiccChannel(port, isdrAid)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/im/angry/openeuicc/di/PrivilegedUiComponentFactory.kt b/app/src/main/java/im/angry/openeuicc/di/PrivilegedUiComponentFactory.kt
index 8af5833..e5b747a 100644
--- a/app/src/main/java/im/angry/openeuicc/di/PrivilegedUiComponentFactory.kt
+++ b/app/src/main/java/im/angry/openeuicc/di/PrivilegedUiComponentFactory.kt
@@ -1,18 +1,13 @@
package im.angry.openeuicc.di
import androidx.fragment.app.Fragment
-import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.ui.EuiccManagementFragment
import im.angry.openeuicc.ui.PrivilegedEuiccManagementFragment
import im.angry.openeuicc.ui.PrivilegedSettingsFragment
class PrivilegedUiComponentFactory : DefaultUiComponentFactory() {
- override fun createEuiccManagementFragment(
- slotId: Int,
- portId: Int,
- seId: EuiccChannel.SecureElementId
- ): EuiccManagementFragment =
- PrivilegedEuiccManagementFragment.newInstance(slotId, portId, seId)
+ override fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment =
+ PrivilegedEuiccManagementFragment.newInstance(slotId, portId)
override fun createSettingsFragment(): Fragment =
PrivilegedSettingsFragment()
diff --git a/app/src/main/java/im/angry/openeuicc/ui/PrivilegedEuiccManagementFragment.kt b/app/src/main/java/im/angry/openeuicc/ui/PrivilegedEuiccManagementFragment.kt
index bc38fb5..12b60bd 100644
--- a/app/src/main/java/im/angry/openeuicc/ui/PrivilegedEuiccManagementFragment.kt
+++ b/app/src/main/java/im/angry/openeuicc/ui/PrivilegedEuiccManagementFragment.kt
@@ -5,18 +5,13 @@ import android.view.ViewGroup
import android.widget.Button
import android.widget.PopupMenu
import im.angry.openeuicc.R
-import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.util.*
import net.typeblog.lpac_jni.LocalProfileInfo
-class PrivilegedEuiccManagementFragment : EuiccManagementFragment() {
+class PrivilegedEuiccManagementFragment: EuiccManagementFragment() {
companion object {
- fun newInstance(
- slotId: Int,
- portId: Int,
- seId: EuiccChannel.SecureElementId
- ): EuiccManagementFragment =
- newInstanceEuicc(PrivilegedEuiccManagementFragment::class.java, slotId, portId, seId)
+ fun newInstance(slotId: Int, portId: Int): EuiccManagementFragment =
+ newInstanceEuicc(PrivilegedEuiccManagementFragment::class.java, slotId, portId)
}
private var isMEP = false
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index acc1728..920801c 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -1,7 +1,7 @@
このデバイスで eUICC が見つかりません。\nデバイスによってはアプリのメニューからデュアル SIM を有効化する必要があります。
- TelephonyManager (特権)
+ TelephonyManager (特権)
デュアル SIM
DSDS の状態が切り替わりました。モデムが再起動するまでお待ちください。
このスロットは MEP (Multiple Enabled Profiles) をサポートしています。この機能を有効化または無効化するには「スロットマッピングツール」を使用してください。
@@ -17,6 +17,7 @@
使用しているデバイスは eSIM をサポートしています。モバイルネットワークに接続するには通信事業者が発行した eSIM をダウンロードするか、物理 SIM を挿入してください。
スキップ
eSIM をダウンロード
- TelephonyManagerをどこでも使用
- デフォルトでは、非特権モード (EasyEUICC) と一致するように、取り外し可能な eUICC に対して OMAPI のみが試行されます。これは、一部のデバイスではうまく機能しない可能性があります。このオプションを選択する場合、取り外し可能な eUICC でも TelephonyManager を使用することになります。
+
+ TelephonyManager を常に使用可能にする
+ デフォルトでは、非特権モード (EasyEUICC など) での動作と一致させるためにリムーバブル eUICC に対しては OMAPI のみを試行します。ただし、一部デバイスでは正常に動作しない場合があります。このオプションを選択するとリムーバブル eUICC でも TelephonyManager の使用が強制されます。
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index d6befc2..acd9a61 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -16,7 +16,7 @@
您的设备支持 eSIM。要连接到移动网络,请下载运营商发布的 eSIM,或插入物理 SIM 卡。
跳过
下载 eSIM
- TelephonyManager (特权)
+ TelephonyManager (特权)
全局使用 TelephonyManager
在默认情况下,可移除 eUICC 将仅使用 OMAPI。这与非特权模式 (EasyEUICC) 一致。在某些设备上 OMAPI 可能存在问题 -- 选择此选项以强制使用 TelephonyManager。
\ No newline at end of file
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index 10285d3..52b5aa8 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -16,7 +16,7 @@
您的裝置支援 eSIM。要連線到行動網路,請下載電信業者釋出的 eSIM,或插入實體 SIM 卡。
跳過
下載 eSIM
- TelephonyManager (特權)
+ TelephonyManager (特權)
全域使用 TelephonyManager
在預設情況下,可移除 eUICC 將僅使用 OMAPI。這與非特權模式 (EasyEUICC) 一致。在某些裝置上 OMAPI 可能有問題 -- 選擇此選項以強制使用 TelephonyManager。
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ddf17e4..2c26bc3 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,7 +1,7 @@
OpenEUICC
No eUICC found on this device.\nOn some devices, you may need to enable dual SIM first in the menu of this app.
- TelephonyManager (Privileged)
+ TelephonyManager (Privileged)
Dual SIM
diff --git a/buildSrc/src/main/kotlin/im/angry/openeuicc/build/Magisk.kt b/buildSrc/src/main/kotlin/im/angry/openeuicc/build/Magisk.kt
new file mode 100644
index 0000000..e5ccfa3
--- /dev/null
+++ b/buildSrc/src/main/kotlin/im/angry/openeuicc/build/Magisk.kt
@@ -0,0 +1,74 @@
+package im.angry.openeuicc.build
+
+import org.gradle.api.DefaultTask
+import org.gradle.api.provider.MapProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputDirectory
+import org.gradle.api.tasks.InputFile
+import org.gradle.api.tasks.OutputDirectory
+import org.gradle.api.tasks.TaskAction
+import java.io.File
+
+abstract class MagiskModuleDirTask : DefaultTask() {
+ @get:Input
+ abstract val variant : Property
+
+ @get:Input
+ abstract val appName : Property
+
+ @get:InputFile
+ abstract val permsFile : Property
+
+ @get:InputFile
+ abstract val moduleInstaller : Property
+
+ @get:Input
+ abstract val moduleCustomizeScriptText : Property
+
+ @get:Input
+ abstract val moduleUninstallScriptText : Property
+
+ @get:Input
+ abstract val moduleProp : MapProperty
+
+ @InputDirectory
+ val inputDir = variant.map { project.layout.buildDirectory.dir("outputs/apk/${it}") }
+
+ @OutputDirectory
+ val outputDir = variant.map { project.layout.buildDirectory.dir("magisk/${it}") }
+
+ @TaskAction
+ fun build() {
+ val dir = outputDir.get().get()
+ project.mkdir(dir)
+ val systemExtDir = dir.dir("system/system_ext")
+ val permDir = dir.dir("system/system_ext/etc/permissions")
+ val appDir = systemExtDir.dir("priv-app/${appName.get()}")
+ val metaInfDir = dir.dir("META-INF/com/google/android")
+ project.mkdir(systemExtDir)
+ project.mkdir(metaInfDir)
+ project.mkdir(appDir)
+ project.mkdir(permDir)
+ project.copy {
+ into(appDir)
+ from(inputDir) {
+ include("app-${variant.get()}.apk")
+ rename("app-${variant.get()}.apk", "${appName.get()}.apk")
+ }
+ }
+ project.copy {
+ from(permsFile)
+ into(permDir)
+ }
+ project.copy {
+ from(moduleInstaller)
+ into(metaInfDir)
+ rename(".*", "update-binary")
+ }
+ dir.file("customize.sh").asFile.writeText(moduleCustomizeScriptText.get())
+ dir.file("uninstall.sh").asFile.writeText(moduleUninstallScriptText.get())
+ metaInfDir.file("updater-script").asFile.writeText("#MAGISK\n")
+ dir.file("module.prop").asFile.writeText(moduleProp.get().map { (k, v) -> "$k=$v" }.joinToString("\n"))
+ }
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/im/angry/openeuicc/build/Versioning.kt b/buildSrc/src/main/kotlin/im/angry/openeuicc/build/Versioning.kt
index 24f0235..ef07c43 100644
--- a/buildSrc/src/main/kotlin/im/angry/openeuicc/build/Versioning.kt
+++ b/buildSrc/src/main/kotlin/im/angry/openeuicc/build/Versioning.kt
@@ -16,7 +16,7 @@ val Project.gitVersionCode: Int
standardOutput = stdout
}
stdout.toString("utf-8").trim('\n').toInt()
- } catch (e: Exception) {
+ } catch (_: Exception) {
0
}
@@ -29,7 +29,7 @@ val Project.gitVersionName: String
standardOutput = stdout
}
stdout.toString("utf-8").trim('\n')
- } catch (e: Exception) {
+ } catch (_: Exception) {
"Unknown"
}
@@ -38,7 +38,7 @@ class MyVersioningPlugin: Plugin {
target.configure {
defaultConfig {
versionCode = target.gitVersionCode
- versionName = target.gitVersionName
+ versionName = target.gitVersionName.removePrefix("unpriv-")
}
applicationVariants.all {
diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt
index 3674f4f..ce09717 100644
--- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt
+++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt
@@ -11,12 +11,14 @@ import net.typeblog.lpac_jni.LocalProfileInfo
import net.typeblog.lpac_jni.LocalProfileNotification
import net.typeblog.lpac_jni.ProfileDownloadCallback
import net.typeblog.lpac_jni.Version
+import java.util.concurrent.locks.ReentrantLock
+import kotlin.concurrent.withLock
class LocalProfileAssistantImpl(
isdrAid: ByteArray,
rawApduInterface: ApduInterface,
rawHttpInterface: HttpInterface
-): LocalProfileAssistant {
+) : LocalProfileAssistant {
companion object {
private const val TAG = "LocalProfileAssistantImpl"
}
@@ -74,6 +76,10 @@ class LocalProfileAssistantImpl(
}
}
+ // Controls concurrency of every single method in this class, since
+ // the C-side is explicitly NOT thread-safe
+ private val lock = ReentrantLock()
+
private val apduInterface = ApduInterfaceWrapper(rawApduInterface)
private val httpInterface = HttpInterfaceWrapper(rawHttpInterface)
@@ -105,23 +111,24 @@ class LocalProfileAssistantImpl(
}
override val profiles: List
- @Synchronized
- get() {
+ get() = lock.withLock {
val head = LpacJni.es10cGetProfilesInfo(contextHandle)
var curr = head
val ret = mutableListOf()
while (curr != 0L) {
val state = LocalProfileInfo.State.fromString(LpacJni.profileGetStateString(curr))
val clazz = LocalProfileInfo.Clazz.fromString(LpacJni.profileGetClassString(curr))
- ret.add(LocalProfileInfo(
- LpacJni.profileGetIccid(curr),
- state,
- LpacJni.profileGetName(curr),
- LpacJni.profileGetNickname(curr),
- LpacJni.profileGetServiceProvider(curr),
- LpacJni.profileGetIsdpAid(curr),
- clazz
- ))
+ ret.add(
+ LocalProfileInfo(
+ LpacJni.profileGetIccid(curr),
+ state,
+ LpacJni.profileGetName(curr),
+ LpacJni.profileGetNickname(curr),
+ LpacJni.profileGetServiceProvider(curr),
+ LpacJni.profileGetIsdpAid(curr),
+ clazz
+ )
+ )
curr = LpacJni.profilesNext(curr)
}
@@ -130,79 +137,87 @@ class LocalProfileAssistantImpl(
}
override val notifications: List
- @Synchronized
- get() {
+ get() = lock.withLock {
val head = LpacJni.es10bListNotification(contextHandle)
var curr = head
- val ret = mutableListOf()
- while (curr != 0L) {
- ret.add(LocalProfileNotification(
- LpacJni.notificationGetSeq(curr),
- LocalProfileNotification.Operation.fromString(LpacJni.notificationGetOperationString(curr)),
- LpacJni.notificationGetAddress(curr),
- LpacJni.notificationGetIccid(curr),
- ))
- curr = LpacJni.notificationsNext(curr)
+
+ try {
+ val ret = mutableListOf()
+ while (curr != 0L) {
+ ret.add(
+ LocalProfileNotification(
+ LpacJni.notificationGetSeq(curr),
+ LocalProfileNotification.Operation.fromString(
+ LpacJni.notificationGetOperationString(
+ curr
+ )
+ ),
+ LpacJni.notificationGetAddress(curr),
+ LpacJni.notificationGetIccid(curr),
+ )
+ )
+ curr = LpacJni.notificationsNext(curr)
+ }
+ return ret.sortedBy { it.seqNumber }.reversed()
+ } finally {
+ LpacJni.notificationsFree(head)
}
- LpacJni.notificationsFree(head)
- return ret.sortedBy { it.seqNumber }.reversed()
}
override val eID: String
- @Synchronized
- get() = LpacJni.es10cGetEid(contextHandle)!!
+ get() = lock.withLock { LpacJni.es10cGetEid(contextHandle)!! }
override val euiccInfo2: EuiccInfo2?
- @Synchronized
- get() {
+ get() = lock.withLock {
val cInfo = LpacJni.es10cexGetEuiccInfo2(contextHandle)
if (cInfo == 0L) return null
- val ret = EuiccInfo2(
- Version(LpacJni.euiccInfo2GetSGP22Version(cInfo)),
- Version(LpacJni.euiccInfo2GetProfileVersion(cInfo)),
- Version(LpacJni.euiccInfo2GetEuiccFirmwareVersion(cInfo)),
- Version(LpacJni.euiccInfo2GetGlobalPlatformVersion(cInfo)),
- LpacJni.euiccInfo2GetSasAcreditationNumber(cInfo),
- Version(LpacJni.euiccInfo2GetPpVersion(cInfo)),
- LpacJni.euiccInfo2GetFreeNonVolatileMemory(cInfo).toInt(),
- LpacJni.euiccInfo2GetFreeVolatileMemory(cInfo).toInt(),
- buildSet {
- var cursor = LpacJni.euiccInfo2GetEuiccCiPKIdListForSigning(cInfo)
- while (cursor != 0L) {
- add(LpacJni.stringDeref(cursor))
- cursor = LpacJni.stringArrNext(cursor)
- }
- },
- buildSet {
- var cursor = LpacJni.euiccInfo2GetEuiccCiPKIdListForVerification(cInfo)
- while (cursor != 0L) {
- add(LpacJni.stringDeref(cursor))
- cursor = LpacJni.stringArrNext(cursor)
- }
- },
- )
-
- LpacJni.euiccInfo2Free(cInfo)
-
- return ret
+ try {
+ return EuiccInfo2(
+ Version(LpacJni.euiccInfo2GetSGP22Version(cInfo)),
+ Version(LpacJni.euiccInfo2GetProfileVersion(cInfo)),
+ Version(LpacJni.euiccInfo2GetEuiccFirmwareVersion(cInfo)),
+ Version(LpacJni.euiccInfo2GetGlobalPlatformVersion(cInfo)),
+ LpacJni.euiccInfo2GetSasAcreditationNumber(cInfo),
+ Version(LpacJni.euiccInfo2GetPpVersion(cInfo)),
+ LpacJni.euiccInfo2GetFreeNonVolatileMemory(cInfo).toInt(),
+ LpacJni.euiccInfo2GetFreeVolatileMemory(cInfo).toInt(),
+ buildSet {
+ var cursor = LpacJni.euiccInfo2GetEuiccCiPKIdListForSigning(cInfo)
+ while (cursor != 0L) {
+ add(LpacJni.stringDeref(cursor))
+ cursor = LpacJni.stringArrNext(cursor)
+ }
+ },
+ buildSet {
+ var cursor = LpacJni.euiccInfo2GetEuiccCiPKIdListForVerification(cInfo)
+ while (cursor != 0L) {
+ add(LpacJni.stringDeref(cursor))
+ cursor = LpacJni.stringArrNext(cursor)
+ }
+ },
+ )
+ } finally {
+ LpacJni.euiccInfo2Free(cInfo)
+ }
}
- @Synchronized
- override fun enableProfile(iccid: String, refresh: Boolean): Boolean =
+ override fun enableProfile(iccid: String, refresh: Boolean): Boolean = lock.withLock {
LpacJni.es10cEnableProfile(contextHandle, iccid, refresh) == 0
+ }
- @Synchronized
- override fun disableProfile(iccid: String, refresh: Boolean): Boolean =
+ override fun disableProfile(iccid: String, refresh: Boolean): Boolean = lock.withLock {
LpacJni.es10cDisableProfile(contextHandle, iccid, refresh) == 0
+ }
- @Synchronized
- override fun deleteProfile(iccid: String): Boolean =
+ override fun deleteProfile(iccid: String): Boolean = lock.withLock {
LpacJni.es10cDeleteProfile(contextHandle, iccid) == 0
+ }
- @Synchronized
- override fun downloadProfile(smdp: String, matchingId: String?, imei: String?,
- confirmationCode: String?, callback: ProfileDownloadCallback) {
+ override fun downloadProfile(
+ smdp: String, matchingId: String?, imei: String?,
+ confirmationCode: String?, callback: ProfileDownloadCallback
+ ) = lock.withLock {
val res = LpacJni.downloadProfile(
contextHandle,
smdp,
@@ -229,18 +244,17 @@ class LocalProfileAssistantImpl(
}
}
- @Synchronized
- override fun deleteNotification(seqNumber: Long): Boolean =
+ override fun deleteNotification(seqNumber: Long): Boolean = lock.withLock {
LpacJni.es10bDeleteNotification(contextHandle, seqNumber) == 0
+ }
- @Synchronized
- override fun handleNotification(seqNumber: Long): Boolean =
+ override fun handleNotification(seqNumber: Long): Boolean = lock.withLock {
LpacJni.handleNotification(contextHandle, seqNumber).also {
Log.d(TAG, "handleNotification $seqNumber = $it")
} == 0
+ }
- @Synchronized
- override fun setNickname(iccid: String, nickname: String) {
+ override fun setNickname(iccid: String, nickname: String) = lock.withLock {
val encoded = try {
Charsets.UTF_8.encode(nickname).array()
} catch (e: CharacterCodingException) {
@@ -259,11 +273,12 @@ class LocalProfileAssistantImpl(
}
override fun euiccMemoryReset() {
- LpacJni.es10cEuiccMemoryReset(contextHandle)
+ lock.withLock {
+ LpacJni.es10cEuiccMemoryReset(contextHandle)
+ }
}
- @Synchronized
- override fun close() {
+ override fun close() = lock.withLock {
if (!finalized) {
LpacJni.euiccFini(contextHandle)
LpacJni.destroyContext(contextHandle)
diff --git a/libs/lpac-jni/src/main/jni/Application.mk b/libs/lpac-jni/src/main/jni/Application.mk
index 2112196..c1d3766 100644
--- a/libs/lpac-jni/src/main/jni/Application.mk
+++ b/libs/lpac-jni/src/main/jni/Application.mk
@@ -1,4 +1,5 @@
APP_ABI := all
APP_SHORT_COMMANDS := true
APP_CFLAGS := -Wno-compound-token-split-by-macro
-APP_LDFLAGS := -Wl,--build-id=none -z muldefs
\ No newline at end of file
+APP_LDFLAGS := -Wl,--build-id=none -z muldefs
+APP_SUPPORT_FLEXIBLE_PAGE_SIZES := true
diff --git a/libs/lpac-jni/src/main/jni/lpac-jni.mk b/libs/lpac-jni/src/main/jni/lpac-jni.mk
index c0bcee7..dad173c 100644
--- a/libs/lpac-jni/src/main/jni/lpac-jni.mk
+++ b/libs/lpac-jni/src/main/jni/lpac-jni.mk
@@ -1,4 +1,5 @@
LOCAL_PATH := $(call my-dir)
+LOCAL_LDFLAGS += "-Wl,-z,max-page-size=16384"
# function to find all *.c files under a directory
define all-c-files-under