WIP: refactor: download wizard #144
9 changed files with 335 additions and 151 deletions
|
@ -370,10 +370,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
||||||
fun launchProfileDownloadTask(
|
fun launchProfileDownloadTask(
|
||||||
slotId: Int,
|
slotId: Int,
|
||||||
portId: Int,
|
portId: Int,
|
||||||
smdp: String,
|
activationCode: ActivationCode
|
||||||
matchingId: String?,
|
|
||||||
confirmationCode: String?,
|
|
||||||
imei: String?
|
|
||||||
): ForegroundTaskSubscriberFlow =
|
): ForegroundTaskSubscriberFlow =
|
||||||
launchForegroundTask(
|
launchForegroundTask(
|
||||||
getString(R.string.task_profile_download),
|
getString(R.string.task_profile_download),
|
||||||
|
@ -383,10 +380,10 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
||||||
euiccChannelManager.beginTrackedOperation(slotId, portId) {
|
euiccChannelManager.beginTrackedOperation(slotId, portId) {
|
||||||
euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
|
euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
|
||||||
channel.lpa.downloadProfile(
|
channel.lpa.downloadProfile(
|
||||||
smdp,
|
activationCode.address,
|
||||||
matchingId,
|
activationCode.matchingId,
|
||||||
imei,
|
activationCode.imei,
|
||||||
confirmationCode,
|
activationCode.confirmationCode,
|
||||||
object : ProfileDownloadCallback {
|
object : ProfileDownloadCallback {
|
||||||
override fun onStateUpdate(state: ProfileDownloadCallback.DownloadState) {
|
override fun onStateUpdate(state: ProfileDownloadCallback.DownloadState) {
|
||||||
if (state.progress == 0) return
|
if (state.progress == 0) return
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
package im.angry.openeuicc.ui.wizard
|
package im.angry.openeuicc.ui.wizard
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
|
@ -19,21 +19,50 @@ import im.angry.openeuicc.ui.BaseEuiccAccessActivity
|
||||||
import im.angry.openeuicc.util.*
|
import im.angry.openeuicc.util.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import net.typeblog.lpac_jni.LocalProfileAssistant
|
import net.typeblog.lpac_jni.LocalProfileAssistant
|
||||||
|
|
||||||
class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
||||||
data class DownloadWizardState(
|
data class DownloadWizardState(
|
||||||
var currentStepFragmentClassName: String?,
|
var currentStepFragmentClassName: String? = null,
|
||||||
var selectedLogicalSlot: Int,
|
var selectedLogicalSlot: Int,
|
||||||
var smdp: String,
|
var activationCode: ActivationCode = ActivationCode(),
|
||||||
var matchingId: String?,
|
var downloadStarted: Boolean = false,
|
||||||
var confirmationCode: String?,
|
var downloadTaskID: Long = -1,
|
||||||
var imei: String?,
|
var downloadError: LocalProfileAssistant.ProfileDownloadException? = null,
|
||||||
var downloadStarted: Boolean,
|
) {
|
||||||
var downloadTaskID: Long,
|
companion object {
|
||||||
var downloadError: LocalProfileAssistant.ProfileDownloadException?,
|
private const val FRAGMENT = "currentStepFragmentClassName"
|
||||||
)
|
private const val SLOT = "selectedLogicalSlot"
|
||||||
|
private const val ACT_CODE = "activationCode"
|
||||||
|
private const val DL_STARTED = "downloadStarted"
|
||||||
|
private const val DL_ID = "downloadTaskID"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
outState.putString(FRAGMENT, currentStepFragmentClassName)
|
||||||
|
outState.putInt(SLOT, selectedLogicalSlot)
|
||||||
|
outState.putParcelable(ACT_CODE, activationCode)
|
||||||
|
outState.putBoolean(DL_STARTED, downloadStarted)
|
||||||
|
outState.putLong(DL_ID, downloadTaskID)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||||
|
currentStepFragmentClassName = savedInstanceState
|
||||||
|
.getString(FRAGMENT, currentStepFragmentClassName)
|
||||||
|
selectedLogicalSlot = savedInstanceState
|
||||||
|
.getInt(SLOT, selectedLogicalSlot)
|
||||||
|
activationCode = savedInstanceState
|
||||||
|
.getCompatParcelable(ACT_CODE, ActivationCode::class.java) ?: ActivationCode()
|
||||||
|
downloadStarted = savedInstanceState
|
||||||
|
.getBoolean(DL_STARTED, downloadStarted)
|
||||||
|
downloadTaskID = savedInstanceState
|
||||||
|
.getLong(DL_ID, downloadTaskID)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T> Bundle.getCompatParcelable(key: String, clazz: Class<T>): T? =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
|
||||||
|
getParcelable(key, clazz) else getParcelable(key)
|
||||||
|
}
|
||||||
|
|
||||||
private lateinit var state: DownloadWizardState
|
private lateinit var state: DownloadWizardState
|
||||||
|
|
||||||
|
@ -61,15 +90,7 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
||||||
})
|
})
|
||||||
|
|
||||||
state = DownloadWizardState(
|
state = DownloadWizardState(
|
||||||
null,
|
selectedLogicalSlot = intent.getIntExtra("selectedLogicalSlot", 0),
|
||||||
intent.getIntExtra("selectedLogicalSlot", 0),
|
|
||||||
"",
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
false,
|
|
||||||
-1,
|
|
||||||
null
|
|
||||||
)
|
)
|
||||||
|
|
||||||
progressBar = requireViewById(R.id.progress)
|
progressBar = requireViewById(R.id.progress)
|
||||||
|
@ -113,30 +134,12 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
outState.putString("currentStepFragmentClassName", state.currentStepFragmentClassName)
|
state.onSaveInstanceState(outState)
|
||||||
outState.putInt("selectedLogicalSlot", state.selectedLogicalSlot)
|
|
||||||
outState.putString("smdp", state.smdp)
|
|
||||||
outState.putString("matchingId", state.matchingId)
|
|
||||||
outState.putString("confirmationCode", state.confirmationCode)
|
|
||||||
outState.putString("imei", state.imei)
|
|
||||||
outState.putBoolean("downloadStarted", state.downloadStarted)
|
|
||||||
outState.putLong("downloadTaskID", state.downloadTaskID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||||
super.onRestoreInstanceState(savedInstanceState)
|
super.onRestoreInstanceState(savedInstanceState)
|
||||||
state.currentStepFragmentClassName = savedInstanceState.getString(
|
state.onRestoreInstanceState(savedInstanceState)
|
||||||
"currentStepFragmentClassName",
|
|
||||||
state.currentStepFragmentClassName
|
|
||||||
)
|
|
||||||
state.selectedLogicalSlot =
|
|
||||||
savedInstanceState.getInt("selectedLogicalSlot", state.selectedLogicalSlot)
|
|
||||||
state.smdp = savedInstanceState.getString("smdp", state.smdp)
|
|
||||||
state.matchingId = savedInstanceState.getString("matchingId", state.matchingId)
|
|
||||||
state.imei = savedInstanceState.getString("imei", state.imei)
|
|
||||||
state.downloadStarted =
|
|
||||||
savedInstanceState.getBoolean("downloadStarted", state.downloadStarted)
|
|
||||||
state.downloadTaskID = savedInstanceState.getLong("downloadTaskID", state.downloadTaskID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onPrevPressed() {
|
private fun onPrevPressed() {
|
||||||
|
@ -168,11 +171,9 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
||||||
if (!channel.valid) throw EuiccChannelManager.EuiccChannelNotFoundException()
|
if (!channel.valid) throw EuiccChannelManager.EuiccChannelNotFoundException()
|
||||||
}
|
}
|
||||||
} catch (e: EuiccChannelManager.EuiccChannelNotFoundException) {
|
} catch (e: EuiccChannelManager.EuiccChannelNotFoundException) {
|
||||||
Toast.makeText(
|
this@DownloadWizardActivity
|
||||||
this@DownloadWizardActivity,
|
.makeLongToast(R.string.download_wizard_slot_removed)
|
||||||
R.string.download_wizard_slot_removed,
|
.show()
|
||||||
Toast.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -238,8 +239,11 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class DownloadWizardStepFragment : Fragment(), OpenEuiccContextMarker {
|
abstract class DownloadWizardStepFragment : Fragment(), OpenEuiccContextMarker {
|
||||||
|
private val activity: DownloadWizardActivity
|
||||||
|
get() = requireActivity() as DownloadWizardActivity
|
||||||
|
|
||||||
protected val state: DownloadWizardState
|
protected val state: DownloadWizardState
|
||||||
get() = (requireActivity() as DownloadWizardActivity).state
|
get() = activity.state
|
||||||
|
|
||||||
abstract val hasNext: Boolean
|
abstract val hasNext: Boolean
|
||||||
abstract val hasPrev: Boolean
|
abstract val hasPrev: Boolean
|
||||||
|
@ -247,20 +251,19 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
||||||
abstract fun createPrevFragment(): DownloadWizardStepFragment?
|
abstract fun createPrevFragment(): DownloadWizardStepFragment?
|
||||||
|
|
||||||
protected fun gotoNextFragment(next: DownloadWizardStepFragment? = null) {
|
protected fun gotoNextFragment(next: DownloadWizardStepFragment? = null) {
|
||||||
val realNext = next ?: createNextFragment()
|
activity.showFragment(
|
||||||
(requireActivity() as DownloadWizardActivity).showFragment(
|
next ?: createNextFragment()!!,
|
||||||
realNext!!,
|
|
||||||
R.anim.slide_in_right,
|
R.anim.slide_in_right,
|
||||||
R.anim.slide_out_left
|
R.anim.slide_out_left
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun hideProgressBar() {
|
protected fun hideProgressBar() {
|
||||||
(requireActivity() as DownloadWizardActivity).progressBar.visibility = View.GONE
|
activity.progressBar.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun showProgressBar(progressValue: Int) {
|
protected fun showProgressBar(progressValue: Int) {
|
||||||
(requireActivity() as DownloadWizardActivity).progressBar.apply {
|
activity.progressBar.apply {
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
if (progressValue >= 0) {
|
if (progressValue >= 0) {
|
||||||
isIndeterminate = false
|
isIndeterminate = false
|
||||||
|
@ -272,7 +275,7 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun refreshButtons() {
|
protected fun refreshButtons() {
|
||||||
(requireActivity() as DownloadWizardActivity).refreshButtons()
|
activity.refreshButtons()
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun beforeNext() {}
|
open fun beforeNext() {}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
package im.angry.openeuicc.ui.wizard
|
package im.angry.openeuicc.ui.wizard
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Patterns
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.EditText
|
||||||
import androidx.core.widget.addTextChangedListener
|
import androidx.core.widget.addTextChangedListener
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import im.angry.openeuicc.common.R
|
import im.angry.openeuicc.common.R
|
||||||
|
@ -17,17 +17,19 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF
|
||||||
override val hasPrev: Boolean
|
override val hasPrev: Boolean
|
||||||
get() = true
|
get() = true
|
||||||
|
|
||||||
private lateinit var smdp: TextInputLayout
|
private lateinit var address: EditText
|
||||||
private lateinit var matchingId: TextInputLayout
|
private lateinit var matchingId: EditText
|
||||||
private lateinit var confirmationCode: TextInputLayout
|
private lateinit var confirmationCode: EditText
|
||||||
private lateinit var imei: TextInputLayout
|
private lateinit var imei: EditText
|
||||||
|
|
||||||
private fun saveState() {
|
private fun saveState() {
|
||||||
state.smdp = smdp.editText!!.text.toString().trim()
|
state.activationCode.let {
|
||||||
// Treat empty inputs as null -- this is important for the download step
|
it.address = address.text.toString().trim()
|
||||||
state.matchingId = matchingId.editText!!.text.toString().trim().ifBlank { null }
|
// Treat empty inputs as null -- this is important for the download step
|
||||||
state.confirmationCode = confirmationCode.editText!!.text.toString().trim().ifBlank { null }
|
it.matchingId = matchingId.text.toString().trim()
|
||||||
state.imei = imei.editText!!.text.toString().ifBlank { null }
|
it.confirmationCode = confirmationCode.text.toString().trim()
|
||||||
|
it.imei = imei.text.toString().trim()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun beforeNext() = saveState()
|
override fun beforeNext() = saveState()
|
||||||
|
@ -42,24 +44,23 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View = inflater.inflate(R.layout.fragment_download_details, container, false).apply {
|
||||||
val view = inflater.inflate(R.layout.fragment_download_details, container, false)
|
address = requireViewById<TextInputLayout>(R.id.profile_download_server).editText!!
|
||||||
smdp = view.requireViewById(R.id.profile_download_server)
|
matchingId = requireViewById<TextInputLayout>(R.id.profile_download_code).editText!!
|
||||||
matchingId = view.requireViewById(R.id.profile_download_code)
|
confirmationCode = requireViewById<TextInputLayout>(R.id.profile_download_confirmation_code).editText!!
|
||||||
confirmationCode = view.requireViewById(R.id.profile_download_confirmation_code)
|
imei = requireViewById<TextInputLayout>(R.id.profile_download_imei).editText!!
|
||||||
imei = view.requireViewById(R.id.profile_download_imei)
|
|
||||||
smdp.editText!!.addTextChangedListener {
|
address.addTextChangedListener { updateInputCompleteness() }
|
||||||
updateInputCompleteness()
|
|
||||||
}
|
|
||||||
return view
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
smdp.editText!!.setText(state.smdp)
|
state.activationCode.let {
|
||||||
matchingId.editText!!.setText(state.matchingId)
|
address.setText(it.address)
|
||||||
confirmationCode.editText!!.setText(state.confirmationCode)
|
matchingId.setText(it.matchingId)
|
||||||
imei.editText!!.setText(state.imei)
|
confirmationCode.setText(it.confirmationCode)
|
||||||
|
imei.setText(it.imei)
|
||||||
|
}
|
||||||
updateInputCompleteness()
|
updateInputCompleteness()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,7 +70,13 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateInputCompleteness() {
|
private fun updateInputCompleteness() {
|
||||||
inputComplete = Patterns.DOMAIN_NAME.matcher(smdp.editText!!.text).matches()
|
saveState()
|
||||||
|
inputComplete = try {
|
||||||
|
state.activationCode.validate()
|
||||||
|
true
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
false
|
||||||
|
}
|
||||||
refreshButtons()
|
refreshButtons()
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -9,7 +9,6 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
|
@ -113,11 +112,9 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
|
||||||
val text = clipboard.primaryClip?.getItemAt(0)?.text
|
val text = clipboard.primaryClip?.getItemAt(0)?.text
|
||||||
|
|
||||||
if (text == null) {
|
if (text == null) {
|
||||||
Toast.makeText(
|
requireContext()
|
||||||
requireContext(),
|
.makeShortToast(R.string.profile_download_no_lpa_string)
|
||||||
R.string.profile_download_no_lpa_string,
|
.show()
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,10 +123,9 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
|
||||||
|
|
||||||
private fun processLpaString(input: String) {
|
private fun processLpaString(input: String) {
|
||||||
try {
|
try {
|
||||||
val parsed = ActivationCode.fromString(input)
|
state.activationCode.fromToken(input)
|
||||||
state.smdp = parsed.address
|
state.activationCode.validate()
|
||||||
state.matchingId = parsed.matchingId
|
if (state.activationCode.confirmationCodeRequired) {
|
||||||
if (parsed.confirmationCodeRequired) {
|
|
||||||
AlertDialog.Builder(requireContext()).apply {
|
AlertDialog.Builder(requireContext()).apply {
|
||||||
setTitle(R.string.profile_download_required_confirmation_code)
|
setTitle(R.string.profile_download_required_confirmation_code)
|
||||||
setMessage(R.string.profile_download_required_confirmation_code_message)
|
setMessage(R.string.profile_download_required_confirmation_code_message)
|
||||||
|
@ -176,6 +172,5 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
|
||||||
override fun onBindViewHolder(holder: DownloadMethodViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: DownloadMethodViewHolder, position: Int) {
|
||||||
holder.bind(downloadMethods[position])
|
holder.bind(downloadMethods[position])
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -79,19 +79,11 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View = inflater.inflate(R.layout.fragment_download_progress, container, false).apply {
|
||||||
val view = inflater.inflate(R.layout.fragment_download_progress, container, false)
|
val view = requireViewById<RecyclerView>(R.id.download_progress_list)
|
||||||
val recyclerView = view.requireViewById<RecyclerView>(R.id.download_progress_list)
|
view.adapter = adapter
|
||||||
recyclerView.adapter = adapter
|
view.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||||
recyclerView.layoutManager =
|
view.addItemDecoration(DividerItemDecoration(context, LinearLayoutManager.VERTICAL))
|
||||||
LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false)
|
|
||||||
recyclerView.addItemDecoration(
|
|
||||||
DividerItemDecoration(
|
|
||||||
requireContext(),
|
|
||||||
LinearLayoutManager.VERTICAL
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return view
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
|
@ -119,8 +111,9 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep
|
||||||
// Change the state of the last InProgress item to success (or error)
|
// Change the state of the last InProgress item to success (or error)
|
||||||
progressItems.forEachIndexed { index, progressItem ->
|
progressItems.forEachIndexed { index, progressItem ->
|
||||||
if (progressItem.state == ProgressState.InProgress) {
|
if (progressItem.state == ProgressState.InProgress) {
|
||||||
progressItem.state =
|
progressItem.state = if (state.downloadError == null)
|
||||||
if (state.downloadError == null) ProgressState.Done else ProgressState.Error
|
ProgressState.Done else
|
||||||
|
ProgressState.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
adapter.notifyItemChanged(index)
|
adapter.notifyItemChanged(index)
|
||||||
|
@ -147,25 +140,15 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep
|
||||||
} else {
|
} else {
|
||||||
euiccChannelManagerService.waitForForegroundTask()
|
euiccChannelManagerService.waitForForegroundTask()
|
||||||
|
|
||||||
val (slotId, portId) = euiccChannelManager.withEuiccChannel(state.selectedLogicalSlot) { channel ->
|
val (slotId, portId) = euiccChannelManager.withEuiccChannel(state.selectedLogicalSlot)
|
||||||
Pair(channel.slotId, channel.portId)
|
{ channel -> Pair(channel.slotId, channel.portId) }
|
||||||
}
|
|
||||||
|
|
||||||
// Set started to true even before we start -- in case we get killed in the middle
|
// Set started to true even before we start -- in case we get killed in the middle
|
||||||
state.downloadStarted = true
|
state.downloadStarted = true
|
||||||
|
|
||||||
val ret = euiccChannelManagerService.launchProfileDownloadTask(
|
euiccChannelManagerService
|
||||||
slotId,
|
.launchProfileDownloadTask(slotId, portId, state.activationCode)
|
||||||
portId,
|
.also { state.downloadTaskID = it.taskId }
|
||||||
state.smdp,
|
|
||||||
state.matchingId,
|
|
||||||
state.confirmationCode,
|
|
||||||
state.imei
|
|
||||||
)
|
|
||||||
|
|
||||||
state.downloadTaskID = ret.taskId
|
|
||||||
|
|
||||||
ret
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateProgress(progress: Int) {
|
private fun updateProgress(progress: Int) {
|
||||||
|
@ -189,10 +172,9 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class ProgressItemHolder(val root: View) : RecyclerView.ViewHolder(root) {
|
private inner class ProgressItemHolder(root: View) : RecyclerView.ViewHolder(root) {
|
||||||
private val title = root.requireViewById<TextView>(R.id.download_progress_item_title)
|
private val title = root.requireViewById<TextView>(R.id.download_progress_item_title)
|
||||||
private val progressBar =
|
private val progressBar = root.requireViewById<ProgressBar>(R.id.download_progress_icon_progress)
|
||||||
root.requireViewById<ProgressBar>(R.id.download_progress_icon_progress)
|
|
||||||
private val icon = root.requireViewById<ImageView>(R.id.download_progress_icon)
|
private val icon = root.requireViewById<ImageView>(R.id.download_progress_icon)
|
||||||
|
|
||||||
fun bind(item: ProgressItem) {
|
fun bind(item: ProgressItem) {
|
||||||
|
|
|
@ -19,7 +19,6 @@ import im.angry.openeuicc.util.*
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.toList
|
import kotlinx.coroutines.flow.toList
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import net.typeblog.lpac_jni.LocalProfileInfo
|
|
||||||
|
|
||||||
class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardStepFragment() {
|
class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardStepFragment() {
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -130,7 +129,7 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
||||||
}
|
}
|
||||||
|
|
||||||
if (slots.isNotEmpty()) {
|
if (slots.isNotEmpty()) {
|
||||||
state.imei = slots[adapter.currentSelectedIdx].imei
|
state.activationCode.imei = slots[adapter.currentSelectedIdx].imei
|
||||||
}
|
}
|
||||||
|
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyDataSetChanged()
|
||||||
|
@ -165,7 +164,7 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
||||||
adapter.notifyItemChanged(curIdx)
|
adapter.notifyItemChanged(curIdx)
|
||||||
// Selected index isn't logical slot ID directly, needs a conversion
|
// Selected index isn't logical slot ID directly, needs a conversion
|
||||||
state.selectedLogicalSlot = adapter.slots[adapter.currentSelectedIdx].logicalSlotId
|
state.selectedLogicalSlot = adapter.slots[adapter.currentSelectedIdx].logicalSlotId
|
||||||
state.imei = adapter.slots[adapter.currentSelectedIdx].imei
|
state.activationCode.imei = adapter.slots[adapter.currentSelectedIdx].imei
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bind(item: SlotInfo, idx: Int) {
|
fun bind(item: SlotInfo, idx: Int) {
|
||||||
|
|
|
@ -1,23 +1,141 @@
|
||||||
package im.angry.openeuicc.util
|
package im.angry.openeuicc.util
|
||||||
|
|
||||||
data class ActivationCode(
|
import android.os.Parcel
|
||||||
val address: String,
|
import android.os.Parcelable
|
||||||
val matchingId: String? = null,
|
import java.util.Objects
|
||||||
val oid: String? = null,
|
|
||||||
val confirmationCodeRequired: Boolean = false,
|
class ActivationCode : Parcelable, Cloneable {
|
||||||
) {
|
var address: String
|
||||||
companion object {
|
var matchingId: String
|
||||||
fun fromString(input: String): ActivationCode {
|
var oid: String
|
||||||
val components = input.removePrefix("LPA:").split('$')
|
var confirmationCodeRequired: Boolean
|
||||||
if (components.size < 2 || components[0] != "1") {
|
get() = field || confirmationCode.isNotBlank()
|
||||||
throw IllegalArgumentException("Invalid activation code format")
|
var confirmationCode: String
|
||||||
}
|
var imei: String
|
||||||
return ActivationCode(
|
|
||||||
address = components[1].trim(),
|
constructor() {
|
||||||
matchingId = components.getOrNull(2)?.trim()?.ifBlank { null },
|
address = ""
|
||||||
oid = components.getOrNull(3)?.trim()?.ifBlank { null },
|
matchingId = ""
|
||||||
confirmationCodeRequired = components.getOrNull(4)?.trim() == "1"
|
oid = ""
|
||||||
)
|
confirmationCodeRequired = false
|
||||||
|
confirmationCode = ""
|
||||||
|
imei = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor(parcel: Parcel) {
|
||||||
|
address = parcel.readString() ?: ""
|
||||||
|
matchingId = parcel.readString() ?: ""
|
||||||
|
oid = parcel.readString() ?: ""
|
||||||
|
confirmationCodeRequired = parcel.readByte() != 0.toByte()
|
||||||
|
confirmationCode = parcel.readString() ?: ""
|
||||||
|
imei = parcel.readString() ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validate() {
|
||||||
|
require(isValidDomain(address)) { "SM-DP+ address is invalid" }
|
||||||
|
require(isValidMatchingId(matchingId)) { "Matching ID is invalid" }
|
||||||
|
require(isValidOID(oid)) { "OID is invalid" }
|
||||||
|
require(confirmationCodeRequired && confirmationCode.isNotBlank()) { "Confirmation code is required" }
|
||||||
|
require(isValidIMEI(imei)) { "IMEI is invalid" }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromToken(token: String) {
|
||||||
|
val components = token.removePrefix("LPA:").split('$')
|
||||||
|
.map { it.trim().ifEmpty { null } }
|
||||||
|
if (components.size < 2 || components[0] != "1" || components[1] == null) {
|
||||||
|
throw IllegalArgumentException("Invalid activation code format")
|
||||||
}
|
}
|
||||||
|
address = components[1]!!
|
||||||
|
matchingId = components.getOrNull(2) ?: ""
|
||||||
|
oid = components.getOrNull(3) ?: ""
|
||||||
|
confirmationCodeRequired = components.getOrNull(4) == "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
val parts = listOf(
|
||||||
|
"1",
|
||||||
|
address,
|
||||||
|
matchingId,
|
||||||
|
oid,
|
||||||
|
if (confirmationCodeRequired) "1" else ""
|
||||||
|
)
|
||||||
|
return parts.joinToString("$").trimEnd('$')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
|
parcel.writeString(address)
|
||||||
|
parcel.writeString(matchingId)
|
||||||
|
parcel.writeString(oid)
|
||||||
|
parcel.writeByte(if (confirmationCodeRequired) 1 else 0)
|
||||||
|
parcel.writeString(confirmationCode)
|
||||||
|
parcel.writeString(imei)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun describeContents(): Int = 0
|
||||||
|
|
||||||
|
override fun hashCode(): Int = Objects.hash(
|
||||||
|
address,
|
||||||
|
matchingId,
|
||||||
|
oid,
|
||||||
|
confirmationCodeRequired,
|
||||||
|
confirmationCode,
|
||||||
|
imei
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is ActivationCode) return false
|
||||||
|
if (other.address != address) return false
|
||||||
|
if (other.matchingId != matchingId) return false
|
||||||
|
if (other.oid != oid) return false
|
||||||
|
if (other.confirmationCodeRequired != confirmationCodeRequired) return false
|
||||||
|
if (other.confirmationCode != confirmationCode) return false
|
||||||
|
if (other.imei != imei) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
public override fun clone() = ActivationCode().also {
|
||||||
|
it.address = address
|
||||||
|
it.matchingId = matchingId
|
||||||
|
it.oid = oid
|
||||||
|
it.confirmationCodeRequired = confirmationCodeRequired
|
||||||
|
it.confirmationCode = confirmationCode
|
||||||
|
it.imei = imei
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object CREATOR : Parcelable.Creator<ActivationCode> {
|
||||||
|
override fun createFromParcel(parcel: Parcel) = ActivationCode(parcel)
|
||||||
|
override fun newArray(size: Int): Array<ActivationCode?> = arrayOfNulls(size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isValidSegment(segment: String): Boolean {
|
||||||
|
return segment.all { it.isLetterOrDigit() || it == '-' }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isValidDomain(fqdn: String): Boolean {
|
||||||
|
val name = fqdn.trimEnd('.')
|
||||||
|
if (name.length !in 1..<255) return false
|
||||||
|
if (!name.contains('.')) return false
|
||||||
|
return name.split('.').all { it.length < 64 && isValidSegment(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isValidMatchingId(matchingId: String): Boolean {
|
||||||
|
// TODO: matching id string max length in specs not defined
|
||||||
|
if (matchingId.isBlank()) return true
|
||||||
|
return isValidSegment(matchingId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isValidOID(oid: String): Boolean {
|
||||||
|
if (oid.isBlank()) return true
|
||||||
|
if (!oid.contains('.')) return false
|
||||||
|
return oid.split('.').all { it.all(Char::isDigit) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isValidIMEI(imei: String): Boolean {
|
||||||
|
// TODO: Strong IMEI validation
|
||||||
|
if (imei.isBlank()) return true
|
||||||
|
if (imei.length !in 14..16) return false
|
||||||
|
return imei.all(Char::isDigit)
|
||||||
|
}
|
18
app-common/src/main/java/im/angry/openeuicc/util/Toast.kt
Normal file
18
app-common/src/main/java/im/angry/openeuicc/util/Toast.kt
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
package im.angry.openeuicc.util
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
|
||||||
|
fun Activity.makeLongToast(@StringRes resId: Int): Toast =
|
||||||
|
Toast.makeText(this, resId, Toast.LENGTH_LONG)
|
||||||
|
|
||||||
|
fun Activity.makeShortToast(@StringRes resId: Int): Toast =
|
||||||
|
Toast.makeText(this, resId, Toast.LENGTH_SHORT)
|
||||||
|
|
||||||
|
fun Context.makeLongToast(@StringRes resId: Int): Toast =
|
||||||
|
Toast.makeText(this, resId, Toast.LENGTH_LONG)
|
||||||
|
|
||||||
|
fun Context.makeShortToast(@StringRes resId: Int): Toast =
|
||||||
|
Toast.makeText(this, resId, Toast.LENGTH_SHORT)
|
|
@ -0,0 +1,65 @@
|
||||||
|
package im.angry.openeuicc.util
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertThrows
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ActivationCodeTest {
|
||||||
|
/**
|
||||||
|
* @see {https://www.gsma.com/esim/wp-content/uploads/2020/06/SGP.22-v2.2.2.pdf#page=112}
|
||||||
|
*/
|
||||||
|
@Suppress("SpellCheckingInspection")
|
||||||
|
private val expectedFixtures = buildMap {
|
||||||
|
val ac = ActivationCode()
|
||||||
|
ac.address = "SMDP.GSMA.COM"
|
||||||
|
ac.matchingId = "04386-AGYFT-A74Y8-3F815"
|
||||||
|
// if SM-DP+ OID and Confirmation Code Required Flag are not present
|
||||||
|
put("1\$SMDP.GSMA.COM\$04386-AGYFT-A74Y8-3F815", ac.clone())
|
||||||
|
// if SM-DP+ OID is not present and Confirmation Code Required Flag is present
|
||||||
|
ac.confirmationCodeRequired = true
|
||||||
|
put("1\$SMDP.GSMA.COM\$04386-AGYFT-A74Y8-3F815\$\$1", ac.clone())
|
||||||
|
// if SM-DP+ OID and Confirmation Code Required flag are present
|
||||||
|
ac.oid = "1.3.6.1.4.1.31746"
|
||||||
|
put("1\$SMDP.GSMA.COM\$04386-AGYFT-A74Y8-3F815\$1.3.6.1.4.1.31746\$1", ac.clone())
|
||||||
|
// if SM-DP+ OID is present and Confirmation Code Required Flag is not present
|
||||||
|
ac.confirmationCodeRequired = false
|
||||||
|
put("1\$SMDP.GSMA.COM\$04386-AGYFT-A74Y8-3F815\$1.3.6.1.4.1.31746", ac.clone())
|
||||||
|
// if SM-DP+ OID is present, Activation token is left blank and Confirmation Code Required Flag is not present
|
||||||
|
ac.matchingId = ""
|
||||||
|
put("1\$SMDP.GSMA.COM\$\$1.3.6.1.4.1.31746", ac.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("SpellCheckingInspection")
|
||||||
|
private val unexpectedFixtures = listOf(
|
||||||
|
"", "LPA:",
|
||||||
|
"1", "LPA:1",
|
||||||
|
"1$", "LPA:1$",
|
||||||
|
"1$$", "LPA:1$$",
|
||||||
|
"2\$SMDP.GSMA.COM", "LPA:2\$SMDP.GSMA.COM",
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testParsing() {
|
||||||
|
val actual = ActivationCode()
|
||||||
|
for ((input, expected) in expectedFixtures) {
|
||||||
|
actual.fromToken(input)
|
||||||
|
assertEquals(expected.address, actual.address)
|
||||||
|
assertEquals(expected.matchingId, actual.matchingId)
|
||||||
|
assertEquals(expected.oid, actual.oid)
|
||||||
|
assertEquals(expected.confirmationCodeRequired, actual.confirmationCodeRequired)
|
||||||
|
assertEquals(expected, actual)
|
||||||
|
assertEquals(expected.toString(), input)
|
||||||
|
assertEquals(expected.hashCode(), actual.hashCode())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testUnexpected() {
|
||||||
|
for (fixture in unexpectedFixtures) {
|
||||||
|
assertThrows(IllegalArgumentException::class.java) {
|
||||||
|
val activationCode = ActivationCode()
|
||||||
|
activationCode.fromToken(fixture)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue