WIP: refactor: download wizard #144
9 changed files with 335 additions and 151 deletions
|
@ -370,10 +370,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
fun launchProfileDownloadTask(
|
||||
slotId: Int,
|
||||
portId: Int,
|
||||
smdp: String,
|
||||
matchingId: String?,
|
||||
confirmationCode: String?,
|
||||
imei: String?
|
||||
activationCode: ActivationCode
|
||||
): ForegroundTaskSubscriberFlow =
|
||||
launchForegroundTask(
|
||||
getString(R.string.task_profile_download),
|
||||
|
@ -383,10 +380,10 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
euiccChannelManager.beginTrackedOperation(slotId, portId) {
|
||||
euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
|
||||
channel.lpa.downloadProfile(
|
||||
smdp,
|
||||
matchingId,
|
||||
imei,
|
||||
confirmationCode,
|
||||
activationCode.address,
|
||||
activationCode.matchingId,
|
||||
activationCode.imei,
|
||||
activationCode.confirmationCode,
|
||||
object : ProfileDownloadCallback {
|
||||
override fun onStateUpdate(state: ProfileDownloadCallback.DownloadState) {
|
||||
if (state.progress == 0) return
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
package im.angry.openeuicc.ui.wizard
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Button
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.core.view.ViewCompat
|
||||
|
@ -19,21 +19,50 @@ import im.angry.openeuicc.ui.BaseEuiccAccessActivity
|
|||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.typeblog.lpac_jni.LocalProfileAssistant
|
||||
|
||||
class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
||||
data class DownloadWizardState(
|
||||
var currentStepFragmentClassName: String?,
|
||||
var currentStepFragmentClassName: String? = null,
|
||||
var selectedLogicalSlot: Int,
|
||||
var smdp: String,
|
||||
var matchingId: String?,
|
||||
var confirmationCode: String?,
|
||||
var imei: String?,
|
||||
var downloadStarted: Boolean,
|
||||
var downloadTaskID: Long,
|
||||
var downloadError: LocalProfileAssistant.ProfileDownloadException?,
|
||||
)
|
||||
var activationCode: ActivationCode = ActivationCode(),
|
||||
var downloadStarted: Boolean = false,
|
||||
var downloadTaskID: Long = -1,
|
||||
var downloadError: LocalProfileAssistant.ProfileDownloadException? = null,
|
||||
) {
|
||||
companion object {
|
||||
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
|
||||
|
||||
|
@ -61,15 +90,7 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
|||
})
|
||||
|
||||
state = DownloadWizardState(
|
||||
null,
|
||||
intent.getIntExtra("selectedLogicalSlot", 0),
|
||||
"",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
-1,
|
||||
null
|
||||
selectedLogicalSlot = intent.getIntExtra("selectedLogicalSlot", 0),
|
||||
)
|
||||
|
||||
progressBar = requireViewById(R.id.progress)
|
||||
|
@ -113,30 +134,12 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
|||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putString("currentStepFragmentClassName", state.currentStepFragmentClassName)
|
||||
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)
|
||||
state.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||
super.onRestoreInstanceState(savedInstanceState)
|
||||
state.currentStepFragmentClassName = savedInstanceState.getString(
|
||||
"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)
|
||||
state.onRestoreInstanceState(savedInstanceState)
|
||||
}
|
||||
|
||||
private fun onPrevPressed() {
|
||||
|
@ -168,11 +171,9 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
|||
if (!channel.valid) throw EuiccChannelManager.EuiccChannelNotFoundException()
|
||||
}
|
||||
} catch (e: EuiccChannelManager.EuiccChannelNotFoundException) {
|
||||
Toast.makeText(
|
||||
this@DownloadWizardActivity,
|
||||
R.string.download_wizard_slot_removed,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
this@DownloadWizardActivity
|
||||
.makeLongToast(R.string.download_wizard_slot_removed)
|
||||
.show()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
@ -238,8 +239,11 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
|||
}
|
||||
|
||||
abstract class DownloadWizardStepFragment : Fragment(), OpenEuiccContextMarker {
|
||||
private val activity: DownloadWizardActivity
|
||||
get() = requireActivity() as DownloadWizardActivity
|
||||
|
||||
protected val state: DownloadWizardState
|
||||
get() = (requireActivity() as DownloadWizardActivity).state
|
||||
get() = activity.state
|
||||
|
||||
abstract val hasNext: Boolean
|
||||
abstract val hasPrev: Boolean
|
||||
|
@ -247,20 +251,19 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
|||
abstract fun createPrevFragment(): DownloadWizardStepFragment?
|
||||
|
||||
protected fun gotoNextFragment(next: DownloadWizardStepFragment? = null) {
|
||||
val realNext = next ?: createNextFragment()
|
||||
(requireActivity() as DownloadWizardActivity).showFragment(
|
||||
realNext!!,
|
||||
activity.showFragment(
|
||||
next ?: createNextFragment()!!,
|
||||
R.anim.slide_in_right,
|
||||
R.anim.slide_out_left
|
||||
)
|
||||
}
|
||||
|
||||
protected fun hideProgressBar() {
|
||||
(requireActivity() as DownloadWizardActivity).progressBar.visibility = View.GONE
|
||||
activity.progressBar.visibility = View.GONE
|
||||
}
|
||||
|
||||
protected fun showProgressBar(progressValue: Int) {
|
||||
(requireActivity() as DownloadWizardActivity).progressBar.apply {
|
||||
activity.progressBar.apply {
|
||||
visibility = View.VISIBLE
|
||||
if (progressValue >= 0) {
|
||||
isIndeterminate = false
|
||||
|
@ -272,7 +275,7 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
|||
}
|
||||
|
||||
protected fun refreshButtons() {
|
||||
(requireActivity() as DownloadWizardActivity).refreshButtons()
|
||||
activity.refreshButtons()
|
||||
}
|
||||
|
||||
open fun beforeNext() {}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
package im.angry.openeuicc.ui.wizard
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Patterns
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import im.angry.openeuicc.common.R
|
||||
|
@ -17,17 +17,19 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF
|
|||
override val hasPrev: Boolean
|
||||
get() = true
|
||||
|
||||
private lateinit var smdp: TextInputLayout
|
||||
private lateinit var matchingId: TextInputLayout
|
||||
private lateinit var confirmationCode: TextInputLayout
|
||||
private lateinit var imei: TextInputLayout
|
||||
private lateinit var address: EditText
|
||||
private lateinit var matchingId: EditText
|
||||
private lateinit var confirmationCode: EditText
|
||||
private lateinit var imei: EditText
|
||||
|
||||
private fun saveState() {
|
||||
state.smdp = smdp.editText!!.text.toString().trim()
|
||||
state.activationCode.let {
|
||||
it.address = address.text.toString().trim()
|
||||
// Treat empty inputs as null -- this is important for the download step
|
||||
state.matchingId = matchingId.editText!!.text.toString().trim().ifBlank { null }
|
||||
state.confirmationCode = confirmationCode.editText!!.text.toString().trim().ifBlank { null }
|
||||
state.imei = imei.editText!!.text.toString().ifBlank { null }
|
||||
it.matchingId = matchingId.text.toString().trim()
|
||||
it.confirmationCode = confirmationCode.text.toString().trim()
|
||||
it.imei = imei.text.toString().trim()
|
||||
}
|
||||
}
|
||||
|
||||
override fun beforeNext() = saveState()
|
||||
|
@ -42,24 +44,23 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF
|
|||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = inflater.inflate(R.layout.fragment_download_details, container, false)
|
||||
smdp = view.requireViewById(R.id.profile_download_server)
|
||||
matchingId = view.requireViewById(R.id.profile_download_code)
|
||||
confirmationCode = view.requireViewById(R.id.profile_download_confirmation_code)
|
||||
imei = view.requireViewById(R.id.profile_download_imei)
|
||||
smdp.editText!!.addTextChangedListener {
|
||||
updateInputCompleteness()
|
||||
}
|
||||
return view
|
||||
): View = inflater.inflate(R.layout.fragment_download_details, container, false).apply {
|
||||
address = requireViewById<TextInputLayout>(R.id.profile_download_server).editText!!
|
||||
matchingId = requireViewById<TextInputLayout>(R.id.profile_download_code).editText!!
|
||||
confirmationCode = requireViewById<TextInputLayout>(R.id.profile_download_confirmation_code).editText!!
|
||||
imei = requireViewById<TextInputLayout>(R.id.profile_download_imei).editText!!
|
||||
|
||||
address.addTextChangedListener { updateInputCompleteness() }
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
smdp.editText!!.setText(state.smdp)
|
||||
matchingId.editText!!.setText(state.matchingId)
|
||||
confirmationCode.editText!!.setText(state.confirmationCode)
|
||||
imei.editText!!.setText(state.imei)
|
||||
state.activationCode.let {
|
||||
address.setText(it.address)
|
||||
matchingId.setText(it.matchingId)
|
||||
confirmationCode.setText(it.confirmationCode)
|
||||
imei.setText(it.imei)
|
||||
}
|
||||
updateInputCompleteness()
|
||||
}
|
||||
|
||||
|
@ -69,7 +70,13 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF
|
|||
}
|
||||
|
||||
private fun updateInputCompleteness() {
|
||||
inputComplete = Patterns.DOMAIN_NAME.matcher(smdp.editText!!.text).matches()
|
||||
saveState()
|
||||
inputComplete = try {
|
||||
state.activationCode.validate()
|
||||
true
|
||||
} catch (e: IllegalArgumentException) {
|
||||
false
|
||||
}
|
||||
refreshButtons()
|
||||
}
|
||||
}
|
|
@ -9,7 +9,6 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
|
@ -113,11 +112,9 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
|
|||
val text = clipboard.primaryClip?.getItemAt(0)?.text
|
||||
|
||||
if (text == null) {
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
R.string.profile_download_no_lpa_string,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
requireContext()
|
||||
.makeShortToast(R.string.profile_download_no_lpa_string)
|
||||
.show()
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -126,10 +123,9 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
|
|||
|
||||
private fun processLpaString(input: String) {
|
||||
try {
|
||||
val parsed = ActivationCode.fromString(input)
|
||||
state.smdp = parsed.address
|
||||
state.matchingId = parsed.matchingId
|
||||
if (parsed.confirmationCodeRequired) {
|
||||
state.activationCode.fromToken(input)
|
||||
state.activationCode.validate()
|
||||
if (state.activationCode.confirmationCodeRequired) {
|
||||
AlertDialog.Builder(requireContext()).apply {
|
||||
setTitle(R.string.profile_download_required_confirmation_code)
|
||||
setMessage(R.string.profile_download_required_confirmation_code_message)
|
||||
|
@ -176,6 +172,5 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
|
|||
override fun onBindViewHolder(holder: DownloadMethodViewHolder, position: Int) {
|
||||
holder.bind(downloadMethods[position])
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -79,19 +79,11 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep
|
|||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = inflater.inflate(R.layout.fragment_download_progress, container, false)
|
||||
val recyclerView = view.requireViewById<RecyclerView>(R.id.download_progress_list)
|
||||
recyclerView.adapter = adapter
|
||||
recyclerView.layoutManager =
|
||||
LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false)
|
||||
recyclerView.addItemDecoration(
|
||||
DividerItemDecoration(
|
||||
requireContext(),
|
||||
LinearLayoutManager.VERTICAL
|
||||
)
|
||||
)
|
||||
return view
|
||||
): View = inflater.inflate(R.layout.fragment_download_progress, container, false).apply {
|
||||
val view = requireViewById<RecyclerView>(R.id.download_progress_list)
|
||||
view.adapter = adapter
|
||||
view.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||
view.addItemDecoration(DividerItemDecoration(context, LinearLayoutManager.VERTICAL))
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
|
@ -119,8 +111,9 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep
|
|||
// Change the state of the last InProgress item to success (or error)
|
||||
progressItems.forEachIndexed { index, progressItem ->
|
||||
if (progressItem.state == ProgressState.InProgress) {
|
||||
progressItem.state =
|
||||
if (state.downloadError == null) ProgressState.Done else ProgressState.Error
|
||||
progressItem.state = if (state.downloadError == null)
|
||||
ProgressState.Done else
|
||||
ProgressState.Error
|
||||
}
|
||||
|
||||
adapter.notifyItemChanged(index)
|
||||
|
@ -147,25 +140,15 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep
|
|||
} else {
|
||||
euiccChannelManagerService.waitForForegroundTask()
|
||||
|
||||
val (slotId, portId) = euiccChannelManager.withEuiccChannel(state.selectedLogicalSlot) { channel ->
|
||||
Pair(channel.slotId, channel.portId)
|
||||
}
|
||||
val (slotId, portId) = euiccChannelManager.withEuiccChannel(state.selectedLogicalSlot)
|
||||
{ channel -> Pair(channel.slotId, channel.portId) }
|
||||
|
||||
// Set started to true even before we start -- in case we get killed in the middle
|
||||
state.downloadStarted = true
|
||||
|
||||
val ret = euiccChannelManagerService.launchProfileDownloadTask(
|
||||
slotId,
|
||||
portId,
|
||||
state.smdp,
|
||||
state.matchingId,
|
||||
state.confirmationCode,
|
||||
state.imei
|
||||
)
|
||||
|
||||
state.downloadTaskID = ret.taskId
|
||||
|
||||
ret
|
||||
euiccChannelManagerService
|
||||
.launchProfileDownloadTask(slotId, portId, state.activationCode)
|
||||
.also { state.downloadTaskID = it.taskId }
|
||||
}
|
||||
|
||||
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 progressBar =
|
||||
root.requireViewById<ProgressBar>(R.id.download_progress_icon_progress)
|
||||
private val progressBar = root.requireViewById<ProgressBar>(R.id.download_progress_icon_progress)
|
||||
private val icon = root.requireViewById<ImageView>(R.id.download_progress_icon)
|
||||
|
||||
fun bind(item: ProgressItem) {
|
||||
|
|
|
@ -19,7 +19,6 @@ 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() {
|
||||
companion object {
|
||||
|
@ -130,7 +129,7 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
|||
}
|
||||
|
||||
if (slots.isNotEmpty()) {
|
||||
state.imei = slots[adapter.currentSelectedIdx].imei
|
||||
state.activationCode.imei = slots[adapter.currentSelectedIdx].imei
|
||||
}
|
||||
|
||||
adapter.notifyDataSetChanged()
|
||||
|
@ -165,7 +164,7 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
|||
adapter.notifyItemChanged(curIdx)
|
||||
// Selected index isn't logical slot ID directly, needs a conversion
|
||||
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) {
|
||||
|
|
|
@ -1,23 +1,141 @@
|
|||
package im.angry.openeuicc.util
|
||||
|
||||
data class ActivationCode(
|
||||
val address: String,
|
||||
val matchingId: String? = null,
|
||||
val oid: String? = null,
|
||||
val confirmationCodeRequired: Boolean = false,
|
||||
) {
|
||||
companion object {
|
||||
fun fromString(input: String): ActivationCode {
|
||||
val components = input.removePrefix("LPA:").split('$')
|
||||
if (components.size < 2 || components[0] != "1") {
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import java.util.Objects
|
||||
|
||||
class ActivationCode : Parcelable, Cloneable {
|
||||
var address: String
|
||||
var matchingId: String
|
||||
var oid: String
|
||||
var confirmationCodeRequired: Boolean
|
||||
get() = field || confirmationCode.isNotBlank()
|
||||
var confirmationCode: String
|
||||
var imei: String
|
||||
|
||||
constructor() {
|
||||
address = ""
|
||||
matchingId = ""
|
||||
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")
|
||||
}
|
||||
return ActivationCode(
|
||||
address = components[1].trim(),
|
||||
matchingId = components.getOrNull(2)?.trim()?.ifBlank { null },
|
||||
oid = components.getOrNull(3)?.trim()?.ifBlank { null },
|
||||
confirmationCodeRequired = components.getOrNull(4)?.trim() == "1"
|
||||
)
|
||||
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