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