forked from PeterCxy/OpenEUICC
Compare commits
12 commits
e9f4d3d1f9
...
c4b513fc0a
Author | SHA1 | Date | |
---|---|---|---|
c4b513fc0a | |||
6458f54db2 | |||
87f36f4166 | |||
4fb59a4b01 | |||
16636988b0 | |||
93e7297caa | |||
1087a676d4 | |||
375d13b7c4 | |||
a3d59a0761 | |||
5f0dbe3098 | |||
efa9b8bfa4 | |||
47d5c3881c |
19 changed files with 698 additions and 19 deletions
|
@ -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"
|
||||
|
|
|
@ -42,7 +42,8 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
|
|||
port,
|
||||
context.preferenceRepository.verboseLoggingFlow
|
||||
),
|
||||
context.preferenceRepository.verboseLoggingFlow
|
||||
context.preferenceRepository.verboseLoggingFlow,
|
||||
context.preferenceRepository.ignoreTLSCertificateFlow,
|
||||
).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.ignoreTLSCertificateFlow,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -11,14 +11,15 @@ class EuiccChannelImpl(
|
|||
override val type: String,
|
||||
override val port: UiccPortInfoCompat,
|
||||
apduInterface: ApduInterface,
|
||||
verboseLoggingFlow: Flow<Boolean>
|
||||
verboseLoggingFlow: Flow<Boolean>,
|
||||
ignoreTLSCertificateFlow: 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, ignoreTLSCertificateFlow))
|
||||
|
||||
override val valid: Boolean
|
||||
get() = lpa.valid
|
||||
|
|
|
@ -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,10 +107,19 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false)
|
||||
|
||||
fab.setOnClickListener {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
|
|
@ -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.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<Boolean>, key: Preferences.Key<Boolean>) {
|
||||
lifecycleScope.launch {
|
||||
flow.collect { isChecked = it }
|
||||
|
|
|
@ -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<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
|
||||
}
|
||||
|
||||
val fragmentRoot = requireViewById<View>(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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<RecyclerView>(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<TextView>(R.id.slot_item_title)
|
||||
private val type = root.requireViewById<TextView>(R.id.slot_item_type)
|
||||
private val eID = root.requireViewById<TextView>(R.id.slot_item_eid)
|
||||
private val activeProfile = root.requireViewById<TextView>(R.id.slot_item_active_profile)
|
||||
private val checkBox = root.requireViewById<CheckBox>(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<SlotItemHolder>() {
|
||||
var slots: List<SlotInfo> = 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -32,9 +32,9 @@ val <T> T.portId: Int where T: Fragment, T: EuiccChannelFragmentMarker
|
|||
val <T> T.isUsb: Boolean where T: Fragment, T: EuiccChannelFragmentMarker
|
||||
get() = requireArguments().getInt("slotId") == EuiccChannelManager.USB_CHANNEL_ID
|
||||
|
||||
val <T> T.euiccChannelManager: EuiccChannelManager where T: Fragment, T: EuiccChannelFragmentMarker
|
||||
val <T> T.euiccChannelManager: EuiccChannelManager where T: Fragment, T: OpenEuiccContextMarker
|
||||
get() = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManager
|
||||
val <T> T.euiccChannelManagerService: EuiccChannelManagerService where T: Fragment, T: EuiccChannelFragmentMarker
|
||||
val <T> T.euiccChannelManagerService: EuiccChannelManagerService where T: Fragment, T: OpenEuiccContextMarker
|
||||
get() = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManagerService
|
||||
|
||||
suspend fun <T, R> T.withEuiccChannel(fn: suspend (EuiccChannel) -> R): R where T : Fragment, T : EuiccChannelFragmentMarker {
|
||||
|
@ -42,7 +42,7 @@ suspend fun <T, R> T.withEuiccChannel(fn: suspend (EuiccChannel) -> R): R where
|
|||
return euiccChannelManager.withEuiccChannel(slotId, portId, fn)
|
||||
}
|
||||
|
||||
suspend fun <T> T.ensureEuiccChannelManager() where T: Fragment, T: EuiccChannelFragmentMarker =
|
||||
suspend fun <T> T.ensureEuiccChannelManager() where T: Fragment, T: OpenEuiccContextMarker =
|
||||
(requireActivity() as BaseEuiccAccessActivity).euiccChannelManagerLoaded.await()
|
||||
|
||||
interface EuiccProfilesChangedListener {
|
||||
|
|
|
@ -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 ignoreTLSCertificateFlow: 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
|
||||
|
|
5
app-common/src/main/res/drawable/ic_chevron_left.xml
Normal file
5
app-common/src/main/res/drawable/ic_chevron_left.xml
Normal 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>
|
5
app-common/src/main/res/drawable/ic_chevron_right.xml
Normal file
5
app-common/src/main/res/drawable/ic_chevron_right.xml
Normal 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>
|
74
app-common/src/main/res/layout/activity_download_wizard.xml
Normal file
74
app-common/src/main/res/layout/activity_download_wizard.xml
Normal file
|
@ -0,0 +1,74 @@
|
|||
<?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">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/step_fragment_container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/download_wizard_navigation"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<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>
|
94
app-common/src/main/res/layout/download_slot_item.xml
Normal file
94
app-common/src/main/res/layout/download_slot_item.xml
Normal file
|
@ -0,0 +1,94 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="20sp"
|
||||
android:paddingTop="10sp"
|
||||
android:paddingStart="20sp"
|
||||
android:paddingEnd="20sp"
|
||||
android:background="?attr/selectableItemBackground">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/slot_item_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="10sp"
|
||||
android:textSize="18sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/slot_item_type_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="100dp"
|
||||
android:text="@string/download_wizard_slot_type"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/slot_item_type"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/slot_item_eid_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="100dp"
|
||||
android:text="@string/download_wizard_slot_eid"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/slot_item_eid"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/slot_item_active_profile_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="100dp"
|
||||
android:text="@string/download_wizard_slot_active_profile"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/slot_item_active_profile"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<androidx.constraintlayout.helper.widget.Flow
|
||||
android:id="@+id/flow1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="10sp"
|
||||
android:layout_marginTop="20sp"
|
||||
android:layout_marginEnd="10sp"
|
||||
app:constraint_referenced_ids="slot_item_type_label,slot_item_type,slot_item_eid_label,slot_item_eid,slot_item_active_profile_label,slot_item_active_profile"
|
||||
app:flow_wrapMode="aligned"
|
||||
app:flow_horizontalAlign="start"
|
||||
app:flow_horizontalBias="1"
|
||||
app:flow_horizontalGap="10sp"
|
||||
app:flow_horizontalStyle="packed"
|
||||
app:flow_maxElementsWrap="2"
|
||||
app:flow_verticalBias="0"
|
||||
app:flow_verticalGap="16sp"
|
||||
app:flow_verticalStyle="packed"
|
||||
app:layout_constraintEnd_toStartOf="@id/slot_checkbox"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/slot_item_title" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/slot_checkbox"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/flow1"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,28 @@
|
|||
<?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">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/download_slot_select_title"
|
||||
android:text="@string/download_wizard_slot_select"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="20sp"
|
||||
android:layout_margin="20sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/download_slot_list"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@id/download_slot_select_title"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constrainedHeight="true" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -58,6 +58,18 @@
|
|||
<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="download_wizard_slot_select">Confirm the eSIM slot:</string>
|
||||
<string name="download_wizard_slot_title">Logical slot %d</string>
|
||||
<string name="download_wizard_slot_type">Type:</string>
|
||||
<string name="download_wizard_slot_type_removable">Removable</string>
|
||||
<string name="download_wizard_slot_type_internal">Internal</string>
|
||||
<string name="download_wizard_slot_type_internal_port">Internal, port %d</string>
|
||||
<string name="download_wizard_slot_eid">eID:</string>
|
||||
<string name="download_wizard_slot_active_profile">Active Profile:</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 +109,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 +128,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">Ignore SM-DP+ TLS certificate</string>
|
||||
<string name="pref_developer_ignore_tls_certificate_desc">Ignore 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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -35,7 +35,8 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto
|
|||
tm,
|
||||
context.preferenceRepository.verboseLoggingFlow
|
||||
),
|
||||
context.preferenceRepository.verboseLoggingFlow
|
||||
context.preferenceRepository.verboseLoggingFlow,
|
||||
context.preferenceRepository.ignoreTLSCertificateFlow,
|
||||
)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// Failed
|
||||
|
|
|
@ -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 ignoreTLSCertificateFlow: 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 { 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<String>) {
|
||||
val trustManagerFactory = TrustManagerFactory.getInstance("PKIX").apply {
|
||||
init(keyIdToKeystore(pkids))
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue