ソースを参照

feat: Customizable ISD-R AID list

This is stored base64-encoded in shared preferences (to avoid XML
encoding issues).

By default we have the standard AID plus the 5ber one. We may add more
going forward.
Peter Cai 10 ヶ月 前
コミット
c6963feb17

+ 4 - 0
app-common/src/main/AndroidManifest.xml

@@ -28,6 +28,10 @@
             android:name="im.angry.openeuicc.ui.LogsActivity"
             android:label="@string/pref_advanced_logs" />
 
+        <activity
+            android:name="im.angry.openeuicc.ui.IsdrAidListActivity"
+            android:label="@string/isdr_aid_list" />
+
         <activity
             android:exported="true"
             android:name="im.angry.openeuicc.ui.wizard.DownloadWizardActivity"

+ 19 - 4
app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt

@@ -26,14 +26,23 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
         }
     }
 
-    override suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel? {
+    override suspend fun tryOpenEuiccChannel(
+        port: UiccPortInfoCompat,
+        isdrAid: ByteArray
+    ): EuiccChannel? {
         if (port.portIndex != 0) {
-            Log.w(DefaultEuiccChannelManager.TAG, "OMAPI channel attempted on non-zero portId, this may or may not work.")
+            Log.w(
+                DefaultEuiccChannelManager.TAG,
+                "OMAPI channel attempted on non-zero portId, this may or may not work."
+            )
         }
 
         ensureSEService()
 
-        Log.i(DefaultEuiccChannelManager.TAG, "Trying OMAPI for physical slot ${port.card.physicalSlotIndex}")
+        Log.i(
+            DefaultEuiccChannelManager.TAG,
+            "Trying OMAPI for physical slot ${port.card.physicalSlotIndex}"
+        )
         try {
             return EuiccChannelImpl(
                 context.getString(R.string.omapi),
@@ -44,6 +53,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
                     port,
                     context.preferenceRepository.verboseLoggingFlow
                 ),
+                isdrAid,
                 context.preferenceRepository.verboseLoggingFlow,
                 context.preferenceRepository.ignoreTLSCertificateFlow,
             ).also {
@@ -61,7 +71,11 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
         return null
     }
 
-    override fun tryOpenUsbEuiccChannel(usbDevice: UsbDevice, usbInterface: UsbInterface): EuiccChannel? {
+    override fun tryOpenUsbEuiccChannel(
+        usbDevice: UsbDevice,
+        usbInterface: UsbInterface,
+        isdrAid: ByteArray
+    ): EuiccChannel? {
         val (bulkIn, bulkOut) = usbInterface.endpoints.bulkPair
         if (bulkIn == null || bulkOut == null) return null
         val conn = usbManager.openDevice(usbDevice) ?: return null
@@ -76,6 +90,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
                 bulkOut,
                 context.preferenceRepository.verboseLoggingFlow
             ),
+            isdrAid,
             context.preferenceRepository.verboseLoggingFlow,
             context.preferenceRepository.ignoreTLSCertificateFlow,
         )

+ 41 - 7
app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt

@@ -12,6 +12,7 @@ import im.angry.openeuicc.util.*
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.merge
@@ -49,6 +50,24 @@ open class DefaultEuiccChannelManager(
     protected open val uiccCards: Collection<UiccCardInfoCompat>
         get() = (0..<tm.activeModemCountCompat).map { FakeUiccCardInfoCompat(it) }
 
+    private suspend inline fun tryOpenChannelFirstValidAid(openFn: (ByteArray) -> 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)
         }

+ 6 - 2
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

+ 2 - 6
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<Boolean>,
     ignoreTLSCertificateFlow: Flow<Boolean>
 ) : 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)
         )

+ 67 - 0
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)
+        }
+}

+ 4 - 0
app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt

@@ -83,6 +83,10 @@ open class SettingsFragment: PreferenceFragmentCompat() {
 
         requirePreference<CheckBoxPreference>("pref_developer_euicc_memory_reset")
             .bindBooleanFlow(preferenceRepository.euiccMemoryResetFlow)
+
+        requirePreference<Preference>("pref_developer_isdr_aid_list").apply {
+            intent = Intent(requireContext(), IsdrAidListActivity::class.java)
+        }
     }
 
     protected fun <T : Preference> requirePreference(key: CharSequence) =

+ 47 - 5
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<Preferences> 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 <T> bindFlow(key: Preferences.Key<T>, defaultValue: T): PreferenceFlowWrapper<T> =
-        PreferenceFlowWrapper(context, key, defaultValue)
+    protected fun <T> bindFlow(
+        key: Preferences.Key<T>,
+        defaultValue: T,
+        encoder: (T) -> T = { it },
+        decoder: (T) -> T = { it }
+    ): PreferenceFlowWrapper<T> =
+        PreferenceFlowWrapper(context, key, defaultValue, encoder, decoder)
 }
 
 class PreferenceFlowWrapper<T> private constructor(
     private val context: Context,
     private val key: Preferences.Key<T>,
     inner: Flow<T>,
+    private val encoder: (T) -> T,
 ) : Flow<T> by inner {
-    internal constructor(context: Context, key: Preferences.Key<T>, defaultValue: T) :
-            this(context, key, context.dataStore.data.map { it[key] ?: defaultValue })
+    internal constructor(
+        context: Context,
+        key: Preferences.Key<T>,
+        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) }
     }
 }

+ 17 - 1
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<ByteArray> =
+    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

+ 23 - 0
app-common/src/main/res/layout/activity_isdr_aid_list.xml

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <include layout="@layout/toolbar_activity" />
+
+    <EditText
+        android:id="@+id/isdr_aid_list_editor"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:importantForAutofill="no"
+        android:inputType="textMultiLine"
+        android:gravity="top|start"
+        app:layout_constraintTop_toBottomOf="@id/toolbar"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        tools:ignore="LabelFor" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 15 - 0
app-common/src/main/res/menu/activity_isdr_aid_list.xml

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+    <item
+        android:id="@+id/save"
+        android:icon="@drawable/ic_save_as_black"
+        android:title="@string/logs_save"
+        app:showAsAction="always" />
+
+    <item
+        android:id="@+id/reset"
+        android:title="@string/reset"
+        android:icon="@drawable/ic_refresh_black"
+        app:showAsAction="ifRoom" />
+</menu>

+ 5 - 0
app-common/src/main/res/values-ja/strings.xml

@@ -124,6 +124,7 @@
     <string name="logs_filename_template">%s のログ</string>
     <string name="developer_options_steps">開発者になるまであと %d ステップです。</string>
     <string name="developer_options_enabled">あなたは開発者になりました!</string>
+    <string name="isdr_aid_list_saved">カスタム ISD-R AID リストが保存されました</string>
     <string name="pref_settings">設定</string>
     <string name="pref_notifications">通知</string>
     <string name="pref_notifications_desc">eSIM のプロファイル操作により、通信事業者に通知が送信されます。必要に応じてこの動作を微調整できます。</string>
@@ -148,6 +149,7 @@
     <string name="pref_developer_unfiltered_profile_list_desc">非運用のプロファイルも含めます</string>
     <string name="pref_developer_ignore_tls_certificate">SM-DP+ TLS 証明書を無視する</string>
     <string name="pref_developer_ignore_tls_certificate_desc">RSP サーバーで使用される TLS 証明書を受け入れます</string>
+    <string name="pref_developer_isdr_aid_list_desc">一部のブランドの取り外し可能な eUICC では、独自の非標準 ISD-R AID が使用されている場合があり、サードパーティ アプリからアクセスできなくなります。アプリはこのリストに追加された非標準の AID の使用を試みる可能性がありますが、動作することは保証されません。</string>
     <string name="pref_info">情報</string>
     <string name="pref_info_app_version">アプリバージョン</string>
     <string name="pref_info_source_code">ソースコード</string>
