Compare commits

...

8 commits

Author SHA1 Message Date
1140ddb249 lpac-jni: IgnoreTLSCertificate -> AllowAllTrustManager 2024-12-08 11:15:11 -05:00
9be1ae7cd1 lpac-jni: Expose error reason enum as string 2024-12-08 11:14:10 -05:00
a7e97378fc Rework EuiccInfoActivity formatting 2024-12-08 10:51:14 -05:00
790cbb5a58 i18n: Update ja translations 2024-12-08 10:42:32 -05:00
2247749b37 ui: Set layout_constrainedWidth for profile title 2024-12-08 10:35:03 -05:00
249aea482b refactor: euicc info activity (#98)
improve maintainability

Co-authored-by: Peter Cai <peter@typeblog.net>
Reviewed-on: PeterCxy/OpenEUICC#98
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2024-12-08 16:31:39 +01:00
dc0489a693 chore: simplify source code intent (#97)
Co-authored-by: Peter Cai <peter@typeblog.net>
Reviewed-on: PeterCxy/OpenEUICC#97
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2024-12-08 16:30:00 +01:00
d3e54ece58 chore: improve compatibility check (#96)
Reviewed-on: PeterCxy/OpenEUICC#96
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2024-12-08 16:29:05 +01:00
13 changed files with 116 additions and 112 deletions

View file

@ -15,12 +15,18 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ViewHolder import androidx.recyclerview.widget.RecyclerView.ViewHolder
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.typeblog.lpac_jni.impl.DEFAULT_PKID_GSMA_RSP2_ROOT_CI1 import net.typeblog.lpac_jni.impl.DEFAULT_PKID_GSMA_RSP2_ROOT_CI1
import net.typeblog.lpac_jni.impl.PKID_GSMA_TEST_CI import net.typeblog.lpac_jni.impl.PKID_GSMA_TEST_CI
class EuiccInfoActivity : BaseEuiccAccessActivity() { class EuiccInfoActivity : BaseEuiccAccessActivity() {
companion object {
private val YES_NO = Pair(R.string.yes, R.string.no)
private val SUPPORTED_UNSUPPORTED = Pair(R.string.supported, R.string.unsupported)
}
private lateinit var swipeRefresh: SwipeRefreshLayout private lateinit var swipeRefresh: SwipeRefreshLayout
private lateinit var infoList: RecyclerView private lateinit var infoList: RecyclerView
@ -71,104 +77,59 @@ class EuiccInfoActivity : BaseEuiccAccessActivity() {
swipeRefresh.isRefreshing = true swipeRefresh.isRefreshing = true
lifecycleScope.launch { lifecycleScope.launch {
val unknownStr = getString(R.string.unknown) (infoList.adapter!! as EuiccInfoAdapter).euiccInfoItems =
euiccChannelManager.withEuiccChannel(logicalSlotId, ::buildPairs).map {
val newItems = mutableListOf<Pair<String, String>>() Pair(getString(it.first), it.second ?: getString(R.string.unknown))
newItems.add(
Pair(
getString(R.string.euicc_info_access_mode),
euiccChannelManager.withEuiccChannel(logicalSlotId) { channel -> channel.type }
)
)
newItems.add(
Pair(
getString(R.string.euicc_info_removable),
if (euiccChannelManager.withEuiccChannel(logicalSlotId) { channel -> channel.port.card.isRemovable }) {
getString(R.string.yes)
} else {
getString(R.string.no)
} }
)
)
newItems.add(
Pair(
getString(R.string.euicc_info_eid),
euiccChannelManager.withEuiccChannel(logicalSlotId) { channel -> channel.lpa.eID }
)
)
val euiccInfo2 = euiccChannelManager.withEuiccChannel(logicalSlotId) { channel ->
channel.lpa.euiccInfo2
}
newItems.add(
Pair(
getString(R.string.euicc_info_firmware_version),
euiccInfo2?.euiccFirmwareVersion ?: unknownStr
)
)
newItems.add(
Pair(
getString(R.string.euicc_info_globalplatform_version),
euiccInfo2?.globalPlatformVersion ?: unknownStr
)
)
newItems.add(
Pair(
getString(R.string.euicc_info_pp_version),
euiccInfo2?.ppVersion ?: unknownStr
)
)
newItems.add(
Pair(
getString(R.string.euicc_info_sas_accreditation_number),
euiccInfo2?.sasAccreditationNumber ?: unknownStr
)
)
newItems.add(
Pair(
getString(R.string.euicc_info_free_nvram),
euiccInfo2?.freeNvram?.let { formatFreeSpace(it) } ?: unknownStr
))
newItems.add(
Pair(
getString(R.string.euicc_info_gsma_prod),
if (euiccInfo2?.euiccCiPKIdListForSigning?.contains(
DEFAULT_PKID_GSMA_RSP2_ROOT_CI1
) == true
) {
getString(R.string.supported)
} else {
getString(R.string.unsupported)
}
)
)
newItems.add(
Pair(
getString(R.string.euicc_info_gsma_test),
if (PKID_GSMA_TEST_CI.any { euiccInfo2?.euiccCiPKIdListForSigning?.contains(it) == true }) {
getString(R.string.supported)
} else {
getString(R.string.unsupported)
}
)
)
(infoList.adapter!! as EuiccInfoAdapter).euiccInfoItems = newItems
swipeRefresh.isRefreshing = false swipeRefresh.isRefreshing = false
} }
} }
private fun buildPairs(channel: EuiccChannel) = buildList {
add(Pair(R.string.euicc_info_access_mode, channel.type))
add(
Pair(
R.string.euicc_info_removable,
formatByBoolean(channel.port.card.isRemovable, YES_NO)
)
)
add(Pair(R.string.euicc_info_eid, channel.lpa.eID))
channel.lpa.euiccInfo2.let { info ->
add(Pair(R.string.euicc_info_firmware_version, info?.euiccFirmwareVersion))
add(Pair(R.string.euicc_info_globalplatform_version, info?.globalPlatformVersion))
add(Pair(R.string.euicc_info_pp_version, info?.ppVersion))
add(Pair(R.string.euicc_info_sas_accreditation_number, info?.sasAccreditationNumber))
add(Pair(R.string.euicc_info_free_nvram, info?.freeNvram?.let(::formatFreeSpace)))
}
channel.lpa.euiccInfo2?.euiccCiPKIdListForSigning.orEmpty().let { signers ->
add(
Pair(
R.string.euicc_info_gsma_prod,
formatByBoolean(
signers.contains(DEFAULT_PKID_GSMA_RSP2_ROOT_CI1),
SUPPORTED_UNSUPPORTED
)
)
)
add(
Pair(
R.string.euicc_info_gsma_test,
formatByBoolean(PKID_GSMA_TEST_CI.any(signers::contains), SUPPORTED_UNSUPPORTED)
)
)
}
}
private fun formatByBoolean(b: Boolean, res: Pair<Int, Int>): String =
getString(
if (b) {
res.first
} else {
res.second
}
)
inner class EuiccInfoViewHolder(root: View) : ViewHolder(root) { inner class EuiccInfoViewHolder(root: View) : ViewHolder(root) {
private val title: TextView = root.requireViewById(R.id.euicc_info_title) private val title: TextView = root.requireViewById(R.id.euicc_info_title)
private val content: TextView = root.requireViewById(R.id.euicc_info_content) private val content: TextView = root.requireViewById(R.id.euicc_info_content)

View file

@ -47,10 +47,6 @@ class SettingsFragment: PreferenceFragmentCompat() {
setOnPreferenceClickListener(::onAppVersionClicked) setOnPreferenceClickListener(::onAppVersionClicked)
} }
findPreference<Preference>("pref_info_source_code")?.apply {
intent = Intent(Intent.ACTION_VIEW, Uri.parse(summary.toString()))
}
findPreference<Preference>("pref_language")?.apply { findPreference<Preference>("pref_language")?.apply {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return@apply if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return@apply
isVisible = true isVisible = true

View file

@ -28,7 +28,8 @@
app:layout_constraintRight_toLeftOf="@+id/profile_menu" app:layout_constraintRight_toLeftOf="@+id/profile_menu"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/state" app:layout_constraintBottom_toTopOf="@+id/state"
app:layout_constraintHorizontal_bias="0" /> app:layout_constraintHorizontal_bias="0"
app:layout_constrainedWidth="true" />
<androidx.appcompat.widget.AppCompatImageButton <androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/profile_menu" android:id="@+id/profile_menu"

View file

@ -136,4 +136,8 @@
<string name="pref_language_desc">アプリの言語を選択</string> <string name="pref_language_desc">アプリの言語を選択</string>
<string name="pref_developer_unfiltered_profile_list">すべてのプロファイルを表示</string> <string name="pref_developer_unfiltered_profile_list">すべてのプロファイルを表示</string>
<string name="pref_developer_unfiltered_profile_list_desc">プロダクション以外のプロファイルも表示する</string> <string name="pref_developer_unfiltered_profile_list_desc">プロダクション以外のプロファイルも表示する</string>
<string name="profile_class">タイプ:</string>
<string name="profile_class_testing">テスティング</string>
<string name="profile_class_provisioning">準備中</string>
<string name="profile_class_operational">動作中</string>
</resources> </resources>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"> <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<im.angry.openeuicc.ui.preference.LongSummaryPreferenceCategory <im.angry.openeuicc.ui.preference.LongSummaryPreferenceCategory
app:title="@string/pref_notifications" app:title="@string/pref_notifications"
app:summary="@string/pref_notifications_desc" app:summary="@string/pref_notifications_desc"
@ -88,6 +89,10 @@
app:iconSpaceReserved="false" app:iconSpaceReserved="false"
app:title="@string/pref_info_source_code" app:title="@string/pref_info_source_code"
app:summary="@string/pref_info_source_code_url" app:summary="@string/pref_info_source_code_url"
app:key="pref_info_source_code"/> app:key="pref_info_source_code">
<intent
android:action="android.intent.action.VIEW"
android:data="@string/pref_info_source_code_url" />
</Preference>
</PreferenceCategory> </PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>

View file

@ -1,6 +1,8 @@
package im.angry.openeuicc.ui package im.angry.openeuicc.ui
import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.text.Html
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -8,6 +10,7 @@ import android.widget.TextView
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.isVisible
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
@ -29,15 +32,16 @@ class CompatibilityCheckActivity: AppCompatActivity() {
setupToolbarInsets() setupToolbarInsets()
supportActionBar!!.setDisplayHomeAsUpEnabled(true) supportActionBar!!.setDisplayHomeAsUpEnabled(true)
compatibilityCheckList = requireViewById(R.id.recycler_view) compatibilityCheckList = requireViewById<RecyclerView>(R.id.recycler_view).also {
compatibilityCheckList.layoutManager = it.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) it.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
compatibilityCheckList.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL)) it.adapter = adapter
compatibilityCheckList.adapter = adapter }
setupRootViewInsets(compatibilityCheckList) setupRootViewInsets(compatibilityCheckList)
} }
@SuppressLint("NotifyDataSetChanged")
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
lifecycleScope.launch { lifecycleScope.launch {
@ -61,10 +65,10 @@ class CompatibilityCheckActivity: AppCompatActivity() {
fun bindItem(item: CompatibilityCheck) { fun bindItem(item: CompatibilityCheck) {
titleView.text = item.title titleView.text = item.title
descView.text = item.description descView.text = Html.fromHtml(item.description, Html.FROM_HTML_MODE_COMPACT)
statusContainer.children.forEach { statusContainer.children.forEach {
it.visibility = View.GONE it.isVisible = false
} }
val viewId = when (item.state) { val viewId = when (item.state) {
@ -73,7 +77,7 @@ class CompatibilityCheckActivity: AppCompatActivity() {
CompatibilityCheck.State.FAILURE_UNKNOWN -> R.id.compatibility_check_unknown CompatibilityCheck.State.FAILURE_UNKNOWN -> R.id.compatibility_check_unknown
else -> R.id.compatibility_check_progress_bar else -> R.id.compatibility_check_progress_bar
} }
root.requireViewById<View>(viewId).visibility = View.VISIBLE root.requireViewById<View>(viewId).isVisible = true
} }
} }

View file

@ -12,11 +12,11 @@
<string name="compatibility_check_omapi_connectivity">OMAPI Connectivity</string> <string name="compatibility_check_omapi_connectivity">OMAPI Connectivity</string>
<string name="compatibility_check_omapi_connectivity_desc">Does your device allow access to Secure Elements on SIM cards via OMAPI?</string> <string name="compatibility_check_omapi_connectivity_desc">Does your device allow access to Secure Elements on SIM cards via OMAPI?</string>
<string name="compatibility_check_omapi_connectivity_fail">Unable to detect Secure Element readers for SIM cards via OMAPI. If you have not inserted a SIM in this device, try inserting one and retry this check.</string> <string name="compatibility_check_omapi_connectivity_fail">Unable to detect Secure Element readers for SIM cards via OMAPI. If you have not inserted a SIM in this device, try inserting one and retry this check.</string>
<string name="compatibility_check_omapi_connectivity_partial_success_sim_number">Successfully detected Secure Element access, but only for the following SIM slots: <b>SIM%s</b>.</string> <string name="compatibility_check_omapi_connectivity_partial_success_sim_number">Successfully detected Secure Element access, but only for the following SIM slots: &lt;b&gt;SIM%s&lt;/b&gt;.</string>
<string name="compatibility_check_isdr_channel">ISD-R Channel Access</string> <string name="compatibility_check_isdr_channel">ISD-R Channel Access</string>
<string name="compatibility_check_isdr_channel_desc">Does your device support opening an ISD-R (management) channel to eSIMs via OMAPI?</string> <string name="compatibility_check_isdr_channel_desc">Does your device support opening an ISD-R (management) channel to eSIMs via OMAPI?</string>
<string name="compatibility_check_isdr_channel_desc_unknown">Cannot determine whether ISD-R access through OMAPI is supported. You might want to retry with SIM cards inserted (any SIM card will do) if not already.</string> <string name="compatibility_check_isdr_channel_desc_unknown">Cannot determine whether ISD-R access through OMAPI is supported. You might want to retry with SIM cards inserted (any SIM card will do) if not already.</string>
<string name="compatibility_check_isdr_channel_desc_partial_fail">OMAPI access to ISD-R is only possible on the following SIM slots: <b>SIM%s</b>.</string> <string name="compatibility_check_isdr_channel_desc_partial_fail">OMAPI access to ISD-R is only possible on the following SIM slots: &lt;b&gt;SIM%s&lt;/b&gt;.</string>
<string name="compatibility_check_known_broken">Not on the Known Broken List</string> <string name="compatibility_check_known_broken">Not on the Known Broken List</string>
<string name="compatibility_check_known_broken_desc">Making sure your device is not known to have bugs associated with removable eSIMs.</string> <string name="compatibility_check_known_broken_desc">Making sure your device is not known to have bugs associated with removable eSIMs.</string>
<string name="compatibility_check_known_broken_fail">Oops, your device is known to have bugs when accessing removable eSIMs. This does not necessarily mean that it will not work at all, but you will have to proceed with caution.</string> <string name="compatibility_check_known_broken_fail">Oops, your device is known to have bugs when accessing removable eSIMs. This does not necessarily mean that it will not work at all, but you will have to proceed with caution.</string>

View file

@ -5,6 +5,7 @@ import net.typeblog.lpac_jni.HttpInterface.HttpResponse
interface LocalProfileAssistant { interface LocalProfileAssistant {
@Suppress("ArrayInDataClass") @Suppress("ArrayInDataClass")
data class ProfileDownloadException( data class ProfileDownloadException(
val lpaErrorReason: String,
val lastHttpResponse: HttpResponse?, val lastHttpResponse: HttpResponse?,
val lastHttpException: Exception?, val lastHttpException: Exception?,
val lastApduResponse: ByteArray?, val lastApduResponse: ByteArray?,

View file

@ -29,6 +29,7 @@ internal object LpacJni {
// 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?,
confirmationCode: String?, callback: ProfileDownloadCallback): Int confirmationCode: String?, callback: ProfileDownloadCallback): Int
external fun downloadErrCodeToString(code: Int): String
external fun handleNotification(handle: Long, seqNumber: Long): Int external fun handleNotification(handle: Long, seqNumber: Long): Int
// Cancel any ongoing es9p and/or es10b sessions // Cancel any ongoing es9p and/or es10b sessions
external fun cancelSessions(handle: Long) external fun cancelSessions(handle: Long)

View file

@ -5,7 +5,7 @@ import java.security.cert.X509Certificate
import javax.net.ssl.X509TrustManager import javax.net.ssl.X509TrustManager
@SuppressLint("CustomX509TrustManager") @SuppressLint("CustomX509TrustManager")
class IgnoreTLSCertificate : X509TrustManager { class AllowAllTrustManager : X509TrustManager {
@SuppressLint("TrustAllX509TrustManager") @SuppressLint("TrustAllX509TrustManager")
override fun checkClientTrusted(p0: Array<out X509Certificate>?, p1: String?) { override fun checkClientTrusted(p0: Array<out X509Certificate>?, p1: String?) {
return return

View file

@ -83,7 +83,7 @@ class HttpInterfaceImpl(
private fun getSocketFactory(): SSLSocketFactory { private fun getSocketFactory(): SSLSocketFactory {
val trustManagers = val trustManagers =
if (runBlocking { ignoreTLSCertificateFlow.first() }) { if (runBlocking { ignoreTLSCertificateFlow.first() }) {
arrayOf(IgnoreTLSCertificate()) arrayOf(AllowAllTrustManager())
} else { } else {
this.trustManagers this.trustManagers
} }

View file

@ -214,6 +214,7 @@ class LocalProfileAssistantImpl(
if (res != 0) { if (res != 0) {
// 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),
httpInterface.lastHttpResponse, httpInterface.lastHttpResponse,
httpInterface.lastHttpException, httpInterface.lastHttpException,
apduInterface.lastApduResponse, apduInterface.lastApduResponse,

View file

@ -149,3 +149,33 @@ Java_net_typeblog_lpac_1jni_LpacJni_cancelSessions(JNIEnv *env, jobject thiz, jl
es10b_cancel_session(ctx, ES10B_CANCEL_SESSION_REASON_UNDEFINED); es10b_cancel_session(ctx, ES10B_CANCEL_SESSION_REASON_UNDEFINED);
euicc_http_cleanup(ctx); euicc_http_cleanup(ctx);
} }
#define QUOTE(S) #S
#define ERRCODE_ENUM_TO_STRING(VARIANT) case VARIANT: return toJString(env, QUOTE(VARIANT))
JNIEXPORT jstring JNICALL
Java_net_typeblog_lpac_1jni_LpacJni_downloadErrCodeToString(JNIEnv *env, jobject thiz, jint code) {
switch (code) {
ERRCODE_ENUM_TO_STRING(ES10B_ERROR_REASON_INCORRECT_INPUT_VALUES);
ERRCODE_ENUM_TO_STRING(ES10B_ERROR_REASON_INVALID_SIGNATURE);
ERRCODE_ENUM_TO_STRING(ES10B_ERROR_REASON_INVALID_TRANSACTION_ID);
ERRCODE_ENUM_TO_STRING(ES10B_ERROR_REASON_UNSUPPORTED_CRT_VALUES);
ERRCODE_ENUM_TO_STRING(ES10B_ERROR_REASON_UNSUPPORTED_REMOTE_OPERATION_TYPE);
ERRCODE_ENUM_TO_STRING(ES10B_ERROR_REASON_UNSUPPORTED_PROFILE_CLASS);
ERRCODE_ENUM_TO_STRING(ES10B_ERROR_REASON_SCP03T_STRUCTURE_ERROR);
ERRCODE_ENUM_TO_STRING(ES10B_ERROR_REASON_SCP03T_SECURITY_ERROR);
ERRCODE_ENUM_TO_STRING(
ES10B_ERROR_REASON_INSTALL_FAILED_DUE_TO_ICCID_ALREADY_EXISTS_ON_EUICC);
ERRCODE_ENUM_TO_STRING(
ES10B_ERROR_REASON_INSTALL_FAILED_DUE_TO_INSUFFICIENT_MEMORY_FOR_PROFILE);
ERRCODE_ENUM_TO_STRING(ES10B_ERROR_REASON_INSTALL_FAILED_DUE_TO_INTERRUPTION);
ERRCODE_ENUM_TO_STRING(ES10B_ERROR_REASON_INSTALL_FAILED_DUE_TO_PE_PROCESSING_ERROR);
ERRCODE_ENUM_TO_STRING(ES10B_ERROR_REASON_INSTALL_FAILED_DUE_TO_ICCID_MISMATCH);
ERRCODE_ENUM_TO_STRING(
ES10B_ERROR_REASON_TEST_PROFILE_INSTALL_FAILED_DUE_TO_INVALID_NAA_KEY);
ERRCODE_ENUM_TO_STRING(ES10B_ERROR_REASON_PPR_NOT_ALLOWED);
ERRCODE_ENUM_TO_STRING(ES10B_ERROR_REASON_INSTALL_FAILED_DUE_TO_UNKNOWN_ERROR);
default:
return toJString(env, "ES10B_ERROR_REASON_UNDEFINED");
}
}