Compare commits

...

2 commits

Author SHA1 Message Date
db9fd3bcf2
feat: discovery 2025-03-10 09:29:34 +08:00
88eb1ce0e2 feat: update TelephonyManager preference key and implement context marker interface (#167)
Reviewed-on: PeterCxy/OpenEUICC#167
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2025-03-09 22:47:02 +01:00
28 changed files with 430 additions and 165 deletions

View file

@ -1,9 +1,11 @@
package im.angry.openeuicc.core
import net.typeblog.lpac_jni.EuiccConfiguredAddresses
import net.typeblog.lpac_jni.EuiccInfo2
import net.typeblog.lpac_jni.LocalProfileAssistant
import net.typeblog.lpac_jni.LocalProfileInfo
import net.typeblog.lpac_jni.LocalProfileNotification
import net.typeblog.lpac_jni.ProfileDiscoveryCallback
import net.typeblog.lpac_jni.ProfileDownloadCallback
class LocalProfileAssistantWrapper(orig: LocalProfileAssistant) :
@ -32,6 +34,9 @@ class LocalProfileAssistantWrapper(orig: LocalProfileAssistant) :
override fun setEs10xMss(mss: Byte) = lpa.setEs10xMss(mss)
override fun getEuiccConfiguredAddresses(): EuiccConfiguredAddresses =
lpa.getEuiccConfiguredAddresses()
override fun enableProfile(iccid: String, refresh: Boolean): Boolean =
lpa.enableProfile(iccid, refresh)
@ -48,6 +53,9 @@ class LocalProfileAssistantWrapper(orig: LocalProfileAssistant) :
callback: ProfileDownloadCallback
) = 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 handleNotification(seqNumber: Long): Boolean = lpa.handleNotification(seqNumber)

View file

@ -72,6 +72,9 @@ open class SettingsFragment: PreferenceFragmentCompat() {
requirePreference<CheckBoxPreference>("pref_advanced_verbose_logging")
.bindBooleanFlow(preferenceRepository.verboseLoggingFlow)
requirePreference<CheckBoxPreference>("pref_developer_discovery_profile")
.bindBooleanFlow(preferenceRepository.discoveryProfileFlow)
requirePreference<CheckBoxPreference>("pref_developer_unfiltered_profile_list")
.bindBooleanFlow(preferenceRepository.unfilteredProfileListFlow)

View file

@ -21,6 +21,8 @@ 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.EuiccConfiguredAddresses
import net.typeblog.lpac_jni.LocalProfileAssistant
class DownloadWizardActivity: BaseEuiccAccessActivity() {
@ -36,6 +38,7 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
var downloadError: LocalProfileAssistant.ProfileDownloadException?,
var skipMethodSelect: Boolean,
var confirmationCodeRequired: Boolean,
var configuredAddresses: EuiccConfiguredAddresses?
)
private lateinit var state: DownloadWizardState
@ -75,6 +78,7 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
downloadError = null,
skipMethodSelect = false,
confirmationCodeRequired = false,
configuredAddresses = null,
)
handleDeepLink()

View file

