Compare commits

...

4 commits

Author SHA1 Message Date
3e0a2f591a
feat: ignore tls certificate 2024-11-17 10:27:44 +08:00
5f0dbe3098 ui: Hide developer settings behind 7 clicks 2024-11-16 20:59:36 -05:00
efa9b8bfa4 ui: Set up progress bar for the new wizard 2024-11-16 20:37:43 -05:00
47d5c3881c ui: Add skeleton of an experimental new download flow
This doesn't work yet at all, and is hidden behind an experimental
settings switch.
2024-11-16 18:34:45 -05:00
15 changed files with 306 additions and 16 deletions

View file

@ -32,6 +32,10 @@
android:name="im.angry.openeuicc.ui.LogsActivity"
android:label="@string/pref_advanced_logs" />
<activity
android:name="im.angry.openeuicc.ui.wizard.DownloadWizardActivity"
android:label="@string/download_wizard" />
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="fullSensor"

View file

@ -42,7 +42,8 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
port,
context.preferenceRepository.verboseLoggingFlow
),
context.preferenceRepository.verboseLoggingFlow
context.preferenceRepository.verboseLoggingFlow,
context.preferenceRepository.ignoreTLSCertificate,
).also {
Log.i(DefaultEuiccChannelManager.TAG, "Is OMAPI channel, setting MSS to 60")
it.lpa.setEs10xMss(60)
@ -72,7 +73,8 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
bulkOut,
context.preferenceRepository.verboseLoggingFlow
),
context.preferenceRepository.verboseLoggingFlow
context.preferenceRepository.verboseLoggingFlow,
context.preferenceRepository.ignoreTLSCertificate,
)
}

View file

@ -11,14 +11,15 @@ class EuiccChannelImpl(
override val type: String,
override val port: UiccPortInfoCompat,
apduInterface: ApduInterface,
verboseLoggingFlow: Flow<Boolean>
verboseLoggingFlow: Flow<Boolean>,
ignoreTLSCertificate: Flow<Boolean>
) : 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

View file

@ -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)
}
}
}
}

View file

@ -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<Preference>("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<Preference>("pref_info_source_code")
?.setOnPreferenceClickListener {
@ -50,6 +72,12 @@ class SettingsFragment: PreferenceFragmentCompat() {
findPreference<CheckBoxPreference>("pref_advanced_verbose_logging")
?.bindBooleanFlow(preferenceRepository.verboseLoggingFlow, PreferenceKeys.VERBOSE_LOGGING)
findPreference<CheckBoxPreference>("pref_developer_experimental_download_wizard")
?.bindBooleanFlow(preferenceRepository.experimentalDownloadWizardFlow, PreferenceKeys.EXPERIMENTAL_DOWNLOAD_WIZARD)
findPreference<CheckBoxPreference>("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<Boolean>, key: Preferences.Key<Boolean>) {
lifecycleScope.launch {
flow.collect { isChecked = it }

View file

@ -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<View>(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
}
}

View file

@ -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<Boolean> =
dataStore.data.map { it[PreferenceKeys.VERBOSE_LOGGING] ?: false }
// ---- Developer Options ----
val developerOptionsEnabledFlow: Flow<Boolean> =
dataStore.data.map { it[PreferenceKeys.DEVELOPER_OPTIONS_ENABLED] ?: false }
val experimentalDownloadWizardFlow: Flow<Boolean> =
dataStore.data.map { it[PreferenceKeys.EXPERIMENTAL_DOWNLOAD_WIZARD] ?: false }
val ignoreTLSCertificate: Flow<Boolean> =
dataStore.data.map { it[PreferenceKeys.IGNORE_TLS_CERTIFICATE] ?: false }
suspend fun <T> updatePreference(key: Preferences.Key<T>, value: T) {
dataStore.edit {
it[key] = value

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M15.41,7.41L14,6l-6,6 6,6 1.41,-1.41L10.83,12z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M10,6L8.59,7.41 13.17,12l-4.58,4.59L10,18l6,-6z"/>
</vector>

View file

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<View
android:id="@+id/guideline"
android:layout_width="0dp"
android:layout_height="0dp"
android:orientation="vertical"
android:visibility="invisible"
app:layout_constraintBottom_toTopOf="@id/download_wizard_navigation"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<ProgressBar
android:id="@+id/progress"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:indeterminate="true"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/guideline"
app:layout_constraintBottom_toTopOf="@id/download_wizard_navigation"
style="@style/Widget.AppCompat.ProgressBar.Horizontal" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/download_wizard_navigation"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="?attr/colorSurfaceContainer"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<com.google.android.material.button.MaterialButton
android:id="@+id/download_wizard_back"
android:text="@string/download_wizard_back"
android:background="?attr/selectableItemBackground"
android:textColor="?attr/colorPrimary"
android:layout_width="wrap_content"
android:layout_height="48dp"
app:icon="@drawable/ic_chevron_left"
app:iconGravity="start"
app:iconTint="?attr/colorPrimary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/download_wizard_next"
android:text="@string/download_wizard_next"
android:background="?attr/selectableItemBackground"
android:textColor="?attr/colorPrimary"
android:layout_width="wrap_content"
android:layout_height="48dp"
app:icon="@drawable/ic_chevron_right"
app:iconGravity="end"
app:iconTint="?attr/colorPrimary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -58,6 +58,10 @@
<string name="profile_download_low_nvram_title">This download may fail</string>
<string name="profile_download_low_nvram_message">This download may fail due to low remaining capacity.</string>
<string name="download_wizard">Download Wizard</string>
<string name="download_wizard_back">Back</string>
<string name="download_wizard_next">Next</string>
<string name="profile_rename_new_name">New nickname</string>
<string name="profile_delete_confirm">Are you sure you want to delete the profile %s? This operation is irreversible.</string>
@ -97,6 +101,9 @@
<string name="logs_save">Save</string>
<string name="logs_filename_template">Logs at %s</string>
<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="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>
@ -113,6 +120,11 @@
<string name="pref_advanced_verbose_logging_desc">Enable verbose logs, which may contain sensitive information. Only share your logs with someone you trust after turning this on.</string>
<string name="pref_advanced_logs">Logs</string>
<string name="pref_advanced_logs_desc">View recent debug logs of the application</string>
<string name="pref_developer">Developer Options</string>
<string name="pref_developer_experimental_download_wizard">Experimental Download Wizard</string>
<string name="pref_developer_experimental_download_wizard_desc">Enable the experimental new download wizard. Note that it is not fully working yet.</string>
<string name="pref_developer_ignore_tls_certificate">Do not check SM-DP+ TLS certificate</string>
<string name="pref_developer_ignore_tls_certificate_desc">Do not check SM-DP+ TLS certificate, allow any RSP</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>

View file

@ -41,6 +41,26 @@
app:iconSpaceReserved="false"
app:title="@string/pref_advanced_logs"
app:summary="@string/pref_advanced_logs_desc" />
</PreferenceCategory>
<PreferenceCategory
app:key="pref_developer"
app:title="@string/pref_developer"
app:iconSpaceReserved="false">
<CheckBoxPreference
app:key="pref_developer_experimental_download_wizard"
app:iconSpaceReserved="false"
app:title="@string/pref_developer_experimental_download_wizard"
app:summary="@string/pref_developer_experimental_download_wizard_desc" />
<CheckBoxPreference
app:iconSpaceReserved="false"
app:key="pref_developer_ignore_tls_certificate"
app:summary="@string/pref_developer_ignore_tls_certificate_desc"
app:title="@string/pref_developer_ignore_tls_certificate" />
</PreferenceCategory>
<PreferenceCategory

View file

@ -35,7 +35,8 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto
tm,
context.preferenceRepository.verboseLoggingFlow
),
context.preferenceRepository.verboseLoggingFlow
context.preferenceRepository.verboseLoggingFlow,
context.preferenceRepository.ignoreTLSCertificate,
)
} catch (e: IllegalArgumentException) {
// Failed

View file

@ -9,10 +9,14 @@ import java.net.URL
import java.security.SecureRandom
import javax.net.ssl.HttpsURLConnection
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.TrustManager
import javax.net.ssl.TrustManagerFactory
class HttpInterfaceImpl(private val verboseLoggingFlow: Flow<Boolean>) : HttpInterface {
class HttpInterfaceImpl(
private val verboseLoggingFlow: Flow<Boolean>,
private val ignoreTLSCertificate: Flow<Boolean>
) : HttpInterface {
companion object {
private const val TAG = "HttpInterfaceImpl"
}
@ -36,9 +40,6 @@ class HttpInterfaceImpl(private val verboseLoggingFlow: Flow<Boolean>) : 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<Boolean>) : 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<Boolean>) : 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<String>) {
val trustManagerFactory = TrustManagerFactory.getInstance("PKIX").apply {
init(keyIdToKeystore(pkids))

View file

@ -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<out X509Certificate>?, p1: String?) {
return
}
@SuppressLint("TrustAllX509TrustManager")
override fun checkServerTrusted(p0: Array<out X509Certificate>?, p1: String?) {
return
}
override fun getAcceptedIssuers(): Array<X509Certificate> {
return emptyArray()
}
}