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

View file

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

View file

@ -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 {
it.address = address.text.toString().trim()
// Treat empty inputs as null -- this is important for the download step // Treat empty inputs as null -- this is important for the download step
state.matchingId = matchingId.editText!!.text.toString().trim().ifBlank { null } it.matchingId = matchingId.text.toString().trim()
state.confirmationCode = confirmationCode.editText!!.text.toString().trim().ifBlank { null } it.confirmationCode = confirmationCode.text.toString().trim()
state.imei = imei.editText!!.text.toString().ifBlank { null } 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()
} }
} }

View file

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

View file

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

View file

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

View file

@ -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()
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") throw IllegalArgumentException("Invalid activation code format")
} }
return ActivationCode( address = components[1]!!
address = components[1].trim(), matchingId = components.getOrNull(2) ?: ""
matchingId = components.getOrNull(2)?.trim()?.ifBlank { null }, oid = components.getOrNull(3) ?: ""
oid = components.getOrNull(3)?.trim()?.ifBlank { null }, confirmationCodeRequired = components.getOrNull(4) == "1"
confirmationCodeRequired = components.getOrNull(4)?.trim() == "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)
}
}
}
}