@@ -164,4 +166,7 @@
     <string name="pref_developer_euicc_memory_reset">eUICC の消去を可能にする</string>
     <string name="pref_developer_euicc_memory_reset_desc">この操作は、デフォルトでは非表示になっている危険な操作です。代わりに、すべての構成ファイルを手動で削除することもできます。</string>
     <string name="pref_developer_refresh_after_switch">モデムに更新コマンドを送信</string>
+    <string name="pref_developer_isdr_aid_list">ISD-R AID リストのカスタマイズ</string>
+    <string name="reset">リセット</string>
+    <string name="isdr_aid_list">ISD-R AID リスト</string>
 </resources>

+ 5 - 0
app-common/src/main/res/values-zh-rCN/strings.xml

@@ -65,6 +65,7 @@
     <string name="profile_notification_delete">删除</string>
     <string name="logs_save">保存日志</string>
     <string name="logs_filename_template">%s 的日志</string>
+    <string name="isdr_aid_list_saved">自定义 ISD-R AID 列表已保存</string>
     <string name="pref_settings">设置</string>
     <string name="pref_notifications">通知</string>
     <string name="pref_notifications_desc">操作 eSIM 配置文件会向运营商发送通知。根据需要在此处微调此行为。</string>
@@ -81,6 +82,7 @@
     <string name="pref_advanced_verbose_logging_desc">详细日志中包含敏感信息,开启此功能后请仅与你信任的人共享你的日志。</string>
     <string name="pref_advanced_logs">日志</string>
     <string name="pref_advanced_logs_desc">查看应用程序的最新调试日志</string>
+    <string name="pref_developer_isdr_aid_list_desc">某些品牌的可移除 eUICC 可能会使用自己的非标准 ISD-R AID,导致第三方应用无法访问。此 App 可以尝试使用此列表中添加的非标准 AID,但不能保证它们一定有效。</string>
     <string name="pref_info">信息</string>
     <string name="pref_info_app_version">App 版本</string>
     <string name="pref_info_source_code">源码</string>
@@ -164,4 +166,7 @@
     <string name="pref_developer_euicc_memory_reset">允许擦除 eUICC</string>
     <string name="pref_developer_euicc_memory_reset_desc">此操作是默认隐藏的危险操作。作为替代方案,您可以手动删除所有配置文件。</string>
     <string name="pref_developer_refresh_after_switch">向基带发送刷新命令</string>
+    <string name="pref_developer_isdr_aid_list">自定义 ISD-R AID 列表</string>
+    <string name="reset">重置</string>
+    <string name="isdr_aid_list">ISD-R AID 列表</string>
 </resources>

+ 5 - 0
app-common/src/main/res/values-zh-rTW/strings.xml

@@ -65,6 +65,7 @@
     <string name="profile_notification_delete">刪除</string>
     <string name="logs_save">儲存日誌</string>
     <string name="logs_filename_template">%s 的日誌</string>
+    <string name="isdr_aid_list_saved">自訂 ISD-R AID 列表已儲存</string>
     <string name="pref_settings">設定</string>
     <string name="pref_notifications">通知</string>
     <string name="pref_notifications_desc">變更 eSIM 設定檔會向電信業者傳送通知。根據需要在此處微調此行為。</string>
@@ -81,6 +82,7 @@
     <string name="pref_advanced">進階</string>
     <string name="pref_advanced_disable_safeguard_removable_esim">允許 停用/刪除 已啟用的設定檔</string>
     <string name="pref_advanced_disable_safeguard_removable_esim_desc">預設情況下,此應用程式會阻止您停用可插拔 eSIM 中已啟用的設定檔。\n因為這樣做 <i>有時</i> 會導致無法存取。\n勾選此框以 <i>移除</i> 此保護措施。</string>
