diff --git a/app-common/src/main/AndroidManifest.xml b/app-common/src/main/AndroidManifest.xml
index 464ae0f..b0324dc 100644
--- a/app-common/src/main/AndroidManifest.xml
+++ b/app-common/src/main/AndroidManifest.xml
@@ -28,6 +28,10 @@
android:name="im.angry.openeuicc.ui.LogsActivity"
android:label="@string/pref_advanced_logs" />
+
+
get() = (0.. EuiccChannel?): EuiccChannel? {
+ val isdrAidList =
+ parseIsdrAidList(appContainer.preferenceRepository.isdrAidListFlow.first())
+
+ return isdrAidList.firstNotNullOfOrNull {
+ Log.i(TAG, "Opening channel, trying ISDR AID ${it.encodeHex()}")
+
+ openFn(it)?.let { channel ->
+ if (channel.valid) {
+ channel
+ } else {
+ channel.close()
+ null
+ }
+ }
+ }
+ }
+
private suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel? {
lock.withLock {
if (port.card.physicalSlotIndex == EuiccChannelManager.USB_CHANNEL_ID) {
@@ -76,9 +95,10 @@ open class DefaultEuiccChannelManager(
return null
}
- val channel = euiccChannelFactory.tryOpenEuiccChannel(port) ?: return null
+ val channel =
+ tryOpenChannelFirstValidAid { euiccChannelFactory.tryOpenEuiccChannel(port, it) }
- if (channel.valid) {
+ if (channel != null) {
channelCache.add(channel)
return channel
} else {
@@ -86,7 +106,6 @@ open class DefaultEuiccChannelManager(
TAG,
"Was able to open channel for logical slot ${port.logicalSlotIndex}, but the channel is invalid (cannot get eID or profiles without errors). This slot might be broken, aborting."
)
- channel.close()
return null
}
}
@@ -212,7 +231,10 @@ open class DefaultEuiccChannelManager(
check(channel.valid) { "Invalid channel" }
break
} catch (e: Exception) {
- Log.d(TAG, "Slot $physicalSlotId port $portId reconnect failure, retrying in 1000 ms")
+ Log.d(
+ TAG,
+ "Slot $physicalSlotId port $portId reconnect failure, retrying in 1000 ms"
+ )
}
delay(1000)
}
@@ -249,9 +271,18 @@ open class DefaultEuiccChannelManager(
// If we don't have permission, tell UI code that we found a candidate device, but we
// need permission to be able to do anything with it
if (!usbManager.hasPermission(device)) return@withContext Pair(device, false)
- Log.i(TAG, "Found CCID interface on ${device.deviceId}:${device.vendorId}, and has permission; trying to open channel")
+ Log.i(
+ TAG,
+ "Found CCID interface on ${device.deviceId}:${device.vendorId}, and has permission; trying to open channel"
+ )
try {
- val channel = euiccChannelFactory.tryOpenUsbEuiccChannel(device, iface)
+ val channel = tryOpenChannelFirstValidAid {
+ euiccChannelFactory.tryOpenUsbEuiccChannel(
+ device,
+ iface,
+ it
+ )
+ }
if (channel != null && channel.lpa.valid) {
usbChannel = channel
return@withContext Pair(device, true)
@@ -260,7 +291,10 @@ open class DefaultEuiccChannelManager(
// Ignored -- skip forward
e.printStackTrace()
}
- Log.i(TAG, "No valid eUICC channel found on USB device ${device.deviceId}:${device.vendorId}")
+ Log.i(
+ TAG,
+ "No valid eUICC channel found on USB device ${device.deviceId}:${device.vendorId}"
+ )
}
return@withContext Pair(null, false)
}
diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelFactory.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelFactory.kt
index fb5d95d..87f5885 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
@@ -7,9 +7,13 @@ 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): EuiccChannel?
+ suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat, isdrAid: ByteArray): EuiccChannel?
- fun tryOpenUsbEuiccChannel(usbDevice: UsbDevice, usbInterface: UsbInterface): EuiccChannel?
+ fun tryOpenUsbEuiccChannel(
+ usbDevice: UsbDevice,
+ usbInterface: UsbInterface,
+ isdrAid: ByteArray
+ ): EuiccChannel?
/**
* Release all resources used by this EuiccChannelFactory
diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt
index a56b1cc..ed8797a 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,21 +13,17 @@ class EuiccChannelImpl(
override val port: UiccPortInfoCompat,
override val intrinsicChannelName: String?,
override val apduInterface: ApduInterface,
+ isdrAid: ByteArray,
verboseLoggingFlow: Flow,
ignoreTLSCertificateFlow: Flow
) : EuiccChannel {
- companion object {
- // TODO: This needs to go somewhere else.
- val ISDR_AID = "A0000005591010FFFFFFFF8900000100".decodeHex()
- }
-
override val slotId = port.card.physicalSlotIndex
override val logicalSlotId = port.logicalSlotIndex
override val portId = port.portIndex
override val lpa: LocalProfileAssistant =
LocalProfileAssistantImpl(
- ISDR_AID,
+ isdrAid,
apduInterface,
HttpInterfaceImpl(verboseLoggingFlow, ignoreTLSCertificateFlow)
)
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/IsdrAidListActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/IsdrAidListActivity.kt
new file mode 100644
index 0000000..6553421
--- /dev/null
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/IsdrAidListActivity.kt
@@ -0,0 +1,67 @@
+package im.angry.openeuicc.ui
+
+import android.os.Bundle
+import android.text.Editable
+import android.view.Menu
+import android.view.MenuItem
+import android.widget.EditText
+import android.widget.Toast
+import androidx.activity.enableEdgeToEdge
+import androidx.appcompat.app.AppCompatActivity
+import androidx.lifecycle.lifecycleScope
+import im.angry.openeuicc.common.R
+import im.angry.openeuicc.util.preferenceRepository
+import im.angry.openeuicc.util.setupToolbarInsets
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+
+class IsdrAidListActivity : AppCompatActivity() {
+ private lateinit var isdrAidListEditor: EditText
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ enableEdgeToEdge()
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_isdr_aid_list)
+ setSupportActionBar(requireViewById(R.id.toolbar))
+ setupToolbarInsets()
+ supportActionBar!!.setDisplayHomeAsUpEnabled(true)
+
+ isdrAidListEditor = requireViewById(R.id.isdr_aid_list_editor)
+
+ lifecycleScope.launch {
+ preferenceRepository.isdrAidListFlow.onEach {
+ isdrAidListEditor.text = Editable.Factory.getInstance().newEditable(it)
+ }.collect()
+ }
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+ menuInflater.inflate(R.menu.activity_isdr_aid_list, menu)
+ return true
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean =
+ when (item.itemId) {
+ R.id.save -> {
+ lifecycleScope.launch {
+ preferenceRepository.isdrAidListFlow.updatePreference(isdrAidListEditor.text.toString())
+ Toast.makeText(
+ this@IsdrAidListActivity,
+ R.string.isdr_aid_list_saved,
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ true
+ }
+
+ R.id.reset -> {
+ lifecycleScope.launch {
+ preferenceRepository.isdrAidListFlow.removePreference()
+ }
+ true
+ }
+
+ else -> super.onOptionsItemSelected(item)
+ }
+}
\ No newline at end of file
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 d137e90..6554142 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
@@ -83,6 +83,10 @@ open class SettingsFragment: PreferenceFragmentCompat() {
requirePreference("pref_developer_euicc_memory_reset")
.bindBooleanFlow(preferenceRepository.euiccMemoryResetFlow)
+
+ requirePreference("pref_developer_isdr_aid_list").apply {
+ intent = Intent(requireContext(), IsdrAidListActivity::class.java)
+ }
}
protected fun requirePreference(key: CharSequence) =
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 c69c5e4..928079f 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,11 +5,13 @@ 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.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import androidx.fragment.app.Fragment
import im.angry.openeuicc.OpenEuiccApplication
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
+import java.util.Base64
private val Context.dataStore: DataStore by preferencesDataStore(name = "prefs")
@@ -35,6 +37,20 @@ internal object PreferenceKeys {
val UNFILTERED_PROFILE_LIST = booleanPreferencesKey("unfiltered_profile_list")
val IGNORE_TLS_CERTIFICATE = booleanPreferencesKey("ignore_tls_certificate")
val EUICC_MEMORY_RESET = booleanPreferencesKey("euicc_memory_reset")
+ val ISDR_AID_LIST = stringPreferencesKey("isdr_aid_list")
+}
+
+const val EUICC_DEFAULT_ISDR_AID = "A0000005591010FFFFFFFF8900000100"
+
+internal object PreferenceConstants {
+ val DEFAULT_AID_LIST = """
+ # One AID per line. Comment lines start with #.
+ # eUICC standard
+ $EUICC_DEFAULT_ISDR_AID
+
+ # 5ber
+ A0000005591010FFFFFFFF8900050500
+ """.trimIndent()
}
open class PreferenceRepository(private val context: Context) {
@@ -54,20 +70,46 @@ open class PreferenceRepository(private val context: Context) {
val unfilteredProfileListFlow = bindFlow(PreferenceKeys.UNFILTERED_PROFILE_LIST, false)
val ignoreTLSCertificateFlow = bindFlow(PreferenceKeys.IGNORE_TLS_CERTIFICATE, false)
val euiccMemoryResetFlow = bindFlow(PreferenceKeys.EUICC_MEMORY_RESET, false)
+ val isdrAidListFlow = bindFlow(
+ PreferenceKeys.ISDR_AID_LIST,
+ PreferenceConstants.DEFAULT_AID_LIST,
+ { Base64.getEncoder().encodeToString(it.encodeToByteArray()) },
+ { Base64.getDecoder().decode(it).decodeToString() })
- protected fun bindFlow(key: Preferences.Key, defaultValue: T): PreferenceFlowWrapper =
- PreferenceFlowWrapper(context, key, defaultValue)
+ protected fun bindFlow(
+ key: Preferences.Key,
+ defaultValue: T,
+ encoder: (T) -> T = { it },
+ decoder: (T) -> T = { it }
+ ): PreferenceFlowWrapper =
+ PreferenceFlowWrapper(context, key, defaultValue, encoder, decoder)
}
class PreferenceFlowWrapper private constructor(
private val context: Context,
private val key: Preferences.Key,
inner: Flow,
+ private val encoder: (T) -> T,
) : Flow by inner {
- internal constructor(context: Context, key: Preferences.Key, defaultValue: T) :
- this(context, key, context.dataStore.data.map { it[key] ?: defaultValue })
+ internal constructor(
+ context: Context,
+ key: Preferences.Key,
+ defaultValue: T,
+ encoder: (T) -> T,
+ decoder: (T) -> T
+ ) :
+ this(
+ context,
+ key,
+ context.dataStore.data.map { it[key]?.let(decoder) ?: defaultValue },
+ encoder
+ )
suspend fun updatePreference(value: T) {
- context.dataStore.edit { it[key] = value }
+ context.dataStore.edit { it[key] = encoder(value) }
+ }
+
+ suspend fun removePreference() {
+ context.dataStore.edit { it.remove(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 8d72462..9f993a3 100644
--- a/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt
@@ -1,7 +1,7 @@
package im.angry.openeuicc.util
fun String.decodeHex(): ByteArray {
- check(length % 2 == 0) { "Must have an even length" }
+ require(length % 2 == 0) { "Must have an even length" }
val decodedLength = length / 2
val out = ByteArray(decodedLength)
@@ -29,6 +29,22 @@ fun formatFreeSpace(size: Int): String =
"$size B"
}
+/**
+ * Decode a list of potential ISDR AIDs, one per line. Lines starting with '#' are ignored.
+ * If none is found, at least EUICC_DEFAULT_ISDR_AID is returned
+ */
+fun parseIsdrAidList(s: String): List =
+ s.split('\n').map(String::trim).filter { !it.startsWith('#') }
+ .map(String::trim)
+ .mapNotNull {
+ try {
+ it.decodeHex()
+ } catch (_: IllegalArgumentException) {
+ null
+ }
+ }
+ .ifEmpty { listOf(EUICC_DEFAULT_ISDR_AID.decodeHex()) }
+
fun String.prettyPrintJson(): String {
val ret = StringBuilder()
var inQuotes = false
diff --git a/app-common/src/main/res/layout/activity_isdr_aid_list.xml b/app-common/src/main/res/layout/activity_isdr_aid_list.xml
new file mode 100644
index 0000000..06a75a1
--- /dev/null
+++ b/app-common/src/main/res/layout/activity_isdr_aid_list.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
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
new file mode 100644
index 0000000..32f178a
--- /dev/null
+++ b/app-common/src/main/res/menu/activity_isdr_aid_list.xml
@@ -0,0 +1,15 @@
+
+
\ 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 35fd400..d51e2c7 100644
--- a/app-common/src/main/res/values-ja/strings.xml
+++ b/app-common/src/main/res/values-ja/strings.xml
@@ -124,6 +124,7 @@
%s のログ
開発者になるまであと %d ステップです。
あなたは開発者になりました!
+ カスタム ISD-R AID リストが保存されました
設定
通知
eSIM のプロファイル操作により、通信事業者に通知が送信されます。必要に応じてこの動作を微調整できます。
@@ -148,6 +149,7 @@
非運用のプロファイルも含めます
SM-DP+ TLS 証明書を無視する
RSP サーバーで使用される TLS 証明書を受け入れます
+ 一部のブランドの取り外し可能な eUICC では、独自の非標準 ISD-R AID が使用されている場合があり、サードパーティ アプリからアクセスできなくなります。アプリはこのリストに追加された非標準の AID の使用を試みる可能性がありますが、動作することは保証されません。
情報
アプリバージョン
ソースコード
@@ -164,4 +166,7 @@
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 2e069bd..32ced90 100644
--- a/app-common/src/main/res/values-zh-rCN/strings.xml
+++ b/app-common/src/main/res/values-zh-rCN/strings.xml
@@ -65,6 +65,7 @@
删除
保存日志
%s 的日志
+ 自定义 ISD-R AID 列表已保存
设置
通知
操作 eSIM 配置文件会向运营商发送通知。根据需要在此处微调此行为。
@@ -81,6 +82,7 @@
详细日志中包含敏感信息,开启此功能后请仅与你信任的人共享你的日志。
日志
查看应用程序的最新调试日志
+ 某些品牌的可移除 eUICC 可能会使用自己的非标准 ISD-R AID,导致第三方应用无法访问。此 App 可以尝试使用此列表中添加的非标准 AID,但不能保证它们一定有效。
信息
App 版本
源码
@@ -164,4 +166,7 @@
允许擦除 eUICC
此操作是默认隐藏的危险操作。作为替代方案,您可以手动删除所有配置文件。
向基带发送刷新命令
+ 自定义 ISD-R AID 列表
+ 重置
+ ISD-R AID 列表
\ No newline at end of file
diff --git a/app-common/src/main/res/values-zh-rTW/strings.xml b/app-common/src/main/res/values-zh-rTW/strings.xml
index 729893c..5136bf7 100644
--- a/app-common/src/main/res/values-zh-rTW/strings.xml
+++ b/app-common/src/main/res/values-zh-rTW/strings.xml
@@ -65,6 +65,7 @@
刪除
儲存日誌
%s 的日誌
+ 自訂 ISD-R AID 列表已儲存
設定
通知
變更 eSIM 設定檔會向電信業者傳送通知。根據需要在此處微調此行為。
@@ -81,6 +82,7 @@
進階
允許 停用/刪除 已啟用的設定檔
預設情況下,此應用程式會阻止您停用可插拔 eSIM 中已啟用的設定檔。\n因為這樣做 有時 會導致無法存取。\n勾選此框以 移除 此保護措施。
+ 某些品牌的可移除 eUICC 可能會使用自己的非標準 ISD-R AID,導致第三方應用程式無法存取。此 App 可以嘗試使用此清單中新增的非標準 AID,但不能保證它們一定有效。
資訊
App 版本
原始碼
@@ -164,4 +166,7 @@
允許擦除 eUICC
此操作是預設隱藏的危險操作。作為替代方案,您可以手動刪除所有設定檔。
向基帶發送刷新命令
+ 自訂 ISD-R AID 列表
+ 重置
+ ISD-R AID 列表
\ No newline at end of file
diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml
index a366b88..5e4e4da 100644
--- a/app-common/src/main/res/values/strings.xml
+++ b/app-common/src/main/res/values/strings.xml
@@ -162,6 +162,11 @@
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.
+
Settings
Notifications
eSIM profile operations send notifications to the carrier. Fine-tune this behavior as needed here.
@@ -189,6 +194,8 @@
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.
+ Customize ISD-R AID list
+ Some brands of removable eUICCs may use their own non-standard ISD-R AID, rendering them inaccessible to third-party apps. We can attempt to use non-standard AIDs added in this list, but there is no guarantee that they will work.
Info
App Version
Source Code
diff --git a/app-common/src/main/res/xml/pref_settings.xml b/app-common/src/main/res/xml/pref_settings.xml
index ce700f5..690a120 100644
--- a/app-common/src/main/res/xml/pref_settings.xml
+++ b/app-common/src/main/res/xml/pref_settings.xml
@@ -81,6 +81,12 @@
app:summary="@string/pref_developer_euicc_memory_reset_desc"
app:title="@string/pref_developer_euicc_memory_reset" />
+
+