feat: add deep link supports #158
5 changed files with 129 additions and 43 deletions
|
@ -30,7 +30,24 @@
|
|||
<activity
|
||||
android:exported="true"
|
||||
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
|
||||
android:exported="true"
|
||||
|
|
|
@ -21,7 +21,6 @@ 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() {
|
||||
|
@ -63,15 +62,15 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
|||
})
|
||||
|
||||
state = DownloadWizardState(
|
||||
null,
|
||||
intent.getIntExtra("selectedLogicalSlot", 0),
|
||||
"",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
-1,
|
||||
null
|
||||
currentStepFragmentClassName = null,
|
||||
selectedLogicalSlot = intent.getIntExtra("selectedLogicalSlot", 0),
|
||||
smdp = "",
|
||||
matchingId = null,
|
||||
confirmationCode = null,
|
||||
imei = null,
|
||||
downloadStarted = false,
|
||||
downloadTaskID = -1,
|
||||
downloadError = null
|
||||
)
|
||||
|
||||
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 {
|
||||
protected val state: DownloadWizardState
|
||||
get() = (requireActivity() as DownloadWizardActivity).state
|
||||
get() = requireWizardActivity().state
|
||||
|
||||
abstract val hasNext: Boolean
|
||||
abstract val hasPrev: Boolean
|
||||
abstract fun createNextFragment(): DownloadWizardStepFragment?
|
||||
abstract fun createPrevFragment(): DownloadWizardStepFragment?
|
||||
|
||||
protected fun requireWizardActivity(): DownloadWizardActivity {
|
||||
return requireActivity() as DownloadWizardActivity
|
||||
}
|
||||
|
||||
protected fun gotoNextFragment(next: DownloadWizardStepFragment? = null) {
|
||||
val realNext = next ?: createNextFragment()
|
||||
(requireActivity() as DownloadWizardActivity).showFragment(
|
||||
requireWizardActivity().showFragment(
|
||||
realNext!!,
|
||||
R.anim.slide_in_right,
|
||||
R.anim.slide_out_left
|
||||
|
@ -273,11 +282,11 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
|||
}
|
||||
|
||||
protected fun hideProgressBar() {
|
||||
(requireActivity() as DownloadWizardActivity).progressBar.visibility = View.GONE
|
||||
requireWizardActivity().progressBar.visibility = View.GONE
|
||||
}
|
||||
|
||||
protected fun showProgressBar(progressValue: Int) {
|
||||
(requireActivity() as DownloadWizardActivity).progressBar.apply {
|
||||
requireWizardActivity().progressBar.apply {
|
||||
visibility = View.VISIBLE
|
||||
if (progressValue >= 0) {
|
||||
isIndeterminate = false
|
||||
|
@ -289,7 +298,7 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
|||
}
|
||||
|
||||
protected fun refreshButtons() {
|
||||
(requireActivity() as DownloadWizardActivity).refreshButtons()
|
||||
requireWizardActivity().refreshButtons()
|
||||
}
|
||||
|
||||
open fun beforeNext() {}
|
||||
|
|
|
@ -35,8 +35,12 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF
|
|||
override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment =
|
||||
DownloadWizardProgressFragment()
|
||||
|
||||
override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment =
|
||||
DownloadWizardMethodSelectFragment()
|
||||
override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment {
|
||||
if (requireWizardActivity().getActivationCodeFromIntent() != null) {
|
||||
|
||||
return DownloadWizardSlotSelectFragment()
|
||||
}
|
||||
return DownloadWizardMethodSelectFragment()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.app.AlertDialog
|
|||
import android.content.ClipboardManager
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
@ -21,10 +22,16 @@ import com.journeyapps.barcodescanner.ScanOptions
|
|||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizardStepFragment() {
|
||||
companion object {
|
||||
const val TAG = "DownloadWizardMethodSelectFragment"
|
||||
}
|
||||
|
||||
data class DownloadMethod(
|
||||
val iconRes: Int,
|
||||
val titleRes: Int,
|
||||
|
@ -89,6 +96,11 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
|
|||
override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment =
|
||||
DownloadWizardSlotSelectFragment()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
requireWizardActivity().getActivationCodeFromIntent()?.let(::processLpaString)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
|
@ -125,28 +137,30 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
|
|||
}
|
||||
|
||||
private fun processLpaString(input: String) {
|
||||
if (runBlocking { preferenceRepository.verboseLoggingFlow.first() }) {
|
||||
Log.i(TAG, "Processing LPA string: $input")
|
||||
}
|
||||
try {
|
||||
val parsed = ActivationCode.fromString(input)
|
||||
state.smdp = parsed.address
|
||||
state.matchingId = parsed.matchingId
|
||||
if (parsed.confirmationCodeRequired) {
|
||||
AlertDialog.Builder(requireContext()).apply {
|
||||
setTitle(R.string.profile_download_required_confirmation_code)
|
||||
setMessage(R.string.profile_download_required_confirmation_code_message)
|
||||
setCancelable(true)
|
||||
setPositiveButton(android.R.string.ok, null)
|
||||
show()
|
||||
}
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.profile_download_required_confirmation_code)
|
||||
.setMessage(R.string.profile_download_required_confirmation_code_message)
|
||||
.setCancelable(true)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
gotoNextFragment(DownloadWizardDetailsFragment())
|
||||
} catch (e: IllegalArgumentException) {
|
||||
AlertDialog.Builder(requireContext()).apply {
|
||||
setTitle(R.string.profile_download_incorrect_lpa_string)
|
||||
setMessage(R.string.profile_download_incorrect_lpa_string_message)
|
||||
setCancelable(true)
|
||||
setNegativeButton(android.R.string.cancel, null)
|
||||
show()
|
||||
}
|
||||
} catch (e: IllegalStateException) {
|
||||
Log.d(TAG, "Failed to parse LPA string", e)
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.profile_download_incorrect_lpa_string)
|
||||
.setMessage(R.string.profile_download_incorrect_lpa_string_message)
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,20 +7,26 @@ data class ActivationCode(
|
|||
val confirmationCodeRequired: Boolean = false,
|
||||
) {
|
||||
companion object {
|
||||
fun fromString(input: String): ActivationCode {
|
||||
val components = input.removePrefix("LPA:").split('$')
|
||||
if (components.size < 2 || components[0] != "1") {
|
||||
throw IllegalArgumentException("Invalid activation code format")
|
||||
}
|
||||
fun fromString(token: String): ActivationCode {
|
||||
val input = if (token.startsWith("LPA:", true)) token.drop(4) else token
|
||||
val components = input.split('$').map { it.trim().ifEmpty { null } }
|
||||
check(components.size >= 2) { "Invalid activation code format" }
|
||||
check(components[0] == "1") { "Invalid activation code version" }
|
||||
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"
|
||||
checkNotNull(components[1]) { "Invalid SM-DP+ address" },
|
||||
components.getOrNull(2),
|
||||
components.getOrNull(3),
|
||||
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 {
|
||||
val parts = arrayOf(
|
||||
"1",
|
||||
|
@ -32,3 +38,39 @@ data class ActivationCode(
|
|||
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.