diff --git a/app-common/src/main/AndroidManifest.xml b/app-common/src/main/AndroidManifest.xml
index 11f16a6..a33838f 100644
--- a/app-common/src/main/AndroidManifest.xml
+++ b/app-common/src/main/AndroidManifest.xml
@@ -32,6 +32,10 @@
android:name="im.angry.openeuicc.ui.LogsActivity"
android:label="@string/pref_advanced_logs" />
+
+
+ verboseLoggingFlow: Flow,
+ ignoreTLSCertificate: 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))
+ LocalProfileAssistantImpl(apduInterface, HttpInterfaceImpl(verboseLoggingFlow, ignoreTLSCertificate))
override val valid: Boolean
get() = lpa.valid
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt
index bf46043..c3ecbf3 100644
--- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt
@@ -31,10 +31,12 @@ import net.typeblog.lpac_jni.LocalProfileInfo
import im.angry.openeuicc.common.R
import im.angry.openeuicc.service.EuiccChannelManagerService
import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
+import im.angry.openeuicc.ui.wizard.DownloadWizardActivity
import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -105,8 +107,14 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false)
fab.setOnClickListener {
- ProfileDownloadFragment.newInstance(slotId, portId)
- .show(childFragmentManager, ProfileDownloadFragment.TAG)
+ lifecycleScope.launch {
+ if (preferenceRepository.experimentalDownloadWizardFlow.first()) {
+ startActivity(Intent(requireContext(), DownloadWizardActivity::class.java))
+ } else {
+ ProfileDownloadFragment.newInstance(slotId, portId)
+ .show(childFragmentManager, ProfileDownloadFragment.TAG)
+ }
+ }
}
}
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 5ed4348..83be1ea 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
@@ -3,26 +3,48 @@ package im.angry.openeuicc.ui
import android.content.Intent
import android.net.Uri
import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
+import android.widget.Toast
import androidx.datastore.preferences.core.Preferences
import androidx.lifecycle.lifecycleScope
import androidx.preference.CheckBoxPreference
import androidx.preference.Preference
+import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import im.angry.openeuicc.common.R
import im.angry.openeuicc.util.*
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
class SettingsFragment: PreferenceFragmentCompat() {
+ private lateinit var developerPref: PreferenceCategory
+
+ // Hidden developer options switch
+ private var numClicks = 0
+ private var lastClickTimestamp = -1L
+ private var lastToast: Toast? = null
+
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.pref_settings, rootKey)
+ developerPref = findPreference("pref_developer")!!
+
+ // Show / hide developer preference based on whether it is enabled
+ lifecycleScope.launch {
+ preferenceRepository.developerOptionsEnabledFlow.onEach {
+ developerPref.isVisible = it
+ }.collect()
+ }
+
findPreference("pref_info_app_version")
- ?.summary = requireContext().selfAppVersion
+ ?.apply {
+ summary = requireContext().selfAppVersion
+
+ // Enable developer options when this is clicked for 7 times
+ setOnPreferenceClickListener(this@SettingsFragment::onAppVersionClicked)
+ }
findPreference("pref_info_source_code")
?.setOnPreferenceClickListener {
@@ -50,6 +72,12 @@ class SettingsFragment: PreferenceFragmentCompat() {
findPreference("pref_advanced_verbose_logging")
?.bindBooleanFlow(preferenceRepository.verboseLoggingFlow, PreferenceKeys.VERBOSE_LOGGING)
+
+ findPreference("pref_developer_experimental_download_wizard")
+ ?.bindBooleanFlow(preferenceRepository.experimentalDownloadWizardFlow, PreferenceKeys.EXPERIMENTAL_DOWNLOAD_WIZARD)
+
+ findPreference("pref_ignore_tls_certificate")
+ ?.bindBooleanFlow(preferenceRepository.ignoreTLSCertificate, PreferenceKeys.IGNORE_TLS_CERTIFICATE)
}
override fun onStart() {
@@ -57,6 +85,44 @@ class SettingsFragment: PreferenceFragmentCompat() {
setupRootViewInsets(requireView().requireViewById(androidx.preference.R.id.recycler_view))
}
+ @Suppress("UNUSED_PARAMETER")
+ private fun onAppVersionClicked(pref: Preference): Boolean {
+ if (developerPref.isVisible) return false
+ val now = System.currentTimeMillis()
+ if (now - lastClickTimestamp >= 1000) {
+ numClicks = 1
+ } else {
+ numClicks++
+ }
+ lastClickTimestamp = now
+
+ if (numClicks == 7) {
+ lifecycleScope.launch {
+ preferenceRepository.updatePreference(
+ PreferenceKeys.DEVELOPER_OPTIONS_ENABLED,
+ true
+ )
+
+ lastToast?.cancel()
+ Toast.makeText(
+ requireContext(),
+ R.string.developer_options_enabled,
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ } else if (numClicks > 1) {
+ lastToast?.cancel()
+ lastToast = Toast.makeText(
+ requireContext(),
+ getString(R.string.developer_options_steps, 7 - numClicks),
+ Toast.LENGTH_SHORT
+ )
+ lastToast!!.show()
+ }
+
+ return true
+ }
+
private fun CheckBoxPreference.bindBooleanFlow(flow: Flow, key: Preferences.Key) {
lifecycleScope.launch {
flow.collect { isChecked = it }
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt
new file mode 100644
index 0000000..edd9f2e
--- /dev/null
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt
@@ -0,0 +1,48 @@
+package im.angry.openeuicc.ui.wizard
+
+import android.os.Bundle
+import android.view.View
+import android.widget.ProgressBar
+import androidx.activity.OnBackPressedCallback
+import androidx.activity.enableEdgeToEdge
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updatePadding
+import im.angry.openeuicc.common.R
+import im.angry.openeuicc.ui.BaseEuiccAccessActivity
+
+class DownloadWizardActivity: BaseEuiccAccessActivity() {
+ private lateinit var progressBar: ProgressBar
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ enableEdgeToEdge()
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_download_wizard)
+ onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ // TODO: Actually implement this
+ }
+ })
+
+ progressBar = requireViewById(R.id.progress)
+
+ val navigation = requireViewById(R.id.download_wizard_navigation)
+ val origHeight = navigation.layoutParams.height
+
+ ViewCompat.setOnApplyWindowInsetsListener(navigation) { v, insets ->
+ val bars = insets.getInsets(
+ WindowInsetsCompat.Type.systemBars()
+ or WindowInsetsCompat.Type.displayCutout()
+ )
+ v.updatePadding(bars.left, 0, bars.right, bars.bottom)
+ val newParams = navigation.layoutParams
+ newParams.height = origHeight + bars.bottom
+ navigation.layoutParams = newParams
+ WindowInsetsCompat.CONSUMED
+ }
+ }
+
+ override fun onInit() {
+ progressBar.visibility = View.GONE
+ }
+}
\ 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 262482a..133204c 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
@@ -20,11 +20,19 @@ val Fragment.preferenceRepository: PreferenceRepository
get() = requireContext().preferenceRepository
object PreferenceKeys {
+ // ---- Profile Notifications ----
val NOTIFICATION_DOWNLOAD = booleanPreferencesKey("notification_download")
val NOTIFICATION_DELETE = booleanPreferencesKey("notification_delete")
val NOTIFICATION_SWITCH = booleanPreferencesKey("notification_switch")
+
+ // ---- Advanced ----
val DISABLE_SAFEGUARD_REMOVABLE_ESIM = booleanPreferencesKey("disable_safeguard_removable_esim")
val VERBOSE_LOGGING = booleanPreferencesKey("verbose_logging")
+
+ // ---- Developer Options ----
+ val DEVELOPER_OPTIONS_ENABLED = booleanPreferencesKey("developer_options_enabled")
+ val EXPERIMENTAL_DOWNLOAD_WIZARD = booleanPreferencesKey("experimental_download_wizard")
+ val IGNORE_TLS_CERTIFICATE = booleanPreferencesKey("ignore_tls_certificate")
}
class PreferenceRepository(context: Context) {
@@ -48,6 +56,16 @@ class PreferenceRepository(context: Context) {
val verboseLoggingFlow: Flow =
dataStore.data.map { it[PreferenceKeys.VERBOSE_LOGGING] ?: false }
+ // ---- Developer Options ----
+ val developerOptionsEnabledFlow: Flow =
+ dataStore.data.map { it[PreferenceKeys.DEVELOPER_OPTIONS_ENABLED] ?: false }
+
+ val experimentalDownloadWizardFlow: Flow =
+ dataStore.data.map { it[PreferenceKeys.EXPERIMENTAL_DOWNLOAD_WIZARD] ?: false }
+
+ val ignoreTLSCertificate: Flow =
+ dataStore.data.map { it[PreferenceKeys.IGNORE_TLS_CERTIFICATE] ?: false }
+
suspend fun updatePreference(key: Preferences.Key, value: T) {
dataStore.edit {
it[key] = value
diff --git a/app-common/src/main/res/drawable/ic_chevron_left.xml b/app-common/src/main/res/drawable/ic_chevron_left.xml
new file mode 100644
index 0000000..1152da9
--- /dev/null
+++ b/app-common/src/main/res/drawable/ic_chevron_left.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app-common/src/main/res/drawable/ic_chevron_right.xml b/app-common/src/main/res/drawable/ic_chevron_right.xml
new file mode 100644
index 0000000..1db5e68
--- /dev/null
+++ b/app-common/src/main/res/drawable/ic_chevron_right.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app-common/src/main/res/layout/activity_download_wizard.xml b/app-common/src/main/res/layout/activity_download_wizard.xml
new file mode 100644
index 0000000..605eca2
--- /dev/null
+++ b/app-common/src/main/res/layout/activity_download_wizard.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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 05ea4c5..a2373a3 100644
--- a/app-common/src/main/res/values/strings.xml
+++ b/app-common/src/main/res/values/strings.xml
@@ -58,6 +58,10 @@
This download may fail
This download may fail due to low remaining capacity.
+ Download Wizard
+ Back
+ Next
+
New nickname
Are you sure you want to delete the profile %s? This operation is irreversible.
@@ -97,6 +101,9 @@
Save
Logs at %s
+ You are %d steps away from being a developer.
+ You are now a developer!
+
Settings
Notifications
eSIM profile operations send notifications to the carrier. Fine-tune this behavior as needed here.
@@ -113,6 +120,11 @@
Enable verbose logs, which may contain sensitive information. Only share your logs with someone you trust after turning this on.
Logs
View recent debug logs of the application
+ Developer Options
+ Experimental Download Wizard
+ Enable the experimental new download wizard. Note that it is not fully working yet.
+ Do not check SM-DP+ TLS certificate
+ Do not check SM-DP+ TLS certificate, allow any RSP
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 53395ed..d43c84b 100644
--- a/app-common/src/main/res/xml/pref_settings.xml
+++ b/app-common/src/main/res/xml/pref_settings.xml
@@ -41,6 +41,26 @@
app:iconSpaceReserved="false"
app:title="@string/pref_advanced_logs"
app:summary="@string/pref_advanced_logs_desc" />
+
+
+
+
+
+
+
+
+
) : HttpInterface {
+class HttpInterfaceImpl(
+ private val verboseLoggingFlow: Flow,
+ private val ignoreTLSCertificate: Flow
+) : HttpInterface {
companion object {
private const val TAG = "HttpInterfaceImpl"
}
@@ -36,9 +40,6 @@ class HttpInterfaceImpl(private val verboseLoggingFlow: Flow) : HttpInt
}
try {
- val sslContext = SSLContext.getInstance("TLS")
- sslContext.init(null, trustManagers, SecureRandom())
-
val conn = parsedUrl.openConnection() as HttpsURLConnection
conn.connectTimeout = 2000
@@ -47,7 +48,7 @@ class HttpInterfaceImpl(private val verboseLoggingFlow: Flow) : HttpInt
conn.readTimeout = 1000
}
- conn.sslSocketFactory = sslContext.socketFactory
+ conn.sslSocketFactory = getSocketFactory()
conn.requestMethod = "POST"
conn.doInput = true
conn.doOutput = true
@@ -79,6 +80,18 @@ class HttpInterfaceImpl(private val verboseLoggingFlow: Flow) : HttpInt
}
}
+ private fun getSocketFactory(): SSLSocketFactory {
+ val trustManagers =
+ if (runBlocking { ignoreTLSCertificate.first() }) {
+ arrayOf(IgnoreTLSCertificate())
+ } else {
+ this.trustManagers
+ }
+ val sslContext = SSLContext.getInstance("TLS")
+ sslContext.init(null, trustManagers, SecureRandom())
+ return sslContext.socketFactory
+ }
+
override fun usePublicKeyIds(pkids: Array) {
val trustManagerFactory = TrustManagerFactory.getInstance("PKIX").apply {
init(keyIdToKeystore(pkids))
diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/IgnoreTLSCertificate.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/IgnoreTLSCertificate.kt
new file mode 100644
index 0000000..7b13282
--- /dev/null
+++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/IgnoreTLSCertificate.kt
@@ -0,0 +1,22 @@
+package net.typeblog.lpac_jni.impl
+
+import android.annotation.SuppressLint
+import java.security.cert.X509Certificate
+import javax.net.ssl.X509TrustManager
+
+@SuppressLint("CustomX509TrustManager")
+class IgnoreTLSCertificate : X509TrustManager {
+ @SuppressLint("TrustAllX509TrustManager")
+ override fun checkClientTrusted(p0: Array?, p1: String?) {
+ return
+ }
+
+ @SuppressLint("TrustAllX509TrustManager")
+ override fun checkServerTrusted(p0: Array?, p1: String?) {
+ return
+ }
+
+ override fun getAcceptedIssuers(): Array {
+ return emptyArray()
+ }
+}
\ No newline at end of file