feat: add deep link supports #158

Closed
septs wants to merge 16 commits from septs:deep-link into master
5 changed files with 129 additions and 43 deletions

View file

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

View file

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

View file

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

This should be a boolean flag in the persisted state of download wizard.

This should be a boolean flag in the persisted state of download wizard.
return DownloadWizardSlotSelectFragment()
}
return DownloadWizardMethodSelectFragment()
}
override fun onCreateView(
inflater: LayoutInflater,

View file

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

View file

@ -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",
@ -31,4 +37,40 @@ 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) }