+    <string name="pref_developer_isdr_aid_list_desc">某些品牌的可移除 eUICC 可能會使用自己的非標準 ISD-R AID,導致第三方應用程式無法存取。此 App 可以嘗試使用此清單中新增的非標準 AID,但不能保證它們一定有效。</string>
     <string name="pref_info">資訊</string>
     <string name="pref_info_app_version">App 版本</string>
     <string name="pref_info_source_code">原始碼</string>
@@ -164,4 +166,7 @@
     <string name="pref_developer_euicc_memory_reset">允許擦除 eUICC</string>
     <string name="pref_developer_euicc_memory_reset_desc">此操作是預設隱藏的危險操作。作為替代方案,您可以手動刪除所有設定檔。</string>
     <string name="pref_developer_refresh_after_switch">向基帶發送刷新命令</string>
+    <string name="pref_developer_isdr_aid_list">自訂 ISD-R AID 列表</string>
+    <string name="reset">重置</string>
+    <string name="isdr_aid_list">ISD-R AID 列表</string>
 </resources>

+ 7 - 0
app-common/src/main/res/values/strings.xml

@@ -162,6 +162,11 @@
     <string name="developer_options_steps">You are %d steps away from being a developer.</string>
     <string name="developer_options_enabled">You are now a developer!</string>
 
+    <string name="reset">Reset</string>
+
+    <string name="isdr_aid_list">ISD-R AID List</string>
+    <string name="isdr_aid_list_saved">Saved custom ISD-R AID list.</string>
+
     <string name="pref_settings">Settings</string>
     <string name="pref_notifications">Notifications</string>
     <string name="pref_notifications_desc">eSIM profile operations send notifications to the carrier. Fine-tune this behavior as needed here.</string>
@@ -189,6 +194,8 @@
     <string name="pref_developer_ignore_tls_certificate_desc">Accept any TLS certificate used by the RSP server</string>
     <string name="pref_developer_euicc_memory_reset">Allow erasing eUICC</string>
     <string name="pref_developer_euicc_memory_reset_desc">This is a dangerous operation and hidden by default. As an alternative, you can delete all profiles manually.</string>
+    <string name="pref_developer_isdr_aid_list">Customize ISD-R AID list</string>
+    <string name="pref_developer_isdr_aid_list_desc">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.</string>
     <string name="pref_info">Info</string>
     <string name="pref_info_app_version">App Version</string>
     <string name="pref_info_source_code">Source Code</string>

+ 6 - 0
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" />
 
+        <Preference
+            app:iconSpaceReserved="false"
+            app:key="pref_developer_isdr_aid_list"
+            app:title="@string/pref_developer_isdr_aid_list"
+            app:summary="@string/pref_developer_isdr_aid_list_desc" />
+
     </im.angry.openeuicc.ui.preference.LongSummaryPreferenceCategory>
 
     <PreferenceCategory

+ 7 - 3
app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt

@@ -14,12 +14,15 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto
         get() = context
 
     @Suppress("NAME_SHADOWING")
-    override suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel? {
+    override suspend fun tryOpenEuiccChannel(
+        port: UiccPortInfoCompat,
+        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)?.let { return it }
+            super.tryOpenEuiccChannel(port, isdrAid)?.let { return it }
         }
 
         if (port.card.isEuicc || preferenceRepository.removableTelephonyManagerFlow.first()) {
@@ -37,6 +40,7 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto
                         telephonyManager,
                         context.preferenceRepository.verboseLoggingFlow
                     ),
+                    isdrAid,
                     context.preferenceRepository.verboseLoggingFlow,
                     context.preferenceRepository.ignoreTLSCertificateFlow,
                 )
@@ -49,6 +53,6 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto
             }
         }
 
-        return super.tryOpenEuiccChannel(port)
+        return super.tryOpenEuiccChannel(port, isdrAid)
     }
 }