feat: add deep link supports #158
5 changed files with 129 additions and 43 deletions
|
@ -30,7 +30,24 @@
|
||||||
<activity
|
<activity
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:name="im.angry.openeuicc.ui.wizard.DownloadWizardActivity"
|
android:name="im.angry.openeuicc.ui.wizard.DownloadWizardActivity"
|
||||||
android:label="@string/download_wizard" />
|
android:label="@string/download_wizard">
|
||||||
|
<intent-filter tools:ignore="AppLinkUrlError">
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<!-- Accepts URIs that begin with "lpa:" -->
|
||||||
|
<!-- for example: "LPA:1$..." -->
|
||||||
|
<!-- refs: https://www.iana.org/assignments/uri-schemes/prov/lpa -->
|
||||||
|
<data
|
||||||
|
android:scheme="lpa"
|
||||||
|
android:sspPrefix="1$" />
|
||||||
|
<data
|
||||||
|
android:scheme="LPA"
|
||||||
|
android:sspPrefix="1$" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
<activity-alias
|
<activity-alias
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
|
|
@ -21,7 +21,6 @@ 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() {
|
||||||
|
@ -63,15 +62,15 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
||||||
})
|
})
|
||||||
|
|
||||||
state = DownloadWizardState(
|
state = DownloadWizardState(
|
||||||
null,
|
currentStepFragmentClassName = null,
|
||||||
intent.getIntExtra("selectedLogicalSlot", 0),
|
selectedLogicalSlot = intent.getIntExtra("selectedLogicalSlot", 0),
|
||||||
"",
|
smdp = "",
|
||||||
null,
|
matchingId = null,
|
||||||
null,
|
confirmationCode = null,
|
||||||
null,
|
imei = null,
|
||||||
false,
|
downloadStarted = false,
|
||||||
-1,
|
downloadTaskID = -1,
|
||||||
null
|
downloadError = null
|
||||||
)
|
)
|
||||||
|
|
||||||
progressBar = requireViewById(R.id.progress)
|
progressBar = requireViewById(R.id.progress)
|
||||||
|
@ -254,18 +253,28 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun getActivationCodeFromIntent(): String? {
|
||||||
|
val uri = intent.data ?: return null
|
||||||
|
if (uri.scheme != "lpa") return null
|
||||||
|
return uri.schemeSpecificPart
|
||||||
|
}
|
||||||
|
|
||||||
abstract class DownloadWizardStepFragment : Fragment(), OpenEuiccContextMarker {
|
abstract class DownloadWizardStepFragment : Fragment(), OpenEuiccContextMarker {
|
||||||
protected val state: DownloadWizardState
|
protected val state: DownloadWizardState
|
||||||
get() = (requireActivity() as DownloadWizardActivity).state
|
get() = requireWizardActivity().state
|
||||||
|
|
||||||
abstract val hasNext: Boolean
|
abstract val hasNext: Boolean
|
||||||
abstract val hasPrev: Boolean
|
abstract val hasPrev: Boolean
|
||||||
abstract fun createNextFragment(): DownloadWizardStepFragment?
|
abstract fun createNextFragment(): DownloadWizardStepFragment?
|
||||||
abstract fun createPrevFragment(): DownloadWizardStepFragment?
|
abstract fun createPrevFragment(): DownloadWizardStepFragment?
|
||||||
|
|
||||||
|
protected fun requireWizardActivity(): DownloadWizardActivity {
|
||||||
|
return requireActivity() as DownloadWizardActivity
|
||||||
|
}
|
||||||
|
|
||||||
protected fun gotoNextFragment(next: DownloadWizardStepFragment? = null) {
|
protected fun gotoNextFragment(next: DownloadWizardStepFragment? = null) {
|
||||||
val realNext = next ?: createNextFragment()
|
val realNext = next ?: createNextFragment()
|
||||||
(requireActivity() as DownloadWizardActivity).showFragment(
|
requireWizardActivity().showFragment(
|
||||||
realNext!!,
|
realNext!!,
|
||||||
R.anim.slide_in_right,
|
R.anim.slide_in_right,
|
||||||
R.anim.slide_out_left
|
R.anim.slide_out_left
|
||||||
|
@ -273,11 +282,11 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun hideProgressBar() {
|
protected fun hideProgressBar() {
|
||||||
(requireActivity() as DownloadWizardActivity).progressBar.visibility = View.GONE
|
requireWizardActivity().progressBar.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun showProgressBar(progressValue: Int) {
|
protected fun showProgressBar(progressValue: Int) {
|
||||||
(requireActivity() as DownloadWizardActivity).progressBar.apply {
|
requireWizardActivity().progressBar.apply {
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
if (progressValue >= 0) {
|
if (progressValue >= 0) {
|
||||||
isIndeterminate = false
|
isIndeterminate = false
|
||||||
|
@ -289,7 +298,7 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun refreshButtons() {
|
protected fun refreshButtons() {
|
||||||
(requireActivity() as DownloadWizardActivity).refreshButtons()
|
requireWizardActivity().refreshButtons()
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun beforeNext() {}
|
open fun beforeNext() {}
|
||||||
|
|
|
@ -35,8 +35,12 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF
|
||||||
override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment =
|
override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment =
|
||||||
DownloadWizardProgressFragment()
|
DownloadWizardProgressFragment()
|
||||||
|
|
||||||
override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment =
|
override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment {
|
||||||
DownloadWizardMethodSelectFragment()
|
if (requireWizardActivity().getActivationCodeFromIntent() != null) {
|
||||||
|
|||||||
|
return DownloadWizardSlotSelectFragment()
|
||||||
|
}
|
||||||
|
return DownloadWizardMethodSelectFragment()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.app.AlertDialog
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
@ -21,10 +22,16 @@ import com.journeyapps.barcodescanner.ScanOptions
|
||||||
import im.angry.openeuicc.common.R
|
import im.angry.openeuicc.common.R
|
||||||
import im.angry.openeuicc.util.*
|
import im.angry.openeuicc.util.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizardStepFragment() {
|
class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizardStepFragment() {
|
||||||
|
companion object {
|
||||||
|
const val TAG = "DownloadWizardMethodSelectFragment"
|
||||||
|
}
|
||||||
|
|
||||||
data class DownloadMethod(
|
data class DownloadMethod(
|
||||||
val iconRes: Int,
|
val iconRes: Int,
|
||||||
val titleRes: Int,
|
val titleRes: Int,
|
||||||
|
@ -89,6 +96,11 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
|
||||||
override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment =
|
override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment =
|
||||||
DownloadWizardSlotSelectFragment()
|
DownloadWizardSlotSelectFragment()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
requireWizardActivity().getActivationCodeFromIntent()?.let(::processLpaString)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
|
@ -125,28 +137,30 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun processLpaString(input: String) {
|
private fun processLpaString(input: String) {
|
||||||
|
if (runBlocking { preferenceRepository.verboseLoggingFlow.first() }) {
|
||||||
|
Log.i(TAG, "Processing LPA string: $input")
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
val parsed = ActivationCode.fromString(input)
|
val parsed = ActivationCode.fromString(input)
|
||||||
state.smdp = parsed.address
|
state.smdp = parsed.address
|
||||||
state.matchingId = parsed.matchingId
|
state.matchingId = parsed.matchingId
|
||||||
if (parsed.confirmationCodeRequired) {
|
if (parsed.confirmationCodeRequired) {
|
||||||
AlertDialog.Builder(requireContext()).apply {
|
AlertDialog.Builder(requireContext())
|
||||||
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)
|
||||||
setCancelable(true)
|
.setCancelable(true)
|
||||||
setPositiveButton(android.R.string.ok, null)
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
show()
|
.show()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
gotoNextFragment(DownloadWizardDetailsFragment())
|
gotoNextFragment(DownloadWizardDetailsFragment())
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalStateException) {
|
||||||
AlertDialog.Builder(requireContext()).apply {
|
Log.d(TAG, "Failed to parse LPA string", e)
|
||||||
setTitle(R.string.profile_download_incorrect_lpa_string)
|
AlertDialog.Builder(requireContext())
|
||||||
setMessage(R.string.profile_download_incorrect_lpa_string_message)
|
.setTitle(R.string.profile_download_incorrect_lpa_string)
|
||||||
setCancelable(true)
|
.setMessage(R.string.profile_download_incorrect_lpa_string_message)
|
||||||
setNegativeButton(android.R.string.cancel, null)
|
.setCancelable(true)
|
||||||
show()
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
}
|
.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,20 +7,26 @@ data class ActivationCode(
|
||||||
val confirmationCodeRequired: Boolean = false,
|
val confirmationCodeRequired: Boolean = false,
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
fun fromString(input: String): ActivationCode {
|
fun fromString(token: String): ActivationCode {
|
||||||
val components = input.removePrefix("LPA:").split('$')
|
val input = if (token.startsWith("LPA:", true)) token.drop(4) else token
|
||||||
if (components.size < 2 || components[0] != "1") {
|
val components = input.split('$').map { it.trim().ifEmpty { null } }
|
||||||
throw IllegalArgumentException("Invalid activation code format")
|
check(components.size >= 2) { "Invalid activation code format" }
|
||||||
}
|
check(components[0] == "1") { "Invalid activation code version" }
|
||||||
return ActivationCode(
|
return ActivationCode(
|
||||||
address = components[1].trim(),
|
checkNotNull(components[1]) { "Invalid SM-DP+ address" },
|
||||||
matchingId = components.getOrNull(2)?.trim()?.ifBlank { null },
|
components.getOrNull(2),
|
||||||
oid = components.getOrNull(3)?.trim()?.ifBlank { null },
|
components.getOrNull(3),
|
||||||
confirmationCodeRequired = components.getOrNull(4)?.trim() == "1"
|
components.getOrNull(4) == "1",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
check(isFQDN(address)) { "Invalid SM-DP+ address" }
|
||||||
|
check(matchingId == null || isMatchingID(matchingId)) { "Invalid Matching ID" }
|
||||||
|
check(oid == null || isObjectIdentifier(oid)) { "Invalid OID" }
|
||||||
|
}
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
val parts = arrayOf(
|
val parts = arrayOf(
|
||||||
"1",
|
"1",
|
||||||
|
@ -32,3 +38,39 @@ data class ActivationCode(
|
||||||
return parts.joinToString("$").trimEnd('$')
|
return parts.joinToString("$").trimEnd('$')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SGP.22 4.1 Activation Code (v2.2.2, p111)
|
||||||
|
*
|
||||||
|
* FQDN (Fully Qualified Domain Name) of the SM-DP+ (e.g., SMDP.GSMA.COM)
|
||||||
|
* restricted to the Alphanumeric mode character set defined in table 5 of ISO/IEC 18004 [15]
|
||||||
|
* excluding '$'
|
||||||
|
*/
|
||||||
|
private fun isFQDN(input: CharSequence): Boolean {
|
||||||
|
if (input.isEmpty() || input.length > 255) return false
|
||||||
|
val parts = input.split('.')
|
||||||
|
if (parts.size < 2) return false
|
||||||
|
for (part in parts) {
|
||||||
|
if (part.isEmpty() || part.length > 63) return false
|
||||||
|
if (part.all { it.isLetterOrDigit() || it == '-' }) continue
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SGP.22 4.1.1 Matching ID (v2.2.2, p112)
|
||||||
|
*
|
||||||
|
* Matching ID is a string of alphanumeric characters and hyphens.
|
||||||
|
*/
|
||||||
|
private fun isMatchingID(input: CharSequence) =
|
||||||
|
input.all { it.isLetterOrDigit() || it == '-' }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SGP.22 4.1 Activation Code (v2.2.2, p111)
|
||||||
|
*
|
||||||
|
* SM-DP+ OID in the CERT.DPauth.ECDSA
|
||||||
|
*/
|
||||||
|
private fun isObjectIdentifier(input: CharSequence) = input.length < 255 &&
|
||||||
|
input.count { it == '.' } > 2 &&
|
||||||
|
input.split('.').all { it.isNotEmpty() && it.all(Char::isDigit) }
|
||||||
|
|
Loading…
Add table
Reference in a new issue
This should be a boolean flag in the persisted state of download wizard.