From 5ee54d14d817ee753896f8a5d296b1c22114372a Mon Sep 17 00:00:00 2001 From: septs Date: Mon, 10 Feb 2025 13:03:49 +0800 Subject: [PATCH 1/9] feat: custom isd-r aids --- .../im/angry/openeuicc/ui/SettingsFragment.kt | 16 ++++++++++++++++ .../im/angry/openeuicc/util/PreferenceUtils.kt | 3 +++ app-common/src/main/res/values/strings.xml | 2 ++ app-common/src/main/res/xml/pref_settings.xml | 6 ++++++ 4 files changed, 27 insertions(+) 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 fab680f..d07be32 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.EditTextPreference import androidx.preference.Preference import androidx.preference.PreferenceCategory import androidx.preference.PreferenceFragmentCompat @@ -72,6 +73,21 @@ open class SettingsFragment: PreferenceFragmentCompat() { requirePreference("pref_advanced_verbose_logging") .bindBooleanFlow(preferenceRepository.verboseLoggingFlow) + requirePreference("pref_developer_custom_aids").apply { + val flow = preferenceRepository.isdRAIDFallbackFlow + + lifecycleScope.launch { + flow.collect { text = it.joinToString("\n") } + } + + setOnPreferenceChangeListener { _, newValue -> + val text = newValue.toString().uppercase() + val valid = text.all { it.isDigit() || it in 'A'..'F' || it == '\n' } + if (valid) runBlocking { flow.updatePreference(text.split('\n').toSet()) } + valid + } + } + requirePreference("pref_developer_unfiltered_profile_list") .bindBooleanFlow(preferenceRepository.unfilteredProfileListFlow) 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 f5e3ca2..aa8a437 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.stringSetPreferencesKey import androidx.datastore.preferences.preferencesDataStore import androidx.fragment.app.Fragment import im.angry.openeuicc.OpenEuiccApplication @@ -31,6 +32,7 @@ internal object PreferenceKeys { // ---- Developer Options ---- val DEVELOPER_OPTIONS_ENABLED = booleanPreferencesKey("developer_options_enabled") + val ISD_R_AID_FALLBACK = stringSetPreferencesKey("isd_r_aid_fallback") val UNFILTERED_PROFILE_LIST = booleanPreferencesKey("unfiltered_profile_list") val IGNORE_TLS_CERTIFICATE = booleanPreferencesKey("ignore_tls_certificate") } @@ -48,6 +50,7 @@ class PreferenceRepository(private val context: Context) { // ---- Developer Options ---- val developerOptionsEnabledFlow = bindFlow(PreferenceKeys.DEVELOPER_OPTIONS_ENABLED, false) + val isdRAIDFallbackFlow = bindFlow(PreferenceKeys.ISD_R_AID_FALLBACK, emptySet()) val unfilteredProfileListFlow = bindFlow(PreferenceKeys.UNFILTERED_PROFILE_LIST, false) val ignoreTLSCertificateFlow = bindFlow(PreferenceKeys.IGNORE_TLS_CERTIFICATE, false) diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index 71e2418..977b088 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -166,6 +166,8 @@ Logs View recent debug logs of the application Developer Options + ISD-R AIDs Fallback + Prefer to use standard ISD-R AID access, one ISD-R AID per line (hexadecimal string) Show unfiltered profile list Include non-production profiles in the list Ignore SM-DP+ TLS certificate diff --git a/app-common/src/main/res/xml/pref_settings.xml b/app-common/src/main/res/xml/pref_settings.xml index bb5bd50..173a0a5 100644 --- a/app-common/src/main/res/xml/pref_settings.xml +++ b/app-common/src/main/res/xml/pref_settings.xml @@ -57,6 +57,12 @@ app:title="@string/pref_developer" app:iconSpaceReserved="false"> + + Date: Mon, 10 Feb 2025 13:22:43 +0800 Subject: [PATCH 2/9] feat: custom isd-r aids --- .../core/DefaultEuiccChannelFactory.kt | 2 ++ .../angry/openeuicc/core/EuiccChannelImpl.kt | 9 ++++++-- .../im/angry/openeuicc/ui/SettingsFragment.kt | 9 +++++--- .../core/PrivilegedEuiccChannelFactory.kt | 1 + .../java/net/typeblog/lpac_jni/LpacJni.kt | 2 +- .../impl/LocalProfileAssistantImpl.kt | 22 ++++++++++++++++--- .../lpac-jni/src/main/jni/lpac-jni/lpac-jni.c | 5 ++++- 7 files changed, 40 insertions(+), 10 deletions(-) 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 5e87564..26d19bb 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 @@ -45,6 +45,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha ), context.preferenceRepository.verboseLoggingFlow, context.preferenceRepository.ignoreTLSCertificateFlow, + context.preferenceRepository.isdRAIDFallbackFlow, ).also { Log.i(DefaultEuiccChannelManager.TAG, "Is OMAPI channel, setting MSS to 60") it.lpa.setEs10xMss(60) @@ -77,6 +78,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha ), context.preferenceRepository.verboseLoggingFlow, context.preferenceRepository.ignoreTLSCertificateFlow, + context.preferenceRepository.isdRAIDFallbackFlow, ) } 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 a82cb97..17182c8 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 @@ -13,14 +13,19 @@ class EuiccChannelImpl( override val intrinsicChannelName: String?, private val apduInterface: ApduInterface, verboseLoggingFlow: Flow, - ignoreTLSCertificateFlow: Flow + ignoreTLSCertificateFlow: Flow, + isdRAidFallback: Flow>, ) : EuiccChannel { override val slotId = port.card.physicalSlotIndex override val logicalSlotId = port.logicalSlotIndex override val portId = port.portIndex override val lpa: LocalProfileAssistant = - LocalProfileAssistantImpl(apduInterface, HttpInterfaceImpl(verboseLoggingFlow, ignoreTLSCertificateFlow)) + LocalProfileAssistantImpl( + apduInterface, + HttpInterfaceImpl(verboseLoggingFlow, ignoreTLSCertificateFlow), + isdRAidFallback, + ) override val atr: ByteArray? get() = (apduInterface as? ApduInterfaceAtrProvider)?.atr 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 d07be32..34066f9 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 @@ -81,9 +81,12 @@ open class SettingsFragment: PreferenceFragmentCompat() { } setOnPreferenceChangeListener { _, newValue -> - val text = newValue.toString().uppercase() - val valid = text.all { it.isDigit() || it in 'A'..'F' || it == '\n' } - if (valid) runBlocking { flow.updatePreference(text.split('\n').toSet()) } + val lines = newValue.toString().uppercase().split('\n') + val valid = lines.all { line -> + line.length % 2 == 0 && + line.all { it.isDigit() || it in 'A'..'F' || it == '\n' } + } + if (valid) runBlocking { flow.updatePreference(lines.toSet()) } valid } } 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 b690c79..db51569 100644 --- a/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt +++ b/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt @@ -38,6 +38,7 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto ), context.preferenceRepository.verboseLoggingFlow, context.preferenceRepository.ignoreTLSCertificateFlow, + context.preferenceRepository.isdRAIDFallbackFlow, ) } catch (e: IllegalArgumentException) { // Failed diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt index 370fcab..06f8905 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt @@ -8,7 +8,7 @@ internal object LpacJni { external fun createContext(apduInterface: ApduInterface, httpInterface: HttpInterface): Long external fun destroyContext(handle: Long) - external fun euiccInit(handle: Long): Int + external fun euiccInit(handle: Long, aid: ByteArray?): Int external fun euiccSetMss(handle: Long, mss: Byte) external fun euiccFini(handle: Long) 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 7310acd..196c27c 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 @@ -1,6 +1,9 @@ package net.typeblog.lpac_jni.impl import android.util.Log +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking import net.typeblog.lpac_jni.LpacJni import net.typeblog.lpac_jni.ApduInterface import net.typeblog.lpac_jni.EuiccInfo2 @@ -13,7 +16,8 @@ import net.typeblog.lpac_jni.ProfileDownloadCallback class LocalProfileAssistantImpl( rawApduInterface: ApduInterface, - rawHttpInterface: HttpInterface + rawHttpInterface: HttpInterface, + isdRAidFallback: Flow>, ): LocalProfileAssistant { companion object { private const val TAG = "LocalProfileAssistantImpl" @@ -79,8 +83,20 @@ class LocalProfileAssistantImpl( private var contextHandle: Long = LpacJni.createContext(apduInterface, httpInterface) init { - if (LpacJni.euiccInit(contextHandle) < 0) { - throw IllegalArgumentException("Failed to initialize LPA") + val aids = runBlocking { isdRAidFallback.first() } + .map { it.toByteArray() } + .toTypedArray() + if (LpacJni.euiccInit(contextHandle, null) < 0) { + var errno = -1 + for (aid in aids) { + errno = LpacJni.euiccInit(contextHandle, aid) + if (errno == 0) { + break + } + } + if (errno < 0) { + throw IllegalArgumentException("Failed to initialize LPA") + } } val pkids = euiccInfo2?.euiccCiPKIdListForVerification ?: arrayOf() diff --git a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c index 38d4f3a..85f9cfe 100644 --- a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c +++ b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c @@ -64,8 +64,11 @@ Java_net_typeblog_lpac_1jni_LpacJni_destroyContext(JNIEnv *env, jobject thiz, jl } JNIEXPORT jint JNICALL -Java_net_typeblog_lpac_1jni_LpacJni_euiccInit(JNIEnv *env, jobject thiz, jlong handle) { +Java_net_typeblog_lpac_1jni_LpacJni_euiccInit(JNIEnv *env, jobject thiz, jlong handle, + jbyteArray aid) { struct euicc_ctx *ctx = (struct euicc_ctx *) handle; + jbyteArray _aid = (*env)->GetByteArrayElements(env, aid, NULL); + (*env)->ReleaseByteArrayElements(env, aid, _aid, JNI_ABORT); return euicc_init(ctx); } From 9d308add4948c110dc9c5a56427a7764b8b7f309 Mon Sep 17 00:00:00 2001 From: septs Date: Mon, 10 Feb 2025 13:43:15 +0800 Subject: [PATCH 3/9] feat: custom isd-r aids --- libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c index 85f9cfe..5750669 100644 --- a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c +++ b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c @@ -67,8 +67,17 @@ JNIEXPORT jint JNICALL Java_net_typeblog_lpac_1jni_LpacJni_euiccInit(JNIEnv *env, jobject thiz, jlong handle, jbyteArray aid) { struct euicc_ctx *ctx = (struct euicc_ctx *) handle; - jbyteArray _aid = (*env)->GetByteArrayElements(env, aid, NULL); - (*env)->ReleaseByteArrayElements(env, aid, _aid, JNI_ABORT); + if ((*env)->IsSameObject(env, aid, NULL)) { + ctx->aid = NULL; + } else { + size_t length = (*env)->GetArrayLength(env, aid); + jbyteArray elements = (*env)->GetByteArrayElements(env, aid, NULL); + uint8_t *copied = malloc(length); + memcpy(copied, elements, length); + (*env)->ReleaseByteArrayElements(env, aid, elements, JNI_ABORT); + ctx->aid = copied; + ctx->aid_len = length; + } return euicc_init(ctx); } From 4518b58a81d7bf7b38954cef5186f491de054c52 Mon Sep 17 00:00:00 2001 From: septs Date: Mon, 10 Feb 2025 13:56:52 +0800 Subject: [PATCH 4/9] feat: custom isd-r aids --- libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c index 5750669..f4626ee 100644 --- a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c +++ b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c @@ -70,13 +70,11 @@ Java_net_typeblog_lpac_1jni_LpacJni_euiccInit(JNIEnv *env, jobject thiz, jlong h if ((*env)->IsSameObject(env, aid, NULL)) { ctx->aid = NULL; } else { - size_t length = (*env)->GetArrayLength(env, aid); - jbyteArray elements = (*env)->GetByteArrayElements(env, aid, NULL); - uint8_t *copied = malloc(length); - memcpy(copied, elements, length); - (*env)->ReleaseByteArrayElements(env, aid, elements, JNI_ABORT); - ctx->aid = copied; - ctx->aid_len = length; + ctx->aid_len = (*env)->GetArrayLength(env, aid); + ctx->aid = malloc(ctx->aid_len); + jbyte *values = (*env)->GetByteArrayElements(env, aid, NULL); + memcpy(&ctx->aid, values, ctx->aid_len); + (*env)->ReleaseByteArrayElements(env, aid, values, JNI_ABORT); } return euicc_init(ctx); } From 5f8a8eba3e9c281febc9892406dd8b22d091fe71 Mon Sep 17 00:00:00 2001 From: septs Date: Mon, 10 Feb 2025 22:16:37 +0800 Subject: [PATCH 5/9] feat: custom isd-r aids --- .../im/angry/openeuicc/ui/SettingsFragment.kt | 59 +++++++++++++------ .../im/angry/openeuicc/util/InputFilters.kt | 18 ++++++ app-common/src/main/res/xml/pref_settings.xml | 1 + .../lpac-jni/src/main/jni/lpac-jni/lpac-jni.c | 10 ++-- 4 files changed, 66 insertions(+), 22 deletions(-) create mode 100644 app-common/src/main/java/im/angry/openeuicc/util/InputFilters.kt 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 34066f9..239e0aa 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 @@ -5,6 +5,8 @@ import android.net.Uri import android.os.Build import android.os.Bundle import android.provider.Settings +import android.text.InputFilter +import android.text.Spanned import android.widget.Toast import androidx.lifecycle.lifecycleScope import androidx.preference.CheckBoxPreference @@ -73,23 +75,8 @@ open class SettingsFragment: PreferenceFragmentCompat() { requirePreference("pref_advanced_verbose_logging") .bindBooleanFlow(preferenceRepository.verboseLoggingFlow) - requirePreference("pref_developer_custom_aids").apply { - val flow = preferenceRepository.isdRAIDFallbackFlow - - lifecycleScope.launch { - flow.collect { text = it.joinToString("\n") } - } - - setOnPreferenceChangeListener { _, newValue -> - val lines = newValue.toString().uppercase().split('\n') - val valid = lines.all { line -> - line.length % 2 == 0 && - line.all { it.isDigit() || it in 'A'..'F' || it == '\n' } - } - if (valid) runBlocking { flow.updatePreference(lines.toSet()) } - valid - } - } + requirePreference("pref_developer_custom_aids") + .let(::setCustomAIDs) requirePreference("pref_developer_unfiltered_profile_list") .bindBooleanFlow(preferenceRepository.unfilteredProfileListFlow) @@ -98,6 +85,44 @@ open class SettingsFragment: PreferenceFragmentCompat() { .bindBooleanFlow(preferenceRepository.ignoreTLSCertificateFlow) } + private fun setCustomAIDs(preference: EditTextPreference) { + val flow = preferenceRepository.isdRAIDFallbackFlow + val maxLines = 5 + + fun prepare(text: String) = text.uppercase().lines() + .map(String::trim).toSet() + .filter { line -> + if (line.length % 2 != 0) return@filter false + if (line.length !in 10..32) return@filter false + line.all { it.isDigit() || it in 'A'..'F' } + } + .take(maxLines) + + lifecycleScope.launch { + flow.collect { preference.text = it.joinToString("\n") } + } + + preference.setOnBindEditTextListener { + it.maxLines = maxLines + it.filters += HexadecimalInputFilter() + it.setText(buildString { + for (line in prepare(it.text.toString())) { + append(line) + append('\n') + } + }) + it.setSelection(it.text.length) + it.requestFocus() + } + + preference.setOnPreferenceChangeListener { _, newValue -> + runBlocking { + flow.updatePreference(prepare(newValue as String).toSet()) + } + true + } + } + protected fun requirePreference(key: CharSequence) = findPreference(key)!! diff --git a/app-common/src/main/java/im/angry/openeuicc/util/InputFilters.kt b/app-common/src/main/java/im/angry/openeuicc/util/InputFilters.kt new file mode 100644 index 0000000..f9b9e6a --- /dev/null +++ b/app-common/src/main/java/im/angry/openeuicc/util/InputFilters.kt @@ -0,0 +1,18 @@ +package im.angry.openeuicc.util + +import android.text.InputFilter +import android.text.Spanned + +class HexadecimalInputFilter : InputFilter { + override fun filter( + source: CharSequence, start: Int, end: Int, dest: Spanned, dstart: Int, dend: Int + ): CharSequence { + val range = start until end + if (range.isEmpty()) return source + val filtered = source.subSequence(range) + .map(Char::uppercaseChar) + .filter { c -> c.isDigit() || c in 'A'..'F' || c == '\n' } + .toCharArray() + return String(filtered) + } +} diff --git a/app-common/src/main/res/xml/pref_settings.xml b/app-common/src/main/res/xml/pref_settings.xml index 173a0a5..e6a75f4 100644 --- a/app-common/src/main/res/xml/pref_settings.xml +++ b/app-common/src/main/res/xml/pref_settings.xml @@ -61,6 +61,7 @@ app:iconSpaceReserved="false" app:key="pref_developer_custom_aids" app:summary="@string/pref_developer_custom_aids_desc" + app:dialogMessage="@string/pref_developer_custom_aids_desc" app:title="@string/pref_developer_custom_aids" /> IsSameObject(env, aid, NULL)) { ctx->aid = NULL; } else { - ctx->aid_len = (*env)->GetArrayLength(env, aid); - ctx->aid = malloc(ctx->aid_len); - jbyte *values = (*env)->GetByteArrayElements(env, aid, NULL); - memcpy(&ctx->aid, values, ctx->aid_len); - (*env)->ReleaseByteArrayElements(env, aid, values, JNI_ABORT); +// ctx->aid_len = (*env)->GetArrayLength(env, aid); +// ctx->aid = malloc(ctx->aid_len); +// jbyte *values = (*env)->GetByteArrayElements(env, aid, NULL); +// memcpy(&ctx->aid, values, ctx->aid_len); +// (*env)->ReleaseByteArrayElements(env, aid, values, JNI_ABORT); } return euicc_init(ctx); } From 2628200ff66cc521837e9e9f4b040abe2efe066c Mon Sep 17 00:00:00 2001 From: septs Date: Tue, 11 Feb 2025 18:30:24 +0800 Subject: [PATCH 6/9] feat: custom isd-r aids --- .../core/DefaultEuiccChannelFactory.kt | 4 +-- .../im/angry/openeuicc/ui/SettingsFragment.kt | 26 +++++-------------- .../im/angry/openeuicc/util/InputFilters.kt | 18 ++++--------- .../angry/openeuicc/util/PreferenceUtils.kt | 2 +- .../core/PrivilegedEuiccChannelFactory.kt | 2 +- 5 files changed, 16 insertions(+), 36 deletions(-) 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 26d19bb..a3e4c56 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 @@ -45,7 +45,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha ), context.preferenceRepository.verboseLoggingFlow, context.preferenceRepository.ignoreTLSCertificateFlow, - context.preferenceRepository.isdRAIDFallbackFlow, + context.preferenceRepository.isdRAidFallbackFlow, ).also { Log.i(DefaultEuiccChannelManager.TAG, "Is OMAPI channel, setting MSS to 60") it.lpa.setEs10xMss(60) @@ -78,7 +78,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha ), context.preferenceRepository.verboseLoggingFlow, context.preferenceRepository.ignoreTLSCertificateFlow, - context.preferenceRepository.isdRAIDFallbackFlow, + context.preferenceRepository.isdRAidFallbackFlow, ) } 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 239e0aa..c420229 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 @@ -5,8 +5,6 @@ import android.net.Uri import android.os.Build import android.os.Bundle import android.provider.Settings -import android.text.InputFilter -import android.text.Spanned import android.widget.Toast import androidx.lifecycle.lifecycleScope import androidx.preference.CheckBoxPreference @@ -86,31 +84,21 @@ open class SettingsFragment: PreferenceFragmentCompat() { } private fun setCustomAIDs(preference: EditTextPreference) { - val flow = preferenceRepository.isdRAIDFallbackFlow - val maxLines = 5 + val flow = preferenceRepository.isdRAidFallbackFlow + val separator = "\n" fun prepare(text: String) = text.uppercase().lines() .map(String::trim).toSet() - .filter { line -> - if (line.length % 2 != 0) return@filter false - if (line.length !in 10..32) return@filter false - line.all { it.isDigit() || it in 'A'..'F' } - } - .take(maxLines) + .filter { it.length % 2 == 0 && it.length in 10..32 && it.all(Char::isHex) } + .take(10) // limit to 10 AIDs lifecycleScope.launch { - flow.collect { preference.text = it.joinToString("\n") } + flow.collect { preference.text = it.joinToString(separator) } } preference.setOnBindEditTextListener { - it.maxLines = maxLines - it.filters += HexadecimalInputFilter() - it.setText(buildString { - for (line in prepare(it.text.toString())) { - append(line) - append('\n') - } - }) + it.filters += allowedInputFilter { c -> c.isHex() || c == '\n' } + it.setText(prepare(it.text.toString()).joinToString(separator)) it.setSelection(it.text.length) it.requestFocus() } diff --git a/app-common/src/main/java/im/angry/openeuicc/util/InputFilters.kt b/app-common/src/main/java/im/angry/openeuicc/util/InputFilters.kt index f9b9e6a..2bb8866 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/InputFilters.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/InputFilters.kt @@ -1,18 +1,10 @@ package im.angry.openeuicc.util import android.text.InputFilter -import android.text.Spanned -class HexadecimalInputFilter : InputFilter { - override fun filter( - source: CharSequence, start: Int, end: Int, dest: Spanned, dstart: Int, dend: Int - ): CharSequence { - val range = start until end - if (range.isEmpty()) return source - val filtered = source.subSequence(range) - .map(Char::uppercaseChar) - .filter { c -> c.isDigit() || c in 'A'..'F' || c == '\n' } - .toCharArray() - return String(filtered) +fun allowedInputFilter(predicate: (Char) -> Boolean) = + InputFilter { source, start, end, _, _, _ -> + source.substring(start until end).filter(predicate) } -} + +fun Char.isHex() = isDigit() || uppercaseChar() in 'A'..'F' \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt index aa8a437..5478945 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt @@ -50,7 +50,7 @@ class PreferenceRepository(private val context: Context) { // ---- Developer Options ---- val developerOptionsEnabledFlow = bindFlow(PreferenceKeys.DEVELOPER_OPTIONS_ENABLED, false) - val isdRAIDFallbackFlow = bindFlow(PreferenceKeys.ISD_R_AID_FALLBACK, emptySet()) + val isdRAidFallbackFlow = bindFlow(PreferenceKeys.ISD_R_AID_FALLBACK, emptySet()) val unfilteredProfileListFlow = bindFlow(PreferenceKeys.UNFILTERED_PROFILE_LIST, false) val ignoreTLSCertificateFlow = bindFlow(PreferenceKeys.IGNORE_TLS_CERTIFICATE, false) 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 db51569..42e5dca 100644 --- a/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt +++ b/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt @@ -38,7 +38,7 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto ), context.preferenceRepository.verboseLoggingFlow, context.preferenceRepository.ignoreTLSCertificateFlow, - context.preferenceRepository.isdRAIDFallbackFlow, + context.preferenceRepository.isdRAidFallbackFlow, ) } catch (e: IllegalArgumentException) { // Failed From 5d5c4a6897b8cd2d6ae166614922fec9bdc42dc1 Mon Sep 17 00:00:00 2001 From: septs Date: Tue, 4 Mar 2025 23:01:20 +0800 Subject: [PATCH 7/9] revert: lpac-jni --- .../angry/openeuicc/core/EuiccChannelImpl.kt | 3 ++- .../openeuicc/core/OmapiApduInterface.kt | 4 ++-- .../java/net/typeblog/lpac_jni/LpacJni.kt | 2 +- .../impl/LocalProfileAssistantImpl.kt | 22 +++---------------- libs/lpac-jni/src/main/jni/lpac | 2 +- .../lpac-jni/src/main/jni/lpac-jni/lpac-jni.c | 12 +--------- 6 files changed, 10 insertions(+), 35 deletions(-) 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 edf9673..ec93048 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 @@ -2,6 +2,8 @@ package im.angry.openeuicc.core import im.angry.openeuicc.util.* import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking import net.typeblog.lpac_jni.ApduInterface import net.typeblog.lpac_jni.LocalProfileAssistant import net.typeblog.lpac_jni.impl.HttpInterfaceImpl @@ -30,7 +32,6 @@ class EuiccChannelImpl( ISDR_AID, apduInterface, HttpInterfaceImpl(verboseLoggingFlow, ignoreTLSCertificateFlow), - isdRAidFallback, ) override val atr: ByteArray? diff --git a/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt b/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt index b3f42b5..33514e3 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt @@ -42,10 +42,10 @@ class OmapiApduInterface( } override fun logicalChannelOpen(aid: ByteArray): Int { - val channel = session.openLogicalChannel(aid) - check(channel != null) { "Failed to open logical channel (${aid.encodeHex()})" } val index = channels.indexOf(null) check(index != -1) { "No free logical channel slots" } + val channel = session.openLogicalChannel(aid) + check(channel != null) { "Failed to open logical channel (${aid.encodeHex()})" } synchronized(channels) { channels[index] = channel } return index } diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt index 02b347a..fa9474f 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt @@ -12,7 +12,7 @@ internal object LpacJni { ): Long external fun destroyContext(handle: Long) - external fun euiccInit(handle: Long, aid: ByteArray?): Int + external fun euiccInit(handle: Long): Int external fun euiccSetMss(handle: Long, mss: Byte) external fun euiccFini(handle: Long) 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 9e819f7..3674f4f 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 @@ -1,9 +1,6 @@ package net.typeblog.lpac_jni.impl import android.util.Log -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.runBlocking import net.typeblog.lpac_jni.LpacJni import net.typeblog.lpac_jni.ApduInterface import net.typeblog.lpac_jni.EuiccInfo2 @@ -18,8 +15,7 @@ import net.typeblog.lpac_jni.Version class LocalProfileAssistantImpl( isdrAid: ByteArray, rawApduInterface: ApduInterface, - rawHttpInterface: HttpInterface, - isdRAidFallback: Flow>, + rawHttpInterface: HttpInterface ): LocalProfileAssistant { companion object { private const val TAG = "LocalProfileAssistantImpl" @@ -85,20 +81,8 @@ class LocalProfileAssistantImpl( private var contextHandle: Long = LpacJni.createContext(isdrAid, apduInterface, httpInterface) init { - val aids = runBlocking { isdRAidFallback.first() } - .map { it.toByteArray() } - .toTypedArray() - if (LpacJni.euiccInit(contextHandle, null) < 0) { - var errno = -1 - for (aid in aids) { - errno = LpacJni.euiccInit(contextHandle, aid) - if (errno == 0) { - break - } - } - if (errno < 0) { - throw IllegalArgumentException("Failed to initialize LPA") - } + if (LpacJni.euiccInit(contextHandle) < 0) { + throw IllegalArgumentException("Failed to initialize LPA") } val pkids = euiccInfo2?.euiccCiPKIdListForVerification ?: setOf() diff --git a/libs/lpac-jni/src/main/jni/lpac b/libs/lpac-jni/src/main/jni/lpac index a5a0516..90f7104 160000 --- a/libs/lpac-jni/src/main/jni/lpac +++ b/libs/lpac-jni/src/main/jni/lpac @@ -1 +1 @@ -Subproject commit a5a0516f084936e7e87cf7420fb99283fa3052ef +Subproject commit 90f7104847d4bb392b275746da20a55177a67573 diff --git a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c index d3b53a4..ca319db 100644 --- a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c +++ b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c @@ -78,18 +78,8 @@ Java_net_typeblog_lpac_1jni_LpacJni_destroyContext(JNIEnv *env, jobject thiz, jl } JNIEXPORT jint JNICALL -Java_net_typeblog_lpac_1jni_LpacJni_euiccInit(JNIEnv *env, jobject thiz, jlong handle, - jbyteArray aid) { +Java_net_typeblog_lpac_1jni_LpacJni_euiccInit(JNIEnv *env, jobject thiz, jlong handle) { struct euicc_ctx *ctx = (struct euicc_ctx *) handle; - if ((*env)->IsSameObject(env, aid, NULL)) { - ctx->aid = NULL; - } else { -// ctx->aid_len = (*env)->GetArrayLength(env, aid); -// ctx->aid = malloc(ctx->aid_len); -// jbyte *values = (*env)->GetByteArrayElements(env, aid, NULL); -// memcpy(&ctx->aid, values, ctx->aid_len); -// (*env)->ReleaseByteArrayElements(env, aid, values, JNI_ABORT); - } return euicc_init(ctx); } From 8e68d0e5c0ed55b5883c8833ed9b5518313e49a2 Mon Sep 17 00:00:00 2001 From: septs Date: Wed, 5 Mar 2025 09:20:04 +0800 Subject: [PATCH 8/9] refactor: EuiccChannelImpl --- .../angry/openeuicc/core/EuiccChannelImpl.kt | 44 +++++++++++++++---- 1 file changed, 35 insertions(+), 9 deletions(-) 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 ec93048..ee9ff06 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,5 +1,6 @@ package im.angry.openeuicc.core +import android.util.Log import im.angry.openeuicc.util.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first @@ -16,23 +17,27 @@ class EuiccChannelImpl( override val apduInterface: ApduInterface, verboseLoggingFlow: Flow, ignoreTLSCertificateFlow: Flow, - isdRAidFallback: Flow>, + private val isdRAidFallback: Flow>, ) : EuiccChannel { companion object { - // TODO: This needs to go somewhere else. - val ISDR_AID = "A0000005591010FFFFFFFF8900000100".decodeHex() + private const val TAG = "EuiccChannelImpl" + + // Specs: SGP.02 v4.3 (Section 2.2.3) + // https://www.gsma.com/esim/wp-content/uploads/2023/02/SGP.02-v4.3.pdf#page=27 + val STANDARD_ISDR_AID = "A0000005591010FFFFFFFF8900000100".decodeHex() } + private val isdRAid = findISDRAID() + override val slotId = port.card.physicalSlotIndex override val logicalSlotId = port.logicalSlotIndex override val portId = port.portIndex - override val lpa: LocalProfileAssistant = - LocalProfileAssistantImpl( - ISDR_AID, - apduInterface, - HttpInterfaceImpl(verboseLoggingFlow, ignoreTLSCertificateFlow), - ) + override val lpa: LocalProfileAssistant = LocalProfileAssistantImpl( + isdRAid, + apduInterface, + HttpInterfaceImpl(verboseLoggingFlow, ignoreTLSCertificateFlow), + ) override val atr: ByteArray? get() = (apduInterface as? ApduInterfaceAtrProvider)?.atr @@ -41,4 +46,25 @@ class EuiccChannelImpl( get() = lpa.valid override fun close() = lpa.close() + + private fun findISDRAID(): ByteArray { + try { + apduInterface.connect() + for (aid in runBlocking { isdRAidFallback.first() }) { + val channel = aid.decodeHex() + try { + Log.d(TAG, "Trying ISD-R AID: $aid") + apduInterface.withLogicalChannel(channel) { true } + Log.d(TAG, "Selected ISD-R AID: $aid") + return channel + } catch (e: Exception) { + Log.d(TAG, "Failed to select ISD-R AID: $aid") + continue + } + } + } finally { + apduInterface.disconnect() + } + return STANDARD_ISDR_AID + } } From 20b634e1be15cce81f7432e68c505d28cb7d8344 Mon Sep 17 00:00:00 2001 From: septs Date: Wed, 5 Mar 2025 09:21:09 +0800 Subject: [PATCH 9/9] feat: custom isd-r aids --- .../core/DefaultEuiccChannelFactory.kt | 2 + .../angry/openeuicc/core/EuiccChannelImpl.kt | 50 +++++++++++++++---- .../openeuicc/core/OmapiApduInterface.kt | 4 +- .../im/angry/openeuicc/ui/SettingsFragment.kt | 32 ++++++++++++ .../im/angry/openeuicc/util/InputFilters.kt | 10 ++++ .../angry/openeuicc/util/PreferenceUtils.kt | 3 ++ app-common/src/main/res/values/strings.xml | 2 + app-common/src/main/res/xml/pref_settings.xml | 7 +++ .../core/PrivilegedEuiccChannelFactory.kt | 1 + 9 files changed, 98 insertions(+), 13 deletions(-) create mode 100644 app-common/src/main/java/im/angry/openeuicc/util/InputFilters.kt 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 ea0bd60..6961a26 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 @@ -46,6 +46,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha ), context.preferenceRepository.verboseLoggingFlow, context.preferenceRepository.ignoreTLSCertificateFlow, + context.preferenceRepository.isdRAidFallbackFlow, ).also { Log.i(DefaultEuiccChannelManager.TAG, "Is OMAPI channel, setting MSS to 60") it.lpa.setEs10xMss(60) @@ -78,6 +79,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha ), context.preferenceRepository.verboseLoggingFlow, context.preferenceRepository.ignoreTLSCertificateFlow, + context.preferenceRepository.isdRAidFallbackFlow, ) } 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 a56b1cc..ee9ff06 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,10 @@ package im.angry.openeuicc.core -import im.angry.openeuicc.util.UiccPortInfoCompat -import im.angry.openeuicc.util.decodeHex +import android.util.Log +import im.angry.openeuicc.util.* import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking import net.typeblog.lpac_jni.ApduInterface import net.typeblog.lpac_jni.LocalProfileAssistant import net.typeblog.lpac_jni.impl.HttpInterfaceImpl @@ -14,23 +16,28 @@ class EuiccChannelImpl( override val intrinsicChannelName: String?, override val apduInterface: ApduInterface, verboseLoggingFlow: Flow, - ignoreTLSCertificateFlow: Flow + ignoreTLSCertificateFlow: Flow, + private val isdRAidFallback: Flow>, ) : EuiccChannel { companion object { - // TODO: This needs to go somewhere else. - val ISDR_AID = "A0000005591010FFFFFFFF8900000100".decodeHex() + private const val TAG = "EuiccChannelImpl" + + // Specs: SGP.02 v4.3 (Section 2.2.3) + // https://www.gsma.com/esim/wp-content/uploads/2023/02/SGP.02-v4.3.pdf#page=27 + val STANDARD_ISDR_AID = "A0000005591010FFFFFFFF8900000100".decodeHex() } + private val isdRAid = findISDRAID() + override val slotId = port.card.physicalSlotIndex override val logicalSlotId = port.logicalSlotIndex override val portId = port.portIndex - override val lpa: LocalProfileAssistant = - LocalProfileAssistantImpl( - ISDR_AID, - apduInterface, - HttpInterfaceImpl(verboseLoggingFlow, ignoreTLSCertificateFlow) - ) + override val lpa: LocalProfileAssistant = LocalProfileAssistantImpl( + isdRAid, + apduInterface, + HttpInterfaceImpl(verboseLoggingFlow, ignoreTLSCertificateFlow), + ) override val atr: ByteArray? get() = (apduInterface as? ApduInterfaceAtrProvider)?.atr @@ -39,4 +46,25 @@ class EuiccChannelImpl( get() = lpa.valid override fun close() = lpa.close() + + private fun findISDRAID(): ByteArray { + try { + apduInterface.connect() + for (aid in runBlocking { isdRAidFallback.first() }) { + val channel = aid.decodeHex() + try { + Log.d(TAG, "Trying ISD-R AID: $aid") + apduInterface.withLogicalChannel(channel) { true } + Log.d(TAG, "Selected ISD-R AID: $aid") + return channel + } catch (e: Exception) { + Log.d(TAG, "Failed to select ISD-R AID: $aid") + continue + } + } + } finally { + apduInterface.disconnect() + } + return STANDARD_ISDR_AID + } } diff --git a/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt b/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt index b3f42b5..33514e3 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt @@ -42,10 +42,10 @@ class OmapiApduInterface( } override fun logicalChannelOpen(aid: ByteArray): Int { - val channel = session.openLogicalChannel(aid) - check(channel != null) { "Failed to open logical channel (${aid.encodeHex()})" } val index = channels.indexOf(null) check(index != -1) { "No free logical channel slots" } + val channel = session.openLogicalChannel(aid) + check(channel != null) { "Failed to open logical channel (${aid.encodeHex()})" } synchronized(channels) { channels[index] = channel } return index } 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 fab680f..c420229 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.EditTextPreference import androidx.preference.Preference import androidx.preference.PreferenceCategory import androidx.preference.PreferenceFragmentCompat @@ -72,6 +73,9 @@ open class SettingsFragment: PreferenceFragmentCompat() { requirePreference("pref_advanced_verbose_logging") .bindBooleanFlow(preferenceRepository.verboseLoggingFlow) + requirePreference("pref_developer_custom_aids") + .let(::setCustomAIDs) + requirePreference("pref_developer_unfiltered_profile_list") .bindBooleanFlow(preferenceRepository.unfilteredProfileListFlow) @@ -79,6 +83,34 @@ open class SettingsFragment: PreferenceFragmentCompat() { .bindBooleanFlow(preferenceRepository.ignoreTLSCertificateFlow) } + private fun setCustomAIDs(preference: EditTextPreference) { + val flow = preferenceRepository.isdRAidFallbackFlow + val separator = "\n" + + fun prepare(text: String) = text.uppercase().lines() + .map(String::trim).toSet() + .filter { it.length % 2 == 0 && it.length in 10..32 && it.all(Char::isHex) } + .take(10) // limit to 10 AIDs + + lifecycleScope.launch { + flow.collect { preference.text = it.joinToString(separator) } + } + + preference.setOnBindEditTextListener { + it.filters += allowedInputFilter { c -> c.isHex() || c == '\n' } + it.setText(prepare(it.text.toString()).joinToString(separator)) + it.setSelection(it.text.length) + it.requestFocus() + } + + preference.setOnPreferenceChangeListener { _, newValue -> + runBlocking { + flow.updatePreference(prepare(newValue as String).toSet()) + } + true + } + } + protected fun requirePreference(key: CharSequence) = findPreference(key)!! diff --git a/app-common/src/main/java/im/angry/openeuicc/util/InputFilters.kt b/app-common/src/main/java/im/angry/openeuicc/util/InputFilters.kt new file mode 100644 index 0000000..2bb8866 --- /dev/null +++ b/app-common/src/main/java/im/angry/openeuicc/util/InputFilters.kt @@ -0,0 +1,10 @@ +package im.angry.openeuicc.util + +import android.text.InputFilter + +fun allowedInputFilter(predicate: (Char) -> Boolean) = + InputFilter { source, start, end, _, _, _ -> + source.substring(start until end).filter(predicate) + } + +fun Char.isHex() = isDigit() || uppercaseChar() in 'A'..'F' \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt index f5e3ca2..5478945 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.stringSetPreferencesKey import androidx.datastore.preferences.preferencesDataStore import androidx.fragment.app.Fragment import im.angry.openeuicc.OpenEuiccApplication @@ -31,6 +32,7 @@ internal object PreferenceKeys { // ---- Developer Options ---- val DEVELOPER_OPTIONS_ENABLED = booleanPreferencesKey("developer_options_enabled") + val ISD_R_AID_FALLBACK = stringSetPreferencesKey("isd_r_aid_fallback") val UNFILTERED_PROFILE_LIST = booleanPreferencesKey("unfiltered_profile_list") val IGNORE_TLS_CERTIFICATE = booleanPreferencesKey("ignore_tls_certificate") } @@ -48,6 +50,7 @@ class PreferenceRepository(private val context: Context) { // ---- Developer Options ---- val developerOptionsEnabledFlow = bindFlow(PreferenceKeys.DEVELOPER_OPTIONS_ENABLED, false) + val isdRAidFallbackFlow = bindFlow(PreferenceKeys.ISD_R_AID_FALLBACK, emptySet()) val unfilteredProfileListFlow = bindFlow(PreferenceKeys.UNFILTERED_PROFILE_LIST, false) val ignoreTLSCertificateFlow = bindFlow(PreferenceKeys.IGNORE_TLS_CERTIFICATE, false) diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index a45ce1f..b1b58d6 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -171,6 +171,8 @@ Logs View recent debug logs of the application Developer Options + ISD-R AIDs Fallback + Prefer to use standard ISD-R AID access, one ISD-R AID per line (hexadecimal string) Show unfiltered profile list Include non-production profiles in the list Ignore SM-DP+ TLS certificate diff --git a/app-common/src/main/res/xml/pref_settings.xml b/app-common/src/main/res/xml/pref_settings.xml index bb5bd50..e6a75f4 100644 --- a/app-common/src/main/res/xml/pref_settings.xml +++ b/app-common/src/main/res/xml/pref_settings.xml @@ -57,6 +57,13 @@ app:title="@string/pref_developer" app:iconSpaceReserved="false"> + +