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" /> + +