diff --git a/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt b/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt index b715ca0..f19616d 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt @@ -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) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt index b085286..db371c6 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt @@ -72,6 +72,9 @@ open class SettingsFragment: PreferenceFragmentCompat() { requirePreference("pref_advanced_verbose_logging") .bindBooleanFlow(preferenceRepository.verboseLoggingFlow) + requirePreference("pref_developer_discovery_profile") + .bindBooleanFlow(preferenceRepository.discoveryProfileFlow) + requirePreference("pref_developer_unfiltered_profile_list") .bindBooleanFlow(preferenceRepository.unfilteredProfileListFlow) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt index a9f868f..0857e76 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt @@ -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() diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt index 4b02b7a..9722e05 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt @@ -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,22 +23,21 @@ 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 = registerForActivityResult(ActivityResultContracts.GetContent()) { result -> @@ -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,38 +93,29 @@ 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(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(R.id.download_method_list).apply { + adapter = DownloadMethodAdapter(getDownloadMethods()) + layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) + addItemDecoration(DividerItemDecoration(context, LinearLayoutManager.VERTICAL)) + } + } private fun handleLoadFromClipboard() { val clipboard = requireContext().getSystemService(ClipboardManager::class.java) 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(R.id.download_method_icon) - private val title = 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() - } - } + private fun isDiscoverable(): Boolean { + val discoverable = runBlocking { preferenceRepository.discoveryProfileFlow.first() } + if (!discoverable) return false + return state.configuredAddresses?.discoverable ?: false } +} - private inner class DownloadMethodAdapter : RecyclerView.Adapter() { - override fun onCreateViewHolder( - parent: ViewGroup, - viewType: Int - ): DownloadMethodViewHolder { - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.download_method_item, parent, false) - return DownloadMethodViewHolder(view) - } +private data class DownloadMethod( + @DrawableRes + val iconRes: Int, + @StringRes + val titleRes: Int, + val isEnabled: Boolean = true, + val onClick: () -> Unit, +) - override fun getItemCount(): Int = downloadMethods.size +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) - override fun onBindViewHolder(holder: DownloadMethodViewHolder, position: Int) { - holder.bind(downloadMethods[position]) - } + fun bind(item: DownloadMethod) { + icon.setImageResource(item.iconRes) + title.setText(item.titleRes) + root.setOnClickListener { item.onClick() } + } +} +private class DownloadMethodAdapter(methods: Array) : + RecyclerView.Adapter() { + + 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) + .let { DownloadMethodViewHolder(it) } + + override fun getItemCount(): Int = methods.size + + override fun onBindViewHolder(holder: DownloadMethodViewHolder, position: Int) { + holder.bind(methods[position]) } } \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt index 28bc9f0..80c43ad 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt @@ -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) { diff --git a/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt index 34d1cfd..c6386e3 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt @@ -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 bindFlow(key: Preferences.Key, defaultValue: T): PreferenceFlowWrapper = diff --git a/app-common/src/main/java/im/angry/openeuicc/util/Utils.kt b/app-common/src/main/java/im/angry/openeuicc/util/Utils.kt index 5a559f9..16fbd10 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/Utils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/Utils.kt @@ -93,13 +93,12 @@ inline fun Bitmap.use(f: (Bitmap) -> T): T = recycle() } -fun decodeQrFromBitmap(bmp: Bitmap): String? = - runCatching { - val pixels = IntArray(bmp.width * bmp.height) - bmp.getPixels(pixels, 0, bmp.width, 0, 0, bmp.width, bmp.height) +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)) + val luminanceSource = RGBLuminanceSource(bmp.width, bmp.height, pixels) + val binaryBmp = BinaryBitmap(HybridBinarizer(luminanceSource)) - QRCodeReader().decode(binaryBmp).text - }.getOrNull() + return QRCodeReader().decode(binaryBmp).text +} diff --git a/app-common/src/main/res/drawable/ic_edit.xml b/app-common/src/main/res/drawable/ic_edit.xml index 3c53db7..5fb90ad 100644 --- a/app-common/src/main/res/drawable/ic_edit.xml +++ b/app-common/src/main/res/drawable/ic_edit.xml @@ -1,5 +1,10 @@ - - - - + + diff --git a/app-common/src/main/res/drawable/ic_gallery_black.xml b/app-common/src/main/res/drawable/ic_gallery_black.xml index 048f74a..127e52d 100644 --- a/app-common/src/main/res/drawable/ic_gallery_black.xml +++ b/app-common/src/main/res/drawable/ic_gallery_black.xml @@ -1,5 +1,10 @@ - - + + diff --git a/app-common/src/main/res/drawable/ic_paste_go.xml b/app-common/src/main/res/drawable/ic_paste_go.xml index 7536fff..2cfa65e 100644 --- a/app-common/src/main/res/drawable/ic_paste_go.xml +++ b/app-common/src/main/res/drawable/ic_paste_go.xml @@ -1,7 +1,13 @@ - - - - - - + + + diff --git a/app-common/src/main/res/drawable/ic_scan_black.xml b/app-common/src/main/res/drawable/ic_scan_black.xml index 597e8d7..3d6820d 100644 --- a/app-common/src/main/res/drawable/ic_scan_black.xml +++ b/app-common/src/main/res/drawable/ic_scan_black.xml @@ -1,10 +1,10 @@ - + android:viewportHeight="24"> + diff --git a/app-common/src/main/res/drawable/ic_search.xml b/app-common/src/main/res/drawable/ic_search.xml new file mode 100644 index 0000000..6dfd2e1 --- /dev/null +++ b/app-common/src/main/res/drawable/ic_search.xml @@ -0,0 +1,10 @@ + + + diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index cc84381..563387d 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -83,6 +83,7 @@ Load a QR code from gallery Load from Clipboard Enter manually + Discovery Input or confirm details for downloading your eSIM: Downloading your eSIM… Preparing @@ -181,6 +182,8 @@ Logs View recent debug logs of the application Developer Options + Discovery Profile + Discover new profile and install them via SM-DS Show unfiltered profile list Include non-production profiles in the list Ignore SM-DP+ TLS certificate diff --git a/app-common/src/main/res/xml/pref_settings.xml b/app-common/src/main/res/xml/pref_settings.xml index 7d25118..7dcb21a 100644 --- a/app-common/src/main/res/xml/pref_settings.xml +++ b/app-common/src/main/res/xml/pref_settings.xml @@ -57,6 +57,12 @@ app:title="@string/pref_developer" app:iconSpaceReserved="false"> + + ("pref_advanced_language").isVisible = false // Force use TelephonyManager API - requirePreference("pref_developer_tmapi_removable") - .bindBooleanFlow((preferenceRepository as PrivilegedPreferenceRepository).removableTelephonyManagerFlow) + requirePreference("pref_developer_removable_telephony_manager") + .bindBooleanFlow(preferenceRepository.removableTelephonyManagerFlow) } } \ No newline at end of file diff --git a/app/src/main/java/im/angry/openeuicc/util/PrivilegedUtils.kt b/app/src/main/java/im/angry/openeuicc/util/PrivilegedUtils.kt index e295f26..21c8002 100644 --- a/app/src/main/java/im/angry/openeuicc/util/PrivilegedUtils.kt +++ b/app/src/main/java/im/angry/openeuicc/util/PrivilegedUtils.kt @@ -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 Unit> = suspendCoroutine { cont -> var binder: IBinder? diff --git a/app/src/main/res/xml/pref_privileged_settings.xml b/app/src/main/res/xml/pref_privileged_settings.xml index 339233b..5279126 100644 --- a/app/src/main/res/xml/pref_privileged_settings.xml +++ b/app/src/main/res/xml/pref_privileged_settings.xml @@ -5,7 +5,7 @@ app:key="pref_developer_overlay"> diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/EuiccConfiguredAddresses.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/EuiccConfiguredAddresses.kt new file mode 100644 index 0000000..437a0a3 --- /dev/null +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/EuiccConfiguredAddresses.kt @@ -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 +} diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt index 48ab1c5..d98c241 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt @@ -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 diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt index fa9474f..bf57f63 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt @@ -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?, diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/ProfileDiscoveryCallback.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/ProfileDiscoveryCallback.kt new file mode 100644 index 0000000..e7b7196 --- /dev/null +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/ProfileDiscoveryCallback.kt @@ -0,0 +1,5 @@ +package net.typeblog.lpac_jni + +interface ProfileDiscoveryCallback { + fun onDiscovered(hosts: Array) +} diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/ProfileDownloadCallback.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/ProfileDownloadCallback.kt index 289ddf6..2c9cbb6 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/ProfileDownloadCallback.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/ProfileDownloadCallback.kt @@ -2,15 +2,14 @@ package net.typeblog.lpac_jni interface ProfileDownloadCallback { companion object { - fun lookupStateFromProgress(progress: Int): DownloadState = - when (progress) { - 0 -> DownloadState.Preparing - 20 -> DownloadState.Connecting - 40 -> DownloadState.Authenticating - 60 -> DownloadState.Downloading - 80 -> DownloadState.Finalizing - else -> throw IllegalArgumentException("Unknown state") - } + fun lookupStateFromProgress(progress: Int): DownloadState = when (progress) { + 0 -> DownloadState.Preparing + 20 -> DownloadState.Connecting + 40 -> DownloadState.Authenticating + 60 -> DownloadState.Downloading + 80 -> DownloadState.Finalizing + else -> throw IllegalArgumentException("Unknown state") + } } enum class DownloadState(val progress: Int) { diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt index 3674f4f..11a9931 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt @@ -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,21 +216,32 @@ class LocalProfileAssistantImpl( callback ) - if (res != 0) { - // Construct the error now to store any error information we _can_ access - val err = LocalProfileAssistant.ProfileDownloadException( - lpaErrorReason = LpacJni.downloadErrCodeToString(-res), - httpInterface.lastHttpResponse, - httpInterface.lastHttpException, - apduInterface.lastApduResponse, - apduInterface.lastApduException, - ) + if (res == 0) return + // Construct the error now to store any error information we _can_ access + val err = LocalProfileAssistant.ProfileDownloadException( + lpaErrorReason = LpacJni.downloadErrCodeToString(-res), + httpInterface.lastHttpResponse, + httpInterface.lastHttpException, + apduInterface.lastApduResponse, + apduInterface.lastApduException, + ) - // Cancel sessions if possible. This will overwrite recorded errors from HTTP and APDU interfaces. - LpacJni.cancelSessions(contextHandle) + // Cancel sessions if possible. This will overwrite recorded errors from HTTP and APDU interfaces. + 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 diff --git a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-discovery.c b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-discovery.c new file mode 100644 index 0000000..6a3f001 --- /dev/null +++ b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-discovery.c @@ -0,0 +1,127 @@ +#include "lpac-notifications.h" +#include +#include +#include +#include +#include +#include + +#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, "", + "(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; +} \ No newline at end of file diff --git a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-discovery.h b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-discovery.h new file mode 100644 index 0000000..cee05e1 --- /dev/null +++ b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-discovery.h @@ -0,0 +1,6 @@ +#pragma once + +#include +#include "lpac-jni.h" + +void lpac_discovery_init(); diff --git a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-download.c b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-download.c index bae2ee8..1ff6582 100644 --- a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-download.c +++ b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-download.c @@ -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) { diff --git a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c index ca319db..b144484 100644 --- a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c +++ b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c @@ -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");