Compare commits

...

2 commits

Author SHA1 Message Date
0fac24902f
feat: discovery 2025-03-09 12:01:30 +08:00
74cc08ce8e chore: remove visibility attribute from euicc memory reset preference (#166)
Reviewed-on: PeterCxy/OpenEUICC#166
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2025-03-09 04:22:07 +01:00
22 changed files with 384 additions and 167 deletions

View file

@ -1,9 +1,11 @@
package im.angry.openeuicc.core package im.angry.openeuicc.core
import net.typeblog.lpac_jni.EuiccConfiguredAddresses
import net.typeblog.lpac_jni.EuiccInfo2 import net.typeblog.lpac_jni.EuiccInfo2
import net.typeblog.lpac_jni.LocalProfileAssistant import net.typeblog.lpac_jni.LocalProfileAssistant
import net.typeblog.lpac_jni.LocalProfileInfo import net.typeblog.lpac_jni.LocalProfileInfo
import net.typeblog.lpac_jni.LocalProfileNotification import net.typeblog.lpac_jni.LocalProfileNotification
import net.typeblog.lpac_jni.ProfileDiscoveryCallback
import net.typeblog.lpac_jni.ProfileDownloadCallback import net.typeblog.lpac_jni.ProfileDownloadCallback
class LocalProfileAssistantWrapper(orig: LocalProfileAssistant) : class LocalProfileAssistantWrapper(orig: LocalProfileAssistant) :
@ -32,6 +34,9 @@ class LocalProfileAssistantWrapper(orig: LocalProfileAssistant) :
override fun setEs10xMss(mss: Byte) = lpa.setEs10xMss(mss) override fun setEs10xMss(mss: Byte) = lpa.setEs10xMss(mss)
override fun getEuiccConfiguredAddresses(): EuiccConfiguredAddresses =
lpa.getEuiccConfiguredAddresses()
override fun enableProfile(iccid: String, refresh: Boolean): Boolean = override fun enableProfile(iccid: String, refresh: Boolean): Boolean =
lpa.enableProfile(iccid, refresh) lpa.enableProfile(iccid, refresh)
@ -48,6 +53,9 @@ class LocalProfileAssistantWrapper(orig: LocalProfileAssistant) :
callback: ProfileDownloadCallback callback: ProfileDownloadCallback
) = lpa.downloadProfile(smdp, matchingId, imei, confirmationCode, callback) ) = lpa.downloadProfile(smdp, matchingId, imei, confirmationCode, callback)
override fun discoveryProfile(smds: String, imei: String?, callback: ProfileDiscoveryCallback) =
lpa.discoveryProfile(smds, imei, callback)
override fun deleteNotification(seqNumber: Long): Boolean = lpa.deleteNotification(seqNumber) override fun deleteNotification(seqNumber: Long): Boolean = lpa.deleteNotification(seqNumber)
override fun handleNotification(seqNumber: Long): Boolean = lpa.handleNotification(seqNumber) override fun handleNotification(seqNumber: Long): Boolean = lpa.handleNotification(seqNumber)

View file

