WIP: refactor: download wizard #144

Closed
septs wants to merge 3 commits from septs:refactor-download-wizard into master
9 changed files with 335 additions and 151 deletions

View file

@ -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

View file

@ -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() {}

View file

@ -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()
}
}

View file

@ -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])
}
}
}

View file

@ -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) {

View file

@ -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) {

View file

@ -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)
}

View 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)

View file

@ -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)
}
}
}
}