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,
+ ignoreTLSCertificateFlow: 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, ignoreTLSCertificateFlow))
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..6ab6935 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,17 @@ 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()) {
+ Intent(requireContext(), DownloadWizardActivity::class.java).apply {
+ putExtra("selectedLogicalSlot", logicalSlotId)
+ startActivity(this)
+ }
+ } 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..89963cb 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.ignoreTLSCertificateFlow, 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..408c55d
--- /dev/null
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt
@@ -0,0 +1,130 @@
+package im.angry.openeuicc.ui.wizard
+
+import android.os.Bundle
+import android.view.View
+import android.widget.Button
+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 androidx.fragment.app.Fragment
+import im.angry.openeuicc.common.R
+import im.angry.openeuicc.ui.BaseEuiccAccessActivity
+import im.angry.openeuicc.util.*
+
+class DownloadWizardActivity: BaseEuiccAccessActivity() {
+ data class DownloadWizardState(
+ var selectedLogicalSlot: Int
+ )
+
+ private lateinit var state: DownloadWizardState
+
+ private lateinit var progressBar: ProgressBar
+ private lateinit var nextButton: Button
+ private lateinit var prevButton: Button
+
+ private var currentFragment: DownloadWizardStepFragment? = null
+
+ 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
+ }
+ })
+
+ state = DownloadWizardState(
+ intent.getIntExtra("selectedLogicalSlot", 0)
+ )
+
+ progressBar = requireViewById(R.id.progress)
+ nextButton = requireViewById(R.id.download_wizard_next)
+ prevButton = requireViewById(R.id.download_wizard_back)
+
+ 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
+ }
+
+ val fragmentRoot = requireViewById(R.id.step_fragment_container)
+ ViewCompat.setOnApplyWindowInsetsListener(fragmentRoot) { v, insets ->
+ val bars = insets.getInsets(
+ WindowInsetsCompat.Type.systemBars()
+ or WindowInsetsCompat.Type.displayCutout()
+ )
+ v.updatePadding(bars.left, bars.top, bars.right, 0)
+ WindowInsetsCompat.CONSUMED
+ }
+ }
+
+ override fun onInit() {
+ progressBar.visibility = View.GONE
+ showFragment(DownloadWizardSlotSelectFragment())
+ }
+
+ private fun showFragment(nextFrag: DownloadWizardStepFragment) {
+ currentFragment = nextFrag
+ supportFragmentManager.beginTransaction().replace(R.id.step_fragment_container, nextFrag)
+ .commit()
+ refreshButtons()
+ }
+
+ private fun refreshButtons() {
+ currentFragment?.let {
+ nextButton.visibility = if (it.hasNext) {
+ View.VISIBLE
+ } else {
+ View.GONE
+ }
+ prevButton.visibility = if (it.hasPrev) {
+ View.VISIBLE
+ } else {
+ View.GONE
+ }
+ }
+ }
+
+ abstract class DownloadWizardStepFragment : Fragment(), OpenEuiccContextMarker {
+ protected val state: DownloadWizardState
+ get() = (requireActivity() as DownloadWizardActivity).state
+
+ abstract val hasNext: Boolean
+ abstract val hasPrev: Boolean
+ abstract fun createNextFragment(): DownloadWizardStepFragment?
+ abstract fun createPrevFragment(): DownloadWizardStepFragment?
+
+ protected fun hideProgressBar() {
+ (requireActivity() as DownloadWizardActivity).progressBar.visibility = View.GONE
+ }
+
+ protected fun showProgressBar(progressValue: Int) {
+ (requireActivity() as DownloadWizardActivity).progressBar.apply {
+ visibility = View.VISIBLE
+ if (progressValue >= 0) {
+ isIndeterminate = false
+ progress = progressValue
+ } else {
+ isIndeterminate = true
+ }
+ }
+ }
+
+ protected fun refreshButtons() {
+ (requireActivity() as DownloadWizardActivity).refreshButtons()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt
new file mode 100644
index 0000000..6720242
--- /dev/null
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt
@@ -0,0 +1,165 @@
+package im.angry.openeuicc.ui.wizard
+
+import android.annotation.SuppressLint
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.CheckBox
+import android.widget.TextView
+import androidx.lifecycle.lifecycleScope
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import im.angry.openeuicc.common.R
+import im.angry.openeuicc.util.*
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
+import net.typeblog.lpac_jni.LocalProfileInfo
+
+class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardStepFragment() {
+ private data class SlotInfo(
+ val logicalSlotId: Int,
+ val isRemovable: Boolean,
+ val hasMultiplePorts: Boolean,
+ val portId: Int,
+ val eID: String,
+ val enabledProfileName: String?
+ )
+
+ private var loaded = false
+
+ private val adapter = SlotInfoAdapter()
+
+ override val hasNext: Boolean
+ get() = loaded && adapter.slots.isNotEmpty()
+ override val hasPrev: Boolean
+ get() = true
+
+ override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment? {
+ TODO("Not yet implemented")
+ }
+
+ override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment? = null
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ val view = inflater.inflate(R.layout.fragment_download_slot_select, container, false)
+ val recyclerView = view.requireViewById(R.id.download_slot_list)
+ recyclerView.adapter = adapter
+ recyclerView.layoutManager =
+ LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false)
+ recyclerView.addItemDecoration(DividerItemDecoration(requireContext(), LinearLayoutManager.VERTICAL))
+ return view
+ }
+
+ override fun onStart() {
+ super.onStart()
+ if (!loaded) {
+ lifecycleScope.launch { init() }
+ }
+ }
+
+ @SuppressLint("NotifyDataSetChanged")
+ private suspend fun init() {
+ ensureEuiccChannelManager()
+ showProgressBar(-1)
+ val slots = euiccChannelManager.flowEuiccPorts().map { (slotId, portId) ->
+ euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
+ SlotInfo(
+ channel.logicalSlotId,
+ channel.port.card.isRemovable,
+ channel.port.card.ports.size > 1,
+ channel.portId,
+ channel.lpa.eID,
+ channel.lpa.profiles.find { it.state == LocalProfileInfo.State.Enabled }?.displayName
+ )
+ }
+ }.toList()
+ adapter.slots = slots
+
+ // Ensure we always have a selected slot by default
+ val selectedIdx = slots.indexOfFirst { it.logicalSlotId == state.selectedLogicalSlot }
+ adapter.currentSelectedIdx = if (selectedIdx > 0) {
+ selectedIdx
+ } else {
+ if (slots.isNotEmpty()) {
+ state.selectedLogicalSlot = slots[0].logicalSlotId
+ }
+ 0
+ }
+
+ adapter.notifyDataSetChanged()
+ hideProgressBar()
+ loaded = true
+ refreshButtons()
+ }
+
+ private inner class SlotItemHolder(val root: View) : ViewHolder(root) {
+ private val title = root.requireViewById(R.id.slot_item_title)
+ private val type = root.requireViewById(R.id.slot_item_type)
+ private val eID = root.requireViewById(R.id.slot_item_eid)
+ private val activeProfile = root.requireViewById(R.id.slot_item_active_profile)
+ private val checkBox = root.requireViewById(R.id.slot_checkbox)
+
+ private var curIdx = -1
+
+ init {
+ root.setOnClickListener(this::onSelect)
+ checkBox.setOnClickListener(this::onSelect)
+ }
+
+ @Suppress("UNUSED_PARAMETER")
+ fun onSelect(view: View) {
+ if (curIdx < 0) return
+ if (adapter.currentSelectedIdx == curIdx) return
+ val lastIdx = adapter.currentSelectedIdx
+ adapter.currentSelectedIdx = curIdx
+ adapter.notifyItemChanged(lastIdx)
+ adapter.notifyItemChanged(curIdx)
+ // Selected index isn't logical slot ID directly, needs a conversion
+ state.selectedLogicalSlot = adapter.slots[adapter.currentSelectedIdx].logicalSlotId
+ }
+
+ fun bind(item: SlotInfo, idx: Int) {
+ curIdx = idx
+
+ type.text = if (item.isRemovable) {
+ root.context.getString(R.string.download_wizard_slot_type_removable)
+ } else if (!item.hasMultiplePorts) {
+ root.context.getString(R.string.download_wizard_slot_type_internal)
+ } else {
+ root.context.getString(
+ R.string.download_wizard_slot_type_internal_port,
+ item.portId
+ )
+ }
+
+ title.text = root.context.getString(R.string.download_wizard_slot_title, item.logicalSlotId)
+ eID.text = item.eID
+ activeProfile.text = item.enabledProfileName ?: root.context.getString(R.string.unknown)
+ checkBox.isChecked = adapter.currentSelectedIdx == idx
+ }
+ }
+
+ private inner class SlotInfoAdapter : RecyclerView.Adapter() {
+ var slots: List = listOf()
+ var currentSelectedIdx = -1
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SlotItemHolder {
+ val root = LayoutInflater.from(parent.context).inflate(R.layout.download_slot_item, parent, false)
+ return SlotItemHolder(root)
+ }
+
+ override fun getItemCount(): Int = slots.size
+
+ override fun onBindViewHolder(holder: SlotItemHolder, position: Int) {
+ holder.bind(slots[position], position)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt
index f0cf193..3f3c4ee 100644
--- a/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt
@@ -32,9 +32,9 @@ val T.portId: Int where T: Fragment, T: EuiccChannelFragmentMarker
val T.isUsb: Boolean where T: Fragment, T: EuiccChannelFragmentMarker
get() = requireArguments().getInt("slotId") == EuiccChannelManager.USB_CHANNEL_ID
-val T.euiccChannelManager: EuiccChannelManager where T: Fragment, T: EuiccChannelFragmentMarker
+val T.euiccChannelManager: EuiccChannelManager where T: Fragment, T: OpenEuiccContextMarker
get() = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManager
-val T.euiccChannelManagerService: EuiccChannelManagerService where T: Fragment, T: EuiccChannelFragmentMarker
+val T.euiccChannelManagerService: EuiccChannelManagerService where T: Fragment, T: OpenEuiccContextMarker
get() = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManagerService
suspend fun T.withEuiccChannel(fn: suspend (EuiccChannel) -> R): R where T : Fragment, T : EuiccChannelFragmentMarker {
@@ -42,7 +42,7 @@ suspend fun T.withEuiccChannel(fn: suspend (EuiccChannel) -> R): R where
return euiccChannelManager.withEuiccChannel(slotId, portId, fn)
}
-suspend fun T.ensureEuiccChannelManager() where T: Fragment, T: EuiccChannelFragmentMarker =
+suspend fun T.ensureEuiccChannelManager() where T: Fragment, T: OpenEuiccContextMarker =
(requireActivity() as BaseEuiccAccessActivity).euiccChannelManagerLoaded.await()
interface EuiccProfilesChangedListener {
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..505630e 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 ignoreTLSCertificateFlow: 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..79513bb
--- /dev/null
+++ b/app-common/src/main/res/layout/activity_download_wizard.xml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app-common/src/main/res/layout/download_slot_item.xml b/app-common/src/main/res/layout/download_slot_item.xml
new file mode 100644
index 0000000..fa06b4c
--- /dev/null
+++ b/app-common/src/main/res/layout/download_slot_item.xml
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app-common/src/main/res/layout/fragment_download_slot_select.xml b/app-common/src/main/res/layout/fragment_download_slot_select.xml
new file mode 100644
index 0000000..6bd2e5d
--- /dev/null
+++ b/app-common/src/main/res/layout/fragment_download_slot_select.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
\ 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..609fe73 100644
--- a/app-common/src/main/res/values/strings.xml
+++ b/app-common/src/main/res/values/strings.xml
@@ -58,6 +58,18 @@
This download may fail
This download may fail due to low remaining capacity.
+ Download Wizard
+ Back
+ Next
+ Confirm the eSIM slot:
+ Logical slot %d
+ Type:
+ Removable
+ Internal
+ Internal, port %d
+ eID:
+ Active Profile:
+
New nickname
Are you sure you want to delete the profile %s? This operation is irreversible.
@@ -97,6 +109,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 +128,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.
+ Ignore SM-DP+ TLS certificate
+ Ignore 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 ignoreTLSCertificateFlow: 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 { ignoreTLSCertificateFlow.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