@ -21,6 +21,7 @@ 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() {
@ -28,6 +29,7 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
var currentStepFragmentClassName: String?, var currentStepFragmentClassName: String?,
var selectedLogicalSlot: Int, var selectedLogicalSlot: Int,
var smdp: String, var smdp: String,
var smds: String?,
var matchingId: String?, var matchingId: String?,
var confirmationCode: String?, var confirmationCode: String?,
var imei: String?, var imei: String?,
@ -67,6 +69,7 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
currentStepFragmentClassName = null, currentStepFragmentClassName = null,
selectedLogicalSlot = intent.getIntExtra("selectedLogicalSlot", 0), selectedLogicalSlot = intent.getIntExtra("selectedLogicalSlot", 0),
smdp = "", smdp = "",
smds = null,
matchingId = null, matchingId = null,
confirmationCode = null, confirmationCode = null,
imei = null, imei = null,

View file

@ -2,8 +2,10 @@ package im.angry.openeuicc.ui.wizard
import android.app.AlertDialog import android.app.AlertDialog
import android.content.ClipboardManager import android.content.ClipboardManager
import android.graphics.Bitmap
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
@ -11,6 +13,8 @@ import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -25,18 +29,15 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizardStepFragment() { class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizardStepFragment() {
data class DownloadMethod( companion object {
val iconRes: Int, const val TAG = "DownloadWizardMethodSelectFragment"
val titleRes: Int, }
val onClick: () -> Unit
)
// TODO: Maybe we should find a better barcode scanner (or an external one?) // TODO: Maybe we should find a better barcode scanner (or an external one?)
private val barcodeScannerLauncher = registerForActivityResult(ScanContract()) { result -> private val barcodeScannerLauncher =
result.contents?.let { content -> registerForActivityResult(ScanContract()) {
processLpaString(content) it.contents?.let(::processLPAString)
} }
}
private val gallerySelectorLauncher = private val gallerySelectorLauncher =
registerForActivityResult(ActivityResultContracts.GetContent()) { result -> registerForActivityResult(ActivityResultContracts.GetContent()) { result ->
@ -51,30 +52,36 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
} }
} }
decoded.getOrNull()?.let { processLpaString(it) } decoded.getOrNull()?.let { processLPAString(it) }
} }
} }
val downloadMethods = arrayOf( private fun getDownloadMethods() = buildList {
DownloadMethod(R.drawable.ic_scan_black, R.string.download_wizard_method_qr_code) { add(DownloadMethod(R.drawable.ic_scan_black, R.string.download_wizard_method_qr_code) {
barcodeScannerLauncher.launch(ScanOptions().apply { val options = ScanOptions()
setDesiredBarcodeFormats(ScanOptions.QR_CODE) .setDesiredBarcodeFormats(ScanOptions.QR_CODE)
setOrientationLocked(false) .setOrientationLocked(false)
}) barcodeScannerLauncher.launch(options)
}, })
DownloadMethod(R.drawable.ic_gallery_black, R.string.download_wizard_method_gallery) { add(DownloadMethod(R.drawable.ic_gallery_black, R.string.download_wizard_method_gallery) {
gallerySelectorLauncher.launch("image/*") gallerySelectorLauncher.launch("image/*")
}, })
DownloadMethod(R.drawable.ic_paste_go, R.string.download_wizard_method_clipboard) { add(DownloadMethod(R.drawable.ic_paste_go, R.string.download_wizard_method_clipboard) {
handleLoadFromClipboard() handleLoadFromClipboard()
}, })
DownloadMethod(R.drawable.ic_edit, R.string.download_wizard_method_manual) { if (state.smds != null) add(
DownloadMethod(R.drawable.ic_search, R.string.download_wizard_method_discovery) {
gotoNextFragment(DownloadWizardDetailsFragment())
}
)
add(DownloadMethod(R.drawable.ic_edit, R.string.download_wizard_method_manual) {
gotoNextFragment(DownloadWizardDetailsFragment()) gotoNextFragment(DownloadWizardDetailsFragment())
} })
) }
override val hasNext: Boolean override val hasNext: Boolean
get() = false get() = false
override val hasPrev: Boolean override val hasPrev: Boolean
get() = true get() = true
@ -88,38 +95,29 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View =
val view = inflater.inflate(R.layout.fragment_download_method_select, container, false) inflater.inflate(R.layout.fragment_download_method_select, container, false).apply {
val recyclerView = view.requireViewById<RecyclerView>(R.id.download_method_list) requireViewById<RecyclerView>(R.id.download_method_list).apply {
recyclerView.adapter = DownloadMethodAdapter() adapter = DownloadMethodAdapter(getDownloadMethods())
recyclerView.layoutManager = layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false) addItemDecoration(DividerItemDecoration(context, LinearLayoutManager.VERTICAL))
recyclerView.addItemDecoration( }
DividerItemDecoration( }
requireContext(),
LinearLayoutManager.VERTICAL
)
)
return view
}
private fun handleLoadFromClipboard() { private fun handleLoadFromClipboard() {
val clipboard = requireContext().getSystemService(ClipboardManager::class.java) val clipboard = requireContext().getSystemService(ClipboardManager::class.java)
val text = clipboard.primaryClip?.getItemAt(0)?.text val text = clipboard.primaryClip?.getItemAt(0)?.text
if (text == null) { if (text == null) {
Toast.makeText( val resId = R.string.profile_download_no_lpa_string
requireContext(), Toast.makeText(requireContext(), resId, Toast.LENGTH_SHORT).show()
R.string.profile_download_no_lpa_string,
Toast.LENGTH_SHORT
).show()
return return
} }
processLpaString(text.toString()) processLPAString(text.toString())
} }
private fun processLpaString(input: String) { private fun processLPAString(input: String) {
try { try {
val parsed = LPAString.parse(input) val parsed = LPAString.parse(input)
state.smdp = parsed.address state.smdp = parsed.address
@ -136,38 +134,39 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
} }
} }
} }
}
private inner class DownloadMethodViewHolder(private val root: View) : ViewHolder(root) { private data class DownloadMethod(
private val icon = root.requireViewById<ImageView>(R.id.download_method_icon) @DrawableRes
private val title = root.requireViewById<TextView>(R.id.download_method_title) val iconRes: Int,
@StringRes
val titleRes: Int,
val disabled: Boolean = false,
val onClick: () -> Unit
)
fun bind(item: DownloadMethod) { private class DownloadMethodViewHolder(private val root: View) : ViewHolder(root) {
icon.setImageResource(item.iconRes) private val icon: ImageView = root.requireViewById(R.id.download_method_icon)
title.setText(item.titleRes) private val title: TextView = root.requireViewById(R.id.download_method_title)
root.setOnClickListener {
// If the user elected to use another download method, reset the confirmation code flag fun bind(item: DownloadMethod) {
// too icon.setImageResource(item.iconRes)
state.confirmationCodeRequired = false title.setText(item.titleRes)
item.onClick() root.setOnClickListener { item.onClick() }
}
}
} }
}
private inner class DownloadMethodAdapter : RecyclerView.Adapter<DownloadMethodViewHolder>() { private class DownloadMethodAdapter(private val methods: List<DownloadMethod>) :
override fun onCreateViewHolder( RecyclerView.Adapter<DownloadMethodViewHolder>() {
parent: ViewGroup,
viewType: Int
): DownloadMethodViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.download_method_item, parent, false)
return DownloadMethodViewHolder(view)
}
override fun getItemCount(): Int = downloadMethods.size override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
LayoutInflater.from(parent.context)
.inflate(R.layout.download_method_item, parent, false)
.let { DownloadMethodViewHolder(it) }
override fun onBindViewHolder(holder: DownloadMethodViewHolder, position: Int) { override fun getItemCount(): Int = methods.size
holder.bind(downloadMethods[position])
}
override fun onBindViewHolder(holder: DownloadMethodViewHolder, position: Int) {
holder.bind(methods[position])
} }
} }