@ -11,6 +11,8 @@ import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
@ -21,21 +23,20 @@ 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() {
data class DownloadMethod(
val iconRes: Int,
val titleRes: Int,
val onClick: () -> Unit
)
companion object {
const val TAG = "DownloadWizardMethodSelectFragment"
}
// TODO: Maybe we should find a better barcode scanner (or an external one?)
private val barcodeScannerLauncher = registerForActivityResult(ScanContract()) { result ->
result.contents?.let { content ->
processLpaString(content)
}
private val barcodeScannerLauncher =
registerForActivityResult(ScanContract()) {
it.contents?.let(::processLPAString)
}
private val gallerySelectorLauncher =
@ -51,16 +52,16 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
}
}
decoded.getOrNull()?.let { processLpaString(it) }
decoded.getOrNull()?.let { processLPAString(it) }
}
}
val downloadMethods = arrayOf(
private fun getDownloadMethods() = arrayOf(
DownloadMethod(R.drawable.ic_scan_black, R.string.download_wizard_method_qr_code) {
barcodeScannerLauncher.launch(ScanOptions().apply {
setDesiredBarcodeFormats(ScanOptions.QR_CODE)
setOrientationLocked(false)
})
val options = ScanOptions()
.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
.setOrientationLocked(false)
barcodeScannerLauncher.launch(options)
},
DownloadMethod(R.drawable.ic_gallery_black, R.string.download_wizard_method_gallery) {
gallerySelectorLauncher.launch("image/*")
@ -68,13 +69,17 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
DownloadMethod(R.drawable.ic_paste_go, R.string.download_wizard_method_clipboard) {
handleLoadFromClipboard()
},
DownloadMethod(R.drawable.ic_search, R.string.download_wizard_method_discovery, isDiscoverable()) {
gotoNextFragment(DownloadWizardDetailsFragment())
},
DownloadMethod(R.drawable.ic_edit, R.string.download_wizard_method_manual) {
gotoNextFragment(DownloadWizardDetailsFragment())
}
},
)
override val hasNext: Boolean
get() = false
override val hasPrev: Boolean
get() = true
@ -88,19 +93,13 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_download_method_select, container, false)
val recyclerView = view.requireViewById<RecyclerView>(R.id.download_method_list)
recyclerView.adapter = DownloadMethodAdapter()
recyclerView.layoutManager =
LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false)
recyclerView.addItemDecoration(
DividerItemDecoration(
requireContext(),
LinearLayoutManager.VERTICAL
)
)
return view
): View =
inflater.inflate(R.layout.fragment_download_method_select, container, false).apply {
requireViewById<RecyclerView>(R.id.download_method_list).apply {
adapter = DownloadMethodAdapter(getDownloadMethods())
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
addItemDecoration(DividerItemDecoration(context, LinearLayoutManager.VERTICAL))
}
}
private fun handleLoadFromClipboard() {
@ -108,18 +107,15 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
val text = clipboard.primaryClip?.getItemAt(0)?.text
if (text == null) {
Toast.makeText(
requireContext(),
R.string.profile_download_no_lpa_string,
Toast.LENGTH_SHORT
).show()
val resId = R.string.profile_download_no_lpa_string
Toast.makeText(requireContext(), resId, Toast.LENGTH_SHORT).show()
return
}
processLpaString(text.toString())
processLPAString(text.toString())
}
private fun processLpaString(input: String) {
private fun processLPAString(input: String) {
try {
val parsed = LPAString.parse(input)
state.smdp = parsed.address
@ -137,37 +133,46 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
}
}
private inner class DownloadMethodViewHolder(private val root: View) : ViewHolder(root) {
private val icon = root.requireViewById<ImageView>(R.id.download_method_icon)
private val title = root.requireViewById<TextView>(R.id.download_method_title)
private fun isDiscoverable(): Boolean {
val discoverable = runBlocking { preferenceRepository.discoveryProfileFlow.first() }
if (!discoverable) return false
return state.configuredAddresses?.discoverable ?: false
}
}
private data class DownloadMethod(
@DrawableRes
val iconRes: Int,
@StringRes
val titleRes: Int,
val isEnabled: Boolean = true,
val onClick: () -> Unit,
)
private class DownloadMethodViewHolder(private val root: View) : ViewHolder(root) {
private val icon: ImageView = root.requireViewById(R.id.download_method_icon)
private val title: TextView = root.requireViewById(R.id.download_method_title)
fun bind(item: DownloadMethod) {
icon.setImageResource(item.iconRes)
title.setText(item.titleRes)
root.setOnClickListener {
// If the user elected to use another download method, reset the confirmation code flag
// too
state.confirmationCodeRequired = false
item.onClick()
}
root.setOnClickListener { item.onClick() }
}
}
private inner class DownloadMethodAdapter : RecyclerView.Adapter<DownloadMethodViewHolder>() {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): DownloadMethodViewHolder {
val view = LayoutInflater.from(parent.context)
private class DownloadMethodAdapter(methods: Array<DownloadMethod>) :
RecyclerView.Adapter<DownloadMethodViewHolder>() {
private val methods = methods.filter(DownloadMethod::isEnabled)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
LayoutInflater.from(parent.context)
.inflate(R.layout.download_method_item, parent, false)
return DownloadMethodViewHolder(view)
}
.let { DownloadMethodViewHolder(it) }
override fun getItemCount(): Int = downloadMethods.size
override fun getItemCount(): Int = methods.size
override fun onBindViewHolder(holder: DownloadMethodViewHolder, position: Int) {
holder.bind(downloadMethods[position])
}
holder.bind(methods[position])
}
}

View file

