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