View file

@ -2,6 +2,7 @@ package im.angry.openeuicc.ui.wizard
import android.annotation.SuppressLint import android.annotation.SuppressLint
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
@ -19,7 +20,7 @@ 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 import net.typeblog.lpac_jni.EuiccConfiguredAddresses
class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardStepFragment() { class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardStepFragment() {
companion object { companion object {
@ -37,6 +38,7 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
val imei: String, val imei: String,
val enabledProfileName: String?, val enabledProfileName: String?,
val intrinsicChannelName: String?, val intrinsicChannelName: String?,
val euiccConfiguredAddresses: EuiccConfiguredAddresses
) )
private var loaded = false private var loaded = false
@ -117,6 +119,7 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
}, },
channel.lpa.profiles.enabled?.displayName, channel.lpa.profiles.enabled?.displayName,
channel.intrinsicChannelName, channel.intrinsicChannelName,
channel.lpa.getEuiccConfiguredAddresses(),
) )
} }
}.toList().sortedBy { it.logicalSlotId } }.toList().sortedBy { it.logicalSlotId }
@ -129,6 +132,7 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
} else { } else {
if (slots.isNotEmpty()) { if (slots.isNotEmpty()) {
state.selectedLogicalSlot = slots[0].logicalSlotId state.selectedLogicalSlot = slots[0].logicalSlotId
state.smds = slots[0].euiccConfiguredAddresses.rootDSAddress
} }
0 0
} }
@ -168,8 +172,11 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
adapter.notifyItemChanged(lastIdx) adapter.notifyItemChanged(lastIdx)
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 adapter.slots[adapter.currentSelectedIdx].let {
state.imei = adapter.slots[adapter.currentSelectedIdx].imei state.selectedLogicalSlot = it.logicalSlotId
state.smds = it.euiccConfiguredAddresses.rootDSAddress
state.imei = it.imei
}
} }
fun bind(item: SlotInfo, idx: Int) { fun bind(item: SlotInfo, idx: Int) {

View file

@ -93,13 +93,12 @@ inline fun <T> Bitmap.use(f: (Bitmap) -> T): T =
recycle() recycle()
} }
fun decodeQrFromBitmap(bmp: Bitmap): String? = fun decodeQrFromBitmap(bmp: Bitmap): String? {
runCatching { val pixels = IntArray(bmp.width * bmp.height)
val pixels = IntArray(bmp.width * bmp.height) bmp.getPixels(pixels, 0, bmp.width, 0, 0, bmp.width, bmp.height)
bmp.getPixels(pixels, 0, bmp.width, 0, 0, bmp.width, bmp.height)
val luminanceSource = RGBLuminanceSource(bmp.width, bmp.height, pixels) val luminanceSource = RGBLuminanceSource(bmp.width, bmp.height, pixels)
val binaryBmp = BinaryBitmap(HybridBinarizer(luminanceSource)) val binaryBmp = BinaryBitmap(HybridBinarizer(luminanceSource))
QRCodeReader().decode(binaryBmp).text return QRCodeReader().decode(binaryBmp).text
}.getOrNull() }

View file

@ -1,5 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp"> <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
<path android:fillColor="@android:color/white" android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/> android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />
</vector> </vector>

View file

@ -1,5 +1,10 @@
<vector android:height="24dp" android:tint="?attr/colorControlNormal" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:viewportHeight="24" android:viewportWidth="24" android:width="24dp"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> android:height="24dp"
<path android:fillColor="@android:color/white" android:pathData="M22,16L22,4c0,-1.1 -0.9,-2 -2,-2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2zM11,12l2.03,2.71L16,11l4,5L8,16l3,-4zM2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6L2,6z"/> android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M22,16L22,4c0,-1.1 -0.9,-2 -2,-2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2zM11,12l2.03,2.71L16,11l4,5L8,16l3,-4zM2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6L2,6z" />
</vector> </vector>

View file

@ -1,7 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp"> <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
<path android:fillColor="@android:color/white" android:pathData="M5,5h2v3h10V5h2v6h2V5c0,-1.1 -0.9,-2 -2,-2h-4.18C14.4,1.84 13.3,1 12,1S9.6,1.84 9.18,3H5C3.9,3 3,3.9 3,5v14c0,1.1 0.9,2 2,2h5v-2H5V5zM12,3c0.55,0 1,0.45 1,1s-0.45,1 -1,1s-1,-0.45 -1,-1S11.45,3 12,3z"/> android:height="24dp"
android:tint="?attr/colorControlNormal"
<path android:fillColor="@android:color/white" android:pathData="M18.01,13l-1.42,1.41l1.58,1.58l-6.17,0l0,2l6.17,0l-1.58,1.59l1.42,1.41l3.99,-4z"/> android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M5,5h2v3h10V5h2v6h2V5c0,-1.1 -0.9,-2 -2,-2h-4.18C14.4,1.84 13.3,1 12,1S9.6,1.84 9.18,3H5C3.9,3 3,3.9 3,5v14c0,1.1 0.9,2 2,2h5v-2H5V5zM12,3c0.55,0 1,0.45 1,1s-0.45,1 -1,1s-1,-0.45 -1,-1S11.45,3 12,3z" />
<path
android:fillColor="@android:color/white"
android:pathData="M18.01,13l-1.42,1.41l1.58,1.58l-6.17,0l0,2l6.17,0l-1.58,1.59l1.42,1.41l3.99,-4z" />
</vector> </vector>

View file

@ -1,10 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24" android:viewportHeight="24">
android:tint="?attr/colorControlNormal"> <path
<path android:fillColor="@android:color/white"
android:fillColor="@android:color/white" android:pathData="M9.5,6.5v3h-3v-3H9.5M11,5H5v6h6V5L11,5zM9.5,14.5v3h-3v-3H9.5M11,13H5v6h6V13L11,13zM17.5,6.5v3h-3v-3H17.5M19,5h-6v6h6V5L19,5zM13,13h1.5v1.5H13V13zM14.5,14.5H16V16h-1.5V14.5zM16,13h1.5v1.5H16V13zM13,16h1.5v1.5H13V16zM14.5,17.5H16V19h-1.5V17.5zM16,16h1.5v1.5H16V16zM17.5,14.5H19V16h-1.5V14.5zM17.5,17.5H19V19h-1.5V17.5zM22,7h-2V4h-3V2h5V7zM22,22v-5h-2v3h-3v2H22zM2,22h5v-2H4v-3H2V22zM2,2v5h2V4h3V2H2z" />
android:pathData="M9.5,6.5v3h-3v-3H9.5M11,5H5v6h6V5L11,5zM9.5,14.5v3h-3v-3H9.5M11,13H5v6h6V13L11,13zM17.5,6.5v3h-3v-3H17.5M19,5h-6v6h6V5L19,5zM13,13h1.5v1.5H13V13zM14.5,14.5H16V16h-1.5V14.5zM16,13h1.5v1.5H16V13zM13,16h1.5v1.5H13V16zM14.5,17.5H16V19h-1.5V17.5zM16,16h1.5v1.5H16V16zM17.5,14.5H19V16h-1.5V14.5zM17.5,17.5H19V19h-1.5V17.5zM22,7h-2V4h-3V2h5V7zM22,22v-5h-2v3h-3v2H22zM2,22h5v-2H4v-3H2V22zM2,2v5h2V4h3V2H2z"/>
</vector> </vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="50"
android:viewportHeight="50">
<path
android:fillColor="@android:color/white"
android:pathData="M21,3a17,17 0,1 0,10 31l13,13 3,-3 -13,-13c2,-3 4,-7 4,-11 0,-9 -8,-17 -17,-17ZM21,5a15,15 0,1 1,0 30,15 15,0 0,1 0,-30Z" />
</vector>

View file

@ -83,6 +83,7 @@
<string name="download_wizard_method_gallery">Load a QR code from gallery</string> <string name="download_wizard_method_gallery">Load a QR code from gallery</string>
<string name="download_wizard_method_clipboard">Load from Clipboard</string> <string name="download_wizard_method_clipboard">Load from Clipboard</string>
<string name="download_wizard_method_manual">Enter manually</string> <string name="download_wizard_method_manual">Enter manually</string>
<string name="download_wizard_method_discovery">Discovery</string>
<string name="download_wizard_details">Input or confirm details for downloading your eSIM:</string> <string name="download_wizard_details">Input or confirm details for downloading your eSIM:</string>
<string name="download_wizard_progress">Downloading your eSIM…</string> <string name="download_wizard_progress">Downloading your eSIM…</string>
<string name="download_wizard_progress_step_preparing">Preparing</string> <string name="download_wizard_progress_step_preparing">Preparing</string>

View file

@ -71,7 +71,6 @@
<CheckBoxPreference <CheckBoxPreference
app:iconSpaceReserved="false" app:iconSpaceReserved="false"
app:isPreferenceVisible="false"
app:key="pref_developer_euicc_memory_reset" app:key="pref_developer_euicc_memory_reset"
app:summary="@string/pref_developer_euicc_memory_reset_desc" app:summary="@string/pref_developer_euicc_memory_reset_desc"
app:title="@string/pref_developer_euicc_memory_reset" /> app:title="@string/pref_developer_euicc_memory_reset" />

View file

@ -0,0 +1,15 @@
package net.typeblog.lpac_jni
class EuiccConfiguredAddresses(defaultDPAddress: String?, rootDSAddress: String?) {
val defaultDPAddress: String? = defaultDPAddress
.takeUnless { it.isNullOrBlank() }
val rootDSAddress = rootDSAddress
.takeIf(::isValidDPAddress)
}
private fun isValidDPAddress(address: String?): Boolean {
if (address.isNullOrBlank()) return false
if (address.endsWith("gsma.com")) return false
if (address.endsWith("example.com")) return false
return true
}

View file