@ -19,7 +19,7 @@ import im.angry.openeuicc.util.*
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import net.typeblog.lpac_jni.LocalProfileInfo
import net.typeblog.lpac_jni.EuiccConfiguredAddresses
class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardStepFragment() {
companion object {
@ -37,6 +37,7 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
val imei: String,
val enabledProfileName: String?,
val intrinsicChannelName: String?,
val configuredAddresses: EuiccConfiguredAddresses?,
)
private var loaded = false
@ -117,6 +118,7 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
},
channel.lpa.profiles.enabled?.displayName,
channel.intrinsicChannelName,
channel.lpa.getEuiccConfiguredAddresses(),
)
}
}.toList().sortedBy { it.logicalSlotId }
@ -129,6 +131,7 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
} else {
if (slots.isNotEmpty()) {
state.selectedLogicalSlot = slots[0].logicalSlotId
state.configuredAddresses = slots[0].configuredAddresses
}
0
}
@ -168,8 +171,11 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
adapter.notifyItemChanged(lastIdx)
adapter.notifyItemChanged(curIdx)
// Selected index isn't logical slot ID directly, needs a conversion
state.selectedLogicalSlot = adapter.slots[adapter.currentSelectedIdx].logicalSlotId
state.imei = adapter.slots[adapter.currentSelectedIdx].imei
adapter.slots[adapter.currentSelectedIdx].let {
state.selectedLogicalSlot = it.logicalSlotId
state.configuredAddresses = it.configuredAddresses
state.imei = it.imei
}
}
fun bind(item: SlotInfo, idx: Int) {

View file

@ -33,6 +33,7 @@ internal object PreferenceKeys {
val DEVELOPER_OPTIONS_ENABLED = booleanPreferencesKey("developer_options_enabled")
val UNFILTERED_PROFILE_LIST = booleanPreferencesKey("unfiltered_profile_list")
val IGNORE_TLS_CERTIFICATE = booleanPreferencesKey("ignore_tls_certificate")
val DISCOVERY_PROFILE = booleanPreferencesKey("discovery")
val EUICC_MEMORY_RESET = booleanPreferencesKey("euicc_memory_reset")
}
@ -51,6 +52,7 @@ open class PreferenceRepository(private val context: Context) {
val developerOptionsEnabledFlow = bindFlow(PreferenceKeys.DEVELOPER_OPTIONS_ENABLED, false)
val unfilteredProfileListFlow = bindFlow(PreferenceKeys.UNFILTERED_PROFILE_LIST, false)
val ignoreTLSCertificateFlow = bindFlow(PreferenceKeys.IGNORE_TLS_CERTIFICATE, false)
val discoveryProfileFlow = bindFlow(PreferenceKeys.DISCOVERY_PROFILE, false)
val euiccMemoryResetFlow = bindFlow(PreferenceKeys.EUICC_MEMORY_RESET, false)
protected fun <T> bindFlow(key: Preferences.Key<T>, defaultValue: T): PreferenceFlowWrapper<T> =

View file

@ -93,13 +93,12 @@ inline fun <T> Bitmap.use(f: (Bitmap) -> T): T =
recycle()
}
fun decodeQrFromBitmap(bmp: Bitmap): String? =
runCatching {
fun decodeQrFromBitmap(bmp: Bitmap): String? {
val pixels = IntArray(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 binaryBmp = BinaryBitmap(HybridBinarizer(luminanceSource))
QRCodeReader().decode(binaryBmp).text
}.getOrNull()
return QRCodeReader().decode(binaryBmp).text
}

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">
<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 xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
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>

View file

@ -1,5 +1,10 @@
<vector android:height="24dp" android:tint="?attr/colorControlNormal"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<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 xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
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>

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">
<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 xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
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>

View file

@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
android:viewportHeight="24">
<path
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" />

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_clipboard">Load from Clipboard</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_progress">Downloading your eSIM…</string>
<string name="download_wizard_progress_step_preparing">Preparing</string>
@ -181,6 +182,8 @@
<string name="pref_advanced_logs">Logs</string>
<string name="pref_advanced_logs_desc">View recent debug logs of the application</string>
<string name="pref_developer">Developer Options</string>
<string name="pref_developer_discovery_profile">Discovery Profile</string>
<string name="pref_developer_discovery_profile_desc">Discover new profile and install them via SM-DS</string>
<string name="pref_developer_unfiltered_profile_list">Show unfiltered profile list</string>
<string name="pref_developer_unfiltered_profile_list_desc">Include non-production profiles in the list</string>
<string name="pref_developer_ignore_tls_certificate">Ignore SM-DP+ TLS certificate</string>

View file

@ -57,6 +57,12 @@
app:title="@string/pref_developer"
app:iconSpaceReserved="false">
<CheckBoxPreference
app:iconSpaceReserved="false"
app:key="pref_developer_discovery_profile"
app:summary="@string/pref_developer_discovery_profile_desc"
app:title="@string/pref_developer_discovery_profile" />
<CheckBoxPreference
app:iconSpaceReserved="false"
app:key="pref_developer_unfiltered_profile_list"

View file

@ -8,7 +8,8 @@ import im.angry.openeuicc.util.*
import kotlinx.coroutines.flow.first
import java.lang.IllegalArgumentException
class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFactory(context) {
class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFactory(context),
PrivilegedEuiccContextMarker {
private val tm by lazy {
(context.applicationContext as OpenEuiccApplication).appContainer.telephonyManager
}
@ -22,7 +23,7 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto
super.tryOpenEuiccChannel(port)?.let { return it }
}
if (port.card.isEuicc || (context.preferenceRepository as PrivilegedPreferenceRepository).removableTelephonyManagerFlow.first()) {
if (port.card.isEuicc || preferenceRepository.removableTelephonyManagerFlow.first()) {
Log.i(
DefaultEuiccChannelManager.TAG,
"Trying TelephonyManager for slot ${port.card.physicalSlotIndex} port ${port.portIndex}"

View file

@ -6,7 +6,7 @@ import androidx.preference.Preference
import im.angry.openeuicc.R
import im.angry.openeuicc.util.*
class PrivilegedSettingsFragment : SettingsFragment() {
class PrivilegedSettingsFragment : SettingsFragment(), PrivilegedEuiccContextMarker {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
super.onCreatePreferences(savedInstanceState, rootKey)
addPreferencesFromResource(R.xml.pref_privileged_settings)
@ -21,7 +21,7 @@ class PrivilegedSettingsFragment : SettingsFragment() {
requirePreference<Preference>("pref_advanced_language").isVisible = false
// Force use TelephonyManager API
requirePreference<CheckBoxPreference>("pref_developer_tmapi_removable")
.bindBooleanFlow((preferenceRepository as PrivilegedPreferenceRepository).removableTelephonyManagerFlow)
requirePreference<CheckBoxPreference>("pref_developer_removable_telephony_manager")
.bindBooleanFlow(preferenceRepository.removableTelephonyManagerFlow)
}
}

View file

@ -5,10 +5,23 @@ import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import androidx.fragment.app.Fragment
import java.util.concurrent.Executors
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
interface PrivilegedEuiccContextMarker {
val privilegedEuiccMarkerContext: Context
get() = when (this) {
is Context -> this
is Fragment -> requireContext()
else -> throw RuntimeException("PrivilegedEuiccContextMarker shall only be used on Fragments or UI types that derive from Context")
}
val preferenceRepository: PrivilegedPreferenceRepository
get() = privilegedEuiccMarkerContext.preferenceRepository as PrivilegedPreferenceRepository
}
suspend fun Context.bindServiceSuspended(intent: Intent, flags: Int): Pair<IBinder?, () -> Unit> =
suspendCoroutine { cont ->
var binder: IBinder?

View file

@ -5,7 +5,7 @@
app:key="pref_developer_overlay">
<CheckBoxPreference
app:iconSpaceReserved="false"
app:key="pref_developer_tmapi_removable"
app:key="pref_developer_removable_telephony_manager"
app:summary="@string/pref_developer_telephony_manager_removable_desc"
app:title="@string/pref_developer_telephony_manager_removable" />
</PreferenceCategory>

View file

@ -0,0 +1,29 @@
package net.typeblog.lpac_jni
import android.util.Patterns
@Suppress("SpellCheckingInspection")
val invalidDPAddresses = setOf(
"testrootsmds.example.com",
"testrootsmds.gsma.com",
)
class EuiccConfiguredAddresses(defaultDPAddress: String?, rootDSAddress: String?) {
val defaultDPAddress: String? = defaultDPAddress.takeUnless(::isInvalidDPAddress)
val rootDSAddress = rootDSAddress.takeUnless(::isInvalidDSAddress)
val discoverable: Boolean
get() = !defaultDPAddress.isNullOrBlank() || !rootDSAddress.isNullOrBlank()
}
private fun isInvalidDPAddress(address: String?): Boolean {
if (address.isNullOrBlank()) return true
return !Patterns.DOMAIN_NAME.matcher(address).matches()
}
private fun isInvalidDSAddress(address: String?): Boolean {
if (address.isNullOrBlank()) return true
if (address in invalidDPAddresses) return true
if (Patterns.DOMAIN_NAME.matcher(address).matches()) return false
return false
}

View file

@ -12,6 +12,15 @@ interface LocalProfileAssistant {
val lastApduException: Exception?,
) : 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 ProfileNameTooLongException() : Exception("Profile name too long")
class ProfileNameIsInvalidUTF8Exception() : Exception("Profile name is invalid UTF-8")
@ -30,6 +39,9 @@ interface LocalProfileAssistant {
*/
fun setEs10xMss(mss: Byte)
// es10a
fun getEuiccConfiguredAddresses(): EuiccConfiguredAddresses
// 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.
fun enableProfile(iccid: String, refresh: Boolean = true): Boolean
@ -38,6 +50,7 @@ interface LocalProfileAssistant {
fun downloadProfile(smdp: String, matchingId: String?, imei: String?,
confirmationCode: String?, callback: ProfileDownloadCallback)
fun discoveryProfile(smds: String, imei: String?, callback: ProfileDiscoveryCallback)
fun deleteNotification(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 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
// We do not expose all of the functions because of tediousness :)
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,8 +2,7 @@ package net.typeblog.lpac_jni
interface ProfileDownloadCallback {
companion object {
fun lookupStateFromProgress(progress: Int): DownloadState =
when (progress) {
fun lookupStateFromProgress(progress: Int): DownloadState = when (progress) {
0 -> DownloadState.Preparing
20 -> DownloadState.Connecting
40 -> DownloadState.Authenticating

View file

@ -9,6 +9,7 @@ import net.typeblog.lpac_jni.HttpInterface.HttpResponse
import net.typeblog.lpac_jni.LocalProfileAssistant
import net.typeblog.lpac_jni.LocalProfileInfo
import net.typeblog.lpac_jni.LocalProfileNotification
import net.typeblog.lpac_jni.ProfileDiscoveryCallback
import net.typeblog.lpac_jni.ProfileDownloadCallback
import net.typeblog.lpac_jni.Version
@ -93,6 +94,9 @@ class LocalProfileAssistantImpl(
LpacJni.euiccSetMss(contextHandle, mss)
}
override fun getEuiccConfiguredAddresses() =
LpacJni.es10aGetEuiccConfiguredAddresses(contextHandle)
override val valid: Boolean
get() = !finalized && apduInterface.valid && try {
// If we can read both eID and euiccInfo2 properly, we are likely looking at
@ -212,7 +216,7 @@ class LocalProfileAssistantImpl(
callback
)
if (res != 0) {
if (res == 0) return
// Construct the error now to store any error information we _can_ access
val err = LocalProfileAssistant.ProfileDownloadException(
lpaErrorReason = LpacJni.downloadErrCodeToString(-res),
@ -227,6 +231,17 @@ class LocalProfileAssistantImpl(
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

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_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() {
LPAC_JNI_SETUP_ENV;
jclass download_state_class = (*env)->FindClass(env,
"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_state_class = (*env)->FindClass(env, DOWNLOAD_STATE_CLASS);
jclass download_callback_class = (*env)->FindClass(env,
"net/typeblog/lpac_jni/ProfileDownloadCallback");
on_state_update = (*env)->GetMethodID(env, download_callback_class, "onStateUpdate",
"(Lnet/typeblog/lpac_jni/ProfileDownloadCallback$DownloadState;)V");
BIND_DOWNLOAD_STATE_STATIC_FIELD(download_state_preparing, "Preparing");
BIND_DOWNLOAD_STATE_STATIC_FIELD(download_state_connecting, "Connecting");
BIND_DOWNLOAD_STATE_STATIC_FIELD(download_state_authenticating, "Authenticating");
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
Java_net_typeblog_lpac_1jni_LpacJni_downloadProfile(JNIEnv *env, jobject thiz, jlong handle,
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;
(*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);
syslog(LOG_INFO, "es10b_get_euicc_challenge_and_info %d", ret);
if (ret < 0) {
@ -87,7 +74,7 @@ Java_net_typeblog_lpac_1jni_LpacJni_downloadProfile(JNIEnv *env, jobject thiz, j
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);
syslog(LOG_INFO, "es9p_initiate_authentication %d", ret);
if (ret < 0) {
@ -95,7 +82,7 @@ Java_net_typeblog_lpac_1jni_LpacJni_downloadProfile(JNIEnv *env, jobject thiz, j
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);
syslog(LOG_INFO, "es10b_authenticate_server %d", ret);
if (ret < 0) {
@ -109,7 +96,7 @@ Java_net_typeblog_lpac_1jni_LpacJni_downloadProfile(JNIEnv *env, jobject thiz, j
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);
syslog(LOG_INFO, "es10b_prepare_download %d", ret);
if (ret < 0) {
@ -121,7 +108,7 @@ Java_net_typeblog_lpac_1jni_LpacJni_downloadProfile(JNIEnv *env, jobject thiz, j
if (ret < 0)
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);
syslog(LOG_INFO, "es10b_load_bound_profile_package %d, reason %d", ret, es10b_load_bound_profile_package_result.errorReason);
if (ret < 0) {

View file

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