@ -12,6 +12,15 @@ interface LocalProfileAssistant {
val lastApduException: Exception?, val lastApduException: Exception?,
) : Exception("Failed to download profile") ) : Exception("Failed to download profile")
@Suppress("ArrayInDataClass")
data class ProfileDiscoveryException(
val lpaErrorReason: String,
val lastHttpResponse: HttpResponse?,
val lastHttpException: Exception?,
val lastApduResponse: ByteArray?,
val lastApduException: Exception?,
) : Exception("Failed to discovery profile")
class ProfileRenameException() : Exception("Failed to rename profile") class ProfileRenameException() : Exception("Failed to rename profile")
class ProfileNameTooLongException() : Exception("Profile name too long") class ProfileNameTooLongException() : Exception("Profile name too long")
class ProfileNameIsInvalidUTF8Exception() : Exception("Profile name is invalid UTF-8") class ProfileNameIsInvalidUTF8Exception() : Exception("Profile name is invalid UTF-8")
@ -30,6 +39,9 @@ interface LocalProfileAssistant {
*/ */
fun setEs10xMss(mss: Byte) fun setEs10xMss(mss: Byte)
// es10a
fun getEuiccConfiguredAddresses(): EuiccConfiguredAddresses
// All blocking functions in this class assume that they are executed on non-Main threads // All blocking functions in this class assume that they are executed on non-Main threads
// The IO context in Kotlin's coroutine library is recommended. // The IO context in Kotlin's coroutine library is recommended.
fun enableProfile(iccid: String, refresh: Boolean = true): Boolean fun enableProfile(iccid: String, refresh: Boolean = true): Boolean
@ -38,6 +50,7 @@ interface LocalProfileAssistant {
fun downloadProfile(smdp: String, matchingId: String?, imei: String?, fun downloadProfile(smdp: String, matchingId: String?, imei: String?,
confirmationCode: String?, callback: ProfileDownloadCallback) confirmationCode: String?, callback: ProfileDownloadCallback)
fun discoveryProfile(smds: String, imei: String?, callback: ProfileDiscoveryCallback)
fun deleteNotification(seqNumber: Long): Boolean fun deleteNotification(seqNumber: Long): Boolean
fun handleNotification(seqNumber: Long): Boolean fun handleNotification(seqNumber: Long): Boolean

View file

@ -29,6 +29,12 @@ internal object LpacJni {
external fun es10bListNotification(handle: Long): Long // A native pointer to a linked list. Handle with linked list-related methods below. May be 0 (null) external fun es10bListNotification(handle: Long): Long // A native pointer to a linked list. Handle with linked list-related methods below. May be 0 (null)
external fun es10bDeleteNotification(handle: Long, seqNumber: Long): Int external fun es10bDeleteNotification(handle: Long, seqNumber: Long): Int
// es10a
external fun es10aGetEuiccConfiguredAddresses(handle: Long): EuiccConfiguredAddresses
// es9p + es11
external fun discoveryProfile(handle: Long, address: String, imei: String?, callback: ProfileDiscoveryCallback): Int
// es9p + es10b // es9p + es10b
// We do not expose all of the functions because of tediousness :) // We do not expose all of the functions because of tediousness :)
external fun downloadProfile(handle: Long, smdp: String, matchingId: String?, imei: String?, external fun downloadProfile(handle: Long, smdp: String, matchingId: String?, imei: String?,

View file

@ -0,0 +1,5 @@
package net.typeblog.lpac_jni
interface ProfileDiscoveryCallback {
fun onDiscovered(hosts: Array<String>)
}

View file

@ -2,15 +2,14 @@ package net.typeblog.lpac_jni
interface ProfileDownloadCallback { interface ProfileDownloadCallback {
companion object { companion object {
fun lookupStateFromProgress(progress: Int): DownloadState = fun lookupStateFromProgress(progress: Int): DownloadState = when (progress) {
when (progress) { 0 -> DownloadState.Preparing
0 -> DownloadState.Preparing 20 -> DownloadState.Connecting
20 -> DownloadState.Connecting 40 -> DownloadState.Authenticating
40 -> DownloadState.Authenticating 60 -> DownloadState.Downloading
60 -> DownloadState.Downloading 80 -> DownloadState.Finalizing
80 -> DownloadState.Finalizing else -> throw IllegalArgumentException("Unknown state")
else -> throw IllegalArgumentException("Unknown state") }
}
} }
enum class DownloadState(val progress: Int) { enum class DownloadState(val progress: Int) {

View file

@ -9,6 +9,7 @@ import net.typeblog.lpac_jni.HttpInterface.HttpResponse
import net.typeblog.lpac_jni.LocalProfileAssistant import net.typeblog.lpac_jni.LocalProfileAssistant
import net.typeblog.lpac_jni.LocalProfileInfo import net.typeblog.lpac_jni.LocalProfileInfo
import net.typeblog.lpac_jni.LocalProfileNotification import net.typeblog.lpac_jni.LocalProfileNotification
import net.typeblog.lpac_jni.ProfileDiscoveryCallback
import net.typeblog.lpac_jni.ProfileDownloadCallback import net.typeblog.lpac_jni.ProfileDownloadCallback
import net.typeblog.lpac_jni.Version import net.typeblog.lpac_jni.Version
@ -93,6 +94,9 @@ class LocalProfileAssistantImpl(
LpacJni.euiccSetMss(contextHandle, mss) LpacJni.euiccSetMss(contextHandle, mss)
} }
override fun getEuiccConfiguredAddresses() =
LpacJni.es10aGetEuiccConfiguredAddresses(contextHandle)
override val valid: Boolean override val valid: Boolean
get() = !finalized && apduInterface.valid && try { get() = !finalized && apduInterface.valid && try {
// If we can read both eID and euiccInfo2 properly, we are likely looking at // If we can read both eID and euiccInfo2 properly, we are likely looking at
@ -212,21 +216,32 @@ class LocalProfileAssistantImpl(
callback callback
) )
if (res != 0) { if (res == 0) return
// Construct the error now to store any error information we _can_ access // Construct the error now to store any error information we _can_ access
val err = LocalProfileAssistant.ProfileDownloadException( val err = LocalProfileAssistant.ProfileDownloadException(
lpaErrorReason = LpacJni.downloadErrCodeToString(-res), lpaErrorReason = LpacJni.downloadErrCodeToString(-res),
httpInterface.lastHttpResponse, httpInterface.lastHttpResponse,
httpInterface.lastHttpException, httpInterface.lastHttpException,
apduInterface.lastApduResponse, apduInterface.lastApduResponse,
apduInterface.lastApduException, apduInterface.lastApduException,
) )
// Cancel sessions if possible. This will overwrite recorded errors from HTTP and APDU interfaces. // Cancel sessions if possible. This will overwrite recorded errors from HTTP and APDU interfaces.
LpacJni.cancelSessions(contextHandle) LpacJni.cancelSessions(contextHandle)
throw err throw err
} }
override fun discoveryProfile(smds: String, imei: String?, callback: ProfileDiscoveryCallback) {
val res = LpacJni.discoveryProfile(contextHandle, smds, imei, callback)
if (res == 0) return
throw LocalProfileAssistant.ProfileDiscoveryException(
lpaErrorReason = LpacJni.downloadErrCodeToString(-res),
httpInterface.lastHttpResponse,
httpInterface.lastHttpException,
apduInterface.lastApduResponse,
apduInterface.lastApduException,
)
} }
@Synchronized @Synchronized

View file

@ -0,0 +1,127 @@
#include "lpac-notifications.h"
#include <euicc/es10a.h>
#include <euicc/es10b.h>
#include <euicc/es9p.h>
#include <string.h>
#include <malloc.h>
#include <syslog.h>
#define EUICC_CONFIGURED_ADDRESSES_CLASS "net/typeblog/lpac_jni/EuiccConfiguredAddresses"
jclass euicc_configured_addresses_class;
jmethodID euicc_configured_addresses_constructor;
jmethodID on_discovered;
#define DISCOVERY_CALLBACK_CLASS "net/typeblog/lpac_jni/ProfileDiscoveryCallback"
#define STRING_CLASS "java/lang/String"
static jobject bind_static_field(JNIEnv *env, jclass clazz, const char *name, const char *sig) {
jfieldID field = (*env)->GetStaticFieldID(env, clazz, name, sig);
jobject bound = (*env)->GetStaticObjectField(env, clazz, field);
return (*env)->NewGlobalRef(env, bound);
}
void lpac_discovery_init() {
LPAC_JNI_SETUP_ENV;
jclass download_callback_class = (*env)->FindClass(env, DISCOVERY_CALLBACK_CLASS);
on_discovered = (*env)->GetMethodID(env, download_callback_class, "onDiscovered",
"([L" STRING_CLASS ";)V");
euicc_configured_addresses_class = (*env)->FindClass(env, EUICC_CONFIGURED_ADDRESSES_CLASS);
euicc_configured_addresses_class = (*env)->NewGlobalRef(env, euicc_configured_addresses_class);
euicc_configured_addresses_constructor = (*env)->GetMethodID(
env, euicc_configured_addresses_class, "<init>",
"(L" STRING_CLASS ";L" STRING_CLASS ";)V");
}
JNIEXPORT jobject JNICALL
Java_net_typeblog_lpac_1jni_LpacJni_es10aGetEuiccConfiguredAddresses(
JNIEnv *env,
__attribute__((unused)) jobject thiz,
jlong handle
) {
struct euicc_ctx *ctx = (struct euicc_ctx *) handle;
struct es10a_euicc_configured_addresses addresses;
jobject ret = NULL;
if (es10a_get_euicc_configured_addresses(ctx, &addresses) == 0) {
jstring default_dp_address = toJString(env, addresses.defaultDpAddress);
jstring root_ds_address = toJString(env, addresses.rootDsAddress);
ret = (*env)->NewObject(env, euicc_configured_addresses_class,
euicc_configured_addresses_constructor,
default_dp_address, root_ds_address);
}
es10a_euicc_configured_addresses_free(&addresses);
return ret;
}
JNIEXPORT jint JNICALL
Java_net_typeblog_lpac_1jni_LpacJni_discoveryProfile(
JNIEnv *env,
__attribute__((unused)) jobject thiz,
jlong handle,
jstring address,
jstring imei,
jobject callback,
) {
struct euicc_ctx *ctx = (struct euicc_ctx *) handle;
const char *_address = (*env)->GetStringUTFChars(env, address, NULL);
const char *_imei = NULL;
if (imei != NULL) {
_imei = (*env)->GetStringUTFChars(env, address, NULL);
}
ctx->http.server_address = _address;
char **smdp_list = NULL;
jobjectArray addresses = NULL;
int ret = -1;
ret = es10b_get_euicc_challenge_and_info(ctx);
syslog(LOG_INFO, "es10b_get_euicc_challenge_and_info %d", ret);
if (ret < 0) {
ret = -ES10B_ERROR_REASON_UNDEFINED;
goto out;
}
ret = es9p_initiate_authentication(ctx);
syslog(LOG_INFO, "es9p_initiate_authentication %d", ret);
if (ret < 0) {
ret = -ES10B_ERROR_REASON_UNDEFINED;
goto out;
}
ret = es10b_authenticate_server(ctx, NULL, _imei);
syslog(LOG_INFO, "es10b_authenticate_server %d", ret);
if (ret < 0) {
ret = -ES10B_ERROR_REASON_UNDEFINED;
goto out;
}
ret = es11_authenticate_client(ctx, &smdp_list);
if (ret < 0) {
ret = -ES10B_ERROR_REASON_UNDEFINED;
goto out;
}
jsize n = 0;
for (n = 0; smdp_list[n] != NULL; n++) continue;
addresses = (*env)->NewObjectArray(env, n, string_class, NULL);
for (jsize index = 0; index < n; index++) {
jstring element = toJString(env, smdp_list[index]);
(*env)->SetObjectArrayElement(env, addresses, index, element);
}
(*env)->CallVoidMethod(env, callback, on_discovered, addresses);
out:
if (_imei != NULL) (*env)->ReleaseStringUTFChars(env, imei, _imei);
(*env)->ReleaseStringUTFChars(env, address, _address);
es11_smdp_list_free_all(smdp_list);
return ret;
}

View file

@ -0,0 +1,6 @@
#pragma once
#include <jni.h>
#include "lpac-jni.h"
void lpac_discovery_init();

View file

@ -11,51 +11,38 @@ jobject download_state_authenticating;
jobject download_state_downloading; jobject download_state_downloading;
jobject download_state_finalizing; jobject download_state_finalizing;
jmethodID on_state_update; jmethodID on_discovered;
#define DOWNLOAD_CALLBACK_CLASS "net/typeblog/lpac_jni/ProfileDownloadCallback"
#define DOWNLOAD_STATE_CLASS DOWNLOAD_CALLBACK_CLASS "$DownloadState"
static jobject bind_static_field(JNIEnv *env, jclass clazz, const char *name, const char *sig) {
jfieldID field = (*env)->GetStaticFieldID(env, clazz, name, sig);
jobject bound = (*env)->GetStaticObjectField(env, clazz, field);
return (*env)->NewGlobalRef(env, bound);
}
#define BIND_DOWNLOAD_STATE_STATIC_FIELD(NAME, FIELD) \
NAME = bind_static_field(env, download_state_class, FIELD, "L" DOWNLOAD_STATE_CLASS ";")
void lpac_download_init() { void lpac_download_init() {
LPAC_JNI_SETUP_ENV; LPAC_JNI_SETUP_ENV;
jclass download_state_class = (*env)->FindClass(env, jclass download_state_class = (*env)->FindClass(env, DOWNLOAD_STATE_CLASS);
"net/typeblog/lpac_jni/ProfileDownloadCallback$DownloadState");
jfieldID download_state_preparing_field = (*env)->GetStaticFieldID(env, download_state_class,
"Preparing",
"Lnet/typeblog/lpac_jni/ProfileDownloadCallback$DownloadState;");
download_state_preparing = (*env)->GetStaticObjectField(env, download_state_class,
download_state_preparing_field);
download_state_preparing = (*env)->NewGlobalRef(env, download_state_preparing);
jfieldID download_state_connecting_field = (*env)->GetStaticFieldID(env, download_state_class,
"Connecting",
"Lnet/typeblog/lpac_jni/ProfileDownloadCallback$DownloadState;");
download_state_connecting = (*env)->GetStaticObjectField(env, download_state_class,
download_state_connecting_field);
download_state_connecting = (*env)->NewGlobalRef(env, download_state_connecting);
jfieldID download_state_authenticating_field = (*env)->GetStaticFieldID(env,
download_state_class,
"Authenticating",
"Lnet/typeblog/lpac_jni/ProfileDownloadCallback$DownloadState;");
download_state_authenticating = (*env)->GetStaticObjectField(env, download_state_class,
download_state_authenticating_field);
download_state_authenticating = (*env)->NewGlobalRef(env, download_state_authenticating);
jfieldID download_state_downloading_field = (*env)->GetStaticFieldID(env, download_state_class,
"Downloading",
"Lnet/typeblog/lpac_jni/ProfileDownloadCallback$DownloadState;");
download_state_downloading = (*env)->GetStaticObjectField(env, download_state_class,
download_state_downloading_field);
download_state_downloading = (*env)->NewGlobalRef(env, download_state_downloading);
jfieldID download_state_finalizng_field = (*env)->GetStaticFieldID(env, download_state_class,
"Finalizing",
"Lnet/typeblog/lpac_jni/ProfileDownloadCallback$DownloadState;");
download_state_finalizing = (*env)->GetStaticObjectField(env, download_state_class,
download_state_finalizng_field);
download_state_finalizing = (*env)->NewGlobalRef(env, download_state_finalizing);
jclass download_callback_class = (*env)->FindClass(env, BIND_DOWNLOAD_STATE_STATIC_FIELD(download_state_preparing, "Preparing");
"net/typeblog/lpac_jni/ProfileDownloadCallback"); BIND_DOWNLOAD_STATE_STATIC_FIELD(download_state_connecting, "Connecting");
on_state_update = (*env)->GetMethodID(env, download_callback_class, "onStateUpdate", BIND_DOWNLOAD_STATE_STATIC_FIELD(download_state_authenticating, "Authenticating");
"(Lnet/typeblog/lpac_jni/ProfileDownloadCallback$DownloadState;)V"); BIND_DOWNLOAD_STATE_STATIC_FIELD(download_state_downloading, "Downloading");
BIND_DOWNLOAD_STATE_STATIC_FIELD(download_state_finalizing, "Finalizing");
jclass download_callback_class = (*env)->FindClass(env, DOWNLOAD_CALLBACK_CLASS);
on_discovered = (*env)->GetMethodID(
env, download_callback_class, "onStateUpdate", "(L" DOWNLOAD_STATE_CLASS ";)V");
} }
#undef BIND_DOWNLOAD_STATE_STATIC_FIELD
JNIEXPORT jint JNICALL JNIEXPORT jint JNICALL
Java_net_typeblog_lpac_1jni_LpacJni_downloadProfile(JNIEnv *env, jobject thiz, jlong handle, Java_net_typeblog_lpac_1jni_LpacJni_downloadProfile(JNIEnv *env, jobject thiz, jlong handle,
jstring smdp, jstring matching_id, jstring smdp, jstring matching_id,
@ -79,7 +66,7 @@ Java_net_typeblog_lpac_1jni_LpacJni_downloadProfile(JNIEnv *env, jobject thiz, j
ctx->http.server_address = _smdp; ctx->http.server_address = _smdp;
(*env)->CallVoidMethod(env, callback, on_state_update, download_state_preparing); (*env)->CallVoidMethod(env, callback, on_discovered, download_state_preparing);
ret = es10b_get_euicc_challenge_and_info(ctx); ret = es10b_get_euicc_challenge_and_info(ctx);
syslog(LOG_INFO, "es10b_get_euicc_challenge_and_info %d", ret); syslog(LOG_INFO, "es10b_get_euicc_challenge_and_info %d", ret);
if (ret < 0) { if (ret < 0) {
@ -87,7 +74,7 @@ Java_net_typeblog_lpac_1jni_LpacJni_downloadProfile(JNIEnv *env, jobject thiz, j
goto out; goto out;
} }
(*env)->CallVoidMethod(env, callback, on_state_update, download_state_connecting); (*env)->CallVoidMethod(env, callback, on_discovered, download_state_connecting);
ret = es9p_initiate_authentication(ctx); ret = es9p_initiate_authentication(ctx);
syslog(LOG_INFO, "es9p_initiate_authentication %d", ret); syslog(LOG_INFO, "es9p_initiate_authentication %d", ret);
if (ret < 0) { if (ret < 0) {
@ -95,7 +82,7 @@ Java_net_typeblog_lpac_1jni_LpacJni_downloadProfile(JNIEnv *env, jobject thiz, j
goto out; goto out;
} }
(*env)->CallVoidMethod(env, callback, on_state_update, download_state_authenticating); (*env)->CallVoidMethod(env, callback, on_discovered, download_state_authenticating);
ret = es10b_authenticate_server(ctx, _matching_id, _imei); ret = es10b_authenticate_server(ctx, _matching_id, _imei);
syslog(LOG_INFO, "es10b_authenticate_server %d", ret); syslog(LOG_INFO, "es10b_authenticate_server %d", ret);
if (ret < 0) { if (ret < 0) {
@ -109,7 +96,7 @@ Java_net_typeblog_lpac_1jni_LpacJni_downloadProfile(JNIEnv *env, jobject thiz, j
goto out; goto out;
} }
(*env)->CallVoidMethod(env, callback, on_state_update, download_state_downloading); (*env)->CallVoidMethod(env, callback, on_discovered, download_state_downloading);
ret = es10b_prepare_download(ctx, _confirmation_code); ret = es10b_prepare_download(ctx, _confirmation_code);
syslog(LOG_INFO, "es10b_prepare_download %d", ret); syslog(LOG_INFO, "es10b_prepare_download %d", ret);
if (ret < 0) { if (ret < 0) {
@ -121,7 +108,7 @@ Java_net_typeblog_lpac_1jni_LpacJni_downloadProfile(JNIEnv *env, jobject thiz, j
if (ret < 0) if (ret < 0)
goto out; goto out;
(*env)->CallVoidMethod(env, callback, on_state_update, download_state_finalizing); (*env)->CallVoidMethod(env, callback, on_discovered, download_state_finalizing);
ret = es10b_load_bound_profile_package(ctx, &es10b_load_bound_profile_package_result); ret = es10b_load_bound_profile_package(ctx, &es10b_load_bound_profile_package_result);
syslog(LOG_INFO, "es10b_load_bound_profile_package %d, reason %d", ret, es10b_load_bound_profile_package_result.errorReason); syslog(LOG_INFO, "es10b_load_bound_profile_package %d, reason %d", ret, es10b_load_bound_profile_package_result.errorReason);
if (ret < 0) { if (ret < 0) {

View file

@ -8,6 +8,7 @@
#include "lpac-jni.h" #include "lpac-jni.h"
#include "lpac-download.h" #include "lpac-download.h"
#include "lpac-notifications.h" #include "lpac-notifications.h"
#include "lpac-discovery.h"
#include "interface-wrapper.h" #include "interface-wrapper.h"
JavaVM *jvm = NULL; JavaVM *jvm = NULL;
@ -21,6 +22,7 @@ jint JNI_OnLoad(JavaVM *vm, void *reserved) {
jvm = vm; jvm = vm;
interface_wrapper_init(); interface_wrapper_init();
lpac_download_init(); lpac_download_init();
lpac_discovery_init();
LPAC_JNI_SETUP_ENV; LPAC_JNI_SETUP_ENV;
string_class = (*env)->FindClass(env, "java/lang/String"); string_class = (*env)->FindClass(env, "java/lang/String");