diff --git a/.forgejo/workflows/build-debug.yml b/.forgejo/workflows/build-debug.yml
index 660dabc..0818b8b 100644
--- a/.forgejo/workflows/build-debug.yml
+++ b/.forgejo/workflows/build-debug.yml
@@ -33,14 +33,23 @@ jobs:
uses: https://gitea.angry.im/actions/setup-android@v3
- name: Build Debug APKs
- run: ./gradlew --no-daemon assembleDebug
+ run: ./gradlew --no-daemon assembleDebug :app:assembleDebugMagiskModuleDir
- name: Copy Artifacts
- run: find . -name 'app*-debug.apk' -exec cp {} . \;
+ run: |
+ find . -name 'app*-debug.apk' -exec cp {} . \;
+ cp -r app/build/magisk/debug ./magisk-debug
- - name: Upload Artifacts
+ - name: Upload APK Artifacts
uses: https://gitea.angry.im/actions/upload-artifact@v3
with:
name: Debug APKs
compression-level: 0
path: app*-debug.apk
+
+ - name: Upload Magisk Artifacts
+ uses: https://gitea.angry.im/actions/upload-artifact@v3
+ with:
+ name: magisk-debug
+ compression-level: 0
+ path: magisk-debug
diff --git a/.idea/.gitignore b/.idea/.gitignore
index b7c2402..d2293f6 100644
--- a/.idea/.gitignore
+++ b/.idea/.gitignore
@@ -1,14 +1,7 @@
-/shelf
-/caches
-/libraries
-/assetWizardSettings.xml
-/deploymentTargetDropDown.xml
-/gradle.xml
-/misc.xml
-/modules.xml
-/navEditor.xml
-/runConfigurations.xml
-/workspace.xml
-/AndroidProjectSystem.xml
-
-**/*.iml
\ No newline at end of file
+*
+!/codeStyles/Project.xml
+!/codeStyles/codeStyleConfig.xml
+!/vcs.xml
+!/kotlinc.xml
+!/compiler.xml
+!/migrations.xml
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
deleted file mode 100644
index e40be60..0000000
--- a/.idea/deploymentTargetSelector.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/README.md b/README.md
index f953f9e..8a67e41 100644
--- a/README.md
+++ b/README.md
@@ -4,19 +4,23 @@ A fully free and open-source Local Profile Assistant implementation for Android
There are two variants of this project, OpenEUICC and EasyEUICC:
-| | OpenEUICC | EasyEUICC |
-|:------------------------------|:-----------------------------------------------:|:-----------------:|
-| Privileged | Must be installed as system app | No |
-| Internal eSIM | Supported | Unsupported |
-| External (Removable) eSIM | Supported | Supported |
-| USB Readers | Supported | Supported |
-| Requires allowlisting by eSIM | No | Yes -- except USB |
-| System Integration | Partial (carrier partner API unimplemented yet) | No |
+| | OpenEUICC | EasyEUICC |
+| :---------------------------- | :-----------------------------: | :-----------------: |
+| Privileged | Must be installed as system app | No |
+| Internal eSIM | Supported | Unsupported |
+| External eSIM [^1] | Supported | Supported |
+| USB Readers | Supported | Supported |
+| Requires allowlisting by eSIM | No | Yes -- except USB |
+| System Integration | Partial [^2] | No |
+| Minimum Android Version | Android 11 or higher | Android 9 or higher |
+
+[^1]: Also known as "Removable eSIM"
+[^2]: Carrier Partner API unimplemented yet
Some side notes:
1. When privileged, OpenEUICC supports any eUICC chip that implements the SGP.22 standard, internal or external. However, there is __no guarantee__ that external (removable) eSIMs actually follow the standard. Please __DO NOT__ submit bug reports for non-functioning removable eSIMs. They are __NOT__ officially supported unless they also support / are supported by EasyEUICC, the unprivileged variant.
2. Both variants support accessing eUICC chips through USB CCID readers, regardless of whether the chip contains the correct ARA-M hash to allow for unprivileged access. However, only `T=0` readers that use the standard [USB CCID protocol](https://en.wikipedia.org/wiki/CCID_(protocol)) are supported.
-3. Prebuilt release-mode EasyEUICC apks can be downloaded [here](https://gitea.angry.im/PeterCxy/OpenEUICC/releases). For OpenEUICC, no official release is currently provided and only debug mode APKs can be found in the CI page.
+3. Prebuilt release-mode EasyEUICC apks can be downloaded [here](https://gitea.angry.im/PeterCxy/OpenEUICC/releases). For OpenEUICC, no official release is currently provided and only debug mode APKs and Magisk modules can be found in the [CI page](https://gitea.angry.im/PeterCxy/OpenEUICC/actions).
4. For removable eSIM chip vendors: to have your chip supported by official builds of EasyEUICC when inserted, include the ARA-M hash `2A2FA878BC7C3354C2CF82935A5945A3EDAE4AFA`.
__This project is Free Software licensed under GNU GPL v3, WITHOUT the "or later" clause.__ Any modification and derivative work __MUST__ be released under the SAME license, which means, at the very least, that the source code __MUST__ be available upon request.
@@ -74,10 +78,7 @@ FAQs
===
- Q: Do you provide prebuilt binaries for OpenEUICC?
-- A: Debug-mode APKs are available continuously as an artifact of the [Actions](https://gitea.angry.im/PeterCxy/OpenEUICC/actions) CI used by this project. However, these debug-mode APKs are **not** intended for inclusion inside system images, nor are they supported by the developer in any sense. If you are a custom ROM developer, either include the entire OpenEUICC repository in your AOSP source tree, or generate an APK using `gradle` and import that as a prebuilt system app. Note that you might want `privapp_whitelist_im.angry.openeuicc.xml` as well.
-
-- Q: AOSP's Settings app seems to be confused by OpenEUICC (for example, disabling / enabling profiles from the Networks page do not work properly)
-- A: When your device has internal eSIM chip(s) __and__ you have inserted a removable eSIM chip, the Settings app can misbehave since it was never designed for this scenario. __Please prefer using OpenEUICC's own management interface whenever possible.__ In the future, there might be an option to exclude removable SIMs from being reported to the Android system.
+- A: Debug-mode APKs and Magisk modules are available continuously as an artifact of the [Actions](https://gitea.angry.im/PeterCxy/OpenEUICC/actions) CI used by this project. However, these debug-mode APKs are **not** intended for inclusion inside system images, nor are they supported by the developer in any sense. If you are a custom ROM developer, either include the entire OpenEUICC repository in your AOSP source tree, or generate an APK using `gradle` and import that as a prebuilt system app. Note that you might want `privapp_whitelist_im.angry.openeuicc.xml` as well.
- Q: Can EasyEUICC manage my phone's internal eSIM?
- A: No. For EasyEUICC to work, the eSIM chip MUST proactively grant access via its ARA-M field.
diff --git a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt
index 87a0eea..78a8c3f 100644
--- a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt
@@ -21,7 +21,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
override suspend fun tryOpenEuiccChannel(
port: UiccPortInfoCompat,
isdrAid: ByteArray
- ): EuiccChannel? {
+ ): EuiccChannel? = try {
if (port.portIndex != 0) {
Log.w(
DefaultEuiccChannelManager.TAG,
@@ -35,58 +35,52 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
DefaultEuiccChannelManager.TAG,
"Trying OMAPI for physical slot ${port.card.physicalSlotIndex}"
)
- try {
- return EuiccChannelImpl(
- context.getString(R.string.channel_type_omapi),
+ EuiccChannelImpl(
+ context.getString(R.string.channel_type_omapi),
+ port,
+ intrinsicChannelName = null,
+ OmapiApduInterface(
+ seService!!,
port,
- intrinsicChannelName = null,
- OmapiApduInterface(
- seService!!,
- port,
- context.preferenceRepository.verboseLoggingFlow
- ),
- isdrAid,
- context.preferenceRepository.verboseLoggingFlow,
- context.preferenceRepository.ignoreTLSCertificateFlow,
- ).also {
- Log.i(DefaultEuiccChannelManager.TAG, "Is OMAPI channel, setting MSS to 60")
- it.lpa.setEs10xMss(60)
- }
- } catch (_: IllegalArgumentException) {
- // Failed
- Log.w(
- DefaultEuiccChannelManager.TAG,
- "OMAPI APDU interface unavailable for physical slot ${port.card.physicalSlotIndex} with ISD-R AID: ${isdrAid.encodeHex()}."
- )
- }
-
- return null
+ context.preferenceRepository.verboseLoggingFlow
+ ),
+ isdrAid,
+ context.preferenceRepository.verboseLoggingFlow,
+ context.preferenceRepository.ignoreTLSCertificateFlow,
+ context.preferenceRepository.es10xMssFlow,
+ )
+ } catch (_: IllegalArgumentException) {
+ // Failed
+ Log.w(
+ DefaultEuiccChannelManager.TAG,
+ "OMAPI APDU interface unavailable for physical slot ${port.card.physicalSlotIndex} with ISD-R AID: ${isdrAid.encodeHex()}."
+ )
+ null
}
override fun tryOpenUsbEuiccChannel(
ccidCtx: UsbCcidContext,
isdrAid: ByteArray
- ): EuiccChannel? {
- try {
- return EuiccChannelImpl(
- context.getString(R.string.channel_type_usb),
- FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)),
- intrinsicChannelName = ccidCtx.productName,
- UsbApduInterface(
- ccidCtx
- ),
- isdrAid,
- context.preferenceRepository.verboseLoggingFlow,
- context.preferenceRepository.ignoreTLSCertificateFlow,
- )
- } catch (_: IllegalArgumentException) {
- // Failed
- Log.w(
- DefaultEuiccChannelManager.TAG,
- "USB APDU interface unavailable for ISD-R AID: ${isdrAid.encodeHex()}."
- )
- }
- return null
+ ): EuiccChannel? = try {
+ EuiccChannelImpl(
+ context.getString(R.string.channel_type_usb),
+ FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)),
+ intrinsicChannelName = ccidCtx.productName,
+ UsbApduInterface(
+ ccidCtx
+ ),
+ isdrAid,
+ context.preferenceRepository.verboseLoggingFlow,
+ context.preferenceRepository.ignoreTLSCertificateFlow,
+ context.preferenceRepository.es10xMssFlow,
+ )
+ } catch (_: IllegalArgumentException) {
+ // Failed
+ Log.w(
+ DefaultEuiccChannelManager.TAG,
+ "USB APDU interface unavailable for ISD-R AID: ${isdrAid.encodeHex()}."
+ )
+ null
}
override fun cleanup() {
diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt
index 2a33c20..eaec522 100644
--- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt
@@ -1,8 +1,9 @@
package im.angry.openeuicc.core
import im.angry.openeuicc.util.UiccPortInfoCompat
-import im.angry.openeuicc.util.decodeHex
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.runBlocking
import net.typeblog.lpac_jni.ApduInterface
import net.typeblog.lpac_jni.LocalProfileAssistant
import net.typeblog.lpac_jni.impl.HttpInterfaceImpl
@@ -15,7 +16,8 @@ class EuiccChannelImpl(
override val apduInterface: ApduInterface,
override val isdrAid: ByteArray,
verboseLoggingFlow: Flow,
- ignoreTLSCertificateFlow: Flow
+ ignoreTLSCertificateFlow: Flow,
+ es10xMssFlow: Flow,
) : EuiccChannel {
override val slotId = port.card.physicalSlotIndex
override val logicalSlotId = port.logicalSlotIndex
@@ -25,8 +27,10 @@ class EuiccChannelImpl(
LocalProfileAssistantImpl(
isdrAid,
apduInterface,
- HttpInterfaceImpl(verboseLoggingFlow, ignoreTLSCertificateFlow)
- )
+ HttpInterfaceImpl(verboseLoggingFlow, ignoreTLSCertificateFlow),
+ ).also {
+ it.setEs10xMss(runBlocking { es10xMssFlow.first().toByte() })
+ }
override val atr: ByteArray?
get() = (apduInterface as? ApduInterfaceAtrProvider)?.atr
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt
index bfbcbd8..248afaf 100644
--- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt
@@ -123,7 +123,13 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
add(Item(R.string.euicc_info_pp_version, info.ppVersion.toString()))
info.sasAccreditationNumber.trim().takeIf(RE_SAS::matches)
?.let { add(Item(R.string.euicc_info_sas_accreditation_number, it.uppercase())) }
- add(Item(R.string.euicc_info_free_nvram, info.freeNvram.let(::formatFreeSpace)))
+
+ val nvramText = buildString {
+ append(formatFreeSpace(info.freeNvram))
+ append(' ')
+ append(getString(R.string.euicc_info_free_nvram_hint))
+ }
+ add(Item(R.string.euicc_info_free_nvram, nvramText))
}
channel.lpa.euiccInfo2?.euiccCiPKIdListForSigning.orEmpty().let { signers ->
// SGP.28 v1.0, eSIM CI Registration Criteria (Page 5 of 9, 2019-10-24)
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 6554142..7a717ac 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
@@ -8,6 +8,7 @@ import android.provider.Settings
import android.widget.Toast
import androidx.lifecycle.lifecycleScope
import androidx.preference.CheckBoxPreference
+import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
@@ -16,7 +17,6 @@ import im.angry.openeuicc.util.*
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
open class SettingsFragment: PreferenceFragmentCompat() {
private lateinit var developerPref: PreferenceCategory
@@ -34,7 +34,7 @@ open class SettingsFragment: PreferenceFragmentCompat() {
// Show / hide developer preference based on whether it is enabled
lifecycleScope.launch {
preferenceRepository.developerOptionsEnabledFlow
- .onEach { developerPref.isVisible = it }
+ .onEach(developerPref::setVisible)
.collect()
}
@@ -84,6 +84,9 @@ open class SettingsFragment: PreferenceFragmentCompat() {
requirePreference("pref_developer_euicc_memory_reset")
.bindBooleanFlow(preferenceRepository.euiccMemoryResetFlow)
+ requirePreference("pref_developer_es10x_mss")
+ .bindIntFlow(preferenceRepository.es10xMssFlow, 63)
+
requirePreference("pref_developer_isdr_aid_list").apply {
intent = Intent(requireContext(), IsdrAidListActivity::class.java)
}
@@ -100,51 +103,53 @@ open class SettingsFragment: PreferenceFragmentCompat() {
@Suppress("UNUSED_PARAMETER")
private fun onAppVersionClicked(pref: Preference): Boolean {
if (developerPref.isVisible) return false
+
val now = System.currentTimeMillis()
- if (now - lastClickTimestamp >= 1000) {
- numClicks = 1
- } else {
- numClicks++
- }
+ numClicks = if (now - lastClickTimestamp >= 1000) 1 else numClicks + 1
lastClickTimestamp = now
- if (numClicks == 7) {
- lifecycleScope.launch {
- preferenceRepository.developerOptionsEnabledFlow.updatePreference(true)
-
- lastToast?.cancel()
- Toast.makeText(
- requireContext(),
- R.string.developer_options_enabled,
- Toast.LENGTH_SHORT
- ).show()
- }
- } else if (numClicks > 1) {
- lastToast?.cancel()
- lastToast = Toast.makeText(
- requireContext(),
- getString(R.string.developer_options_steps, 7 - numClicks),
- Toast.LENGTH_SHORT
- )
- lastToast!!.show()
+ lifecycleScope.launch {
+ preferenceRepository.developerOptionsEnabledFlow.updatePreference(numClicks >= 7)
}
+ val toastText = when {
+ numClicks == 7 -> getString(R.string.developer_options_enabled)
+ numClicks > 1 -> getString(R.string.developer_options_steps, 7 - numClicks)
+ else -> return true
+ }
+
+ lastToast?.cancel()
+ lastToast = Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT)
+ lastToast!!.show()
return true
}
protected fun CheckBoxPreference.bindBooleanFlow(flow: PreferenceFlowWrapper) {
lifecycleScope.launch {
- flow.collect { isChecked = it }
+ flow.collect(::setChecked)
}
setOnPreferenceChangeListener { _, newValue ->
- runBlocking {
+ lifecycleScope.launch {
flow.updatePreference(newValue as Boolean)
}
true
}
}
+ private fun ListPreference.bindIntFlow(flow: PreferenceFlowWrapper, defaultValue: Int) {
+ lifecycleScope.launch {
+ flow.collect { value = it.toString() }
+ }
+
+ setOnPreferenceChangeListener { _, newValue ->
+ lifecycleScope.launch {
+ flow.updatePreference((newValue as String).toIntOrNull() ?: defaultValue)
+ }
+ true
+ }
+ }
+
protected fun mergePreferenceOverlay(overlayKey: String, targetKey: String) {
val overlayCat = requirePreference(overlayKey)
val targetCat = requirePreference(targetKey)
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt
index 342a687..0048190 100644
--- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt
@@ -7,6 +7,7 @@ import android.view.ViewGroup
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
+import androidx.annotation.StringRes
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
@@ -42,19 +43,17 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep
}
private data class ProgressItem(
- val titleRes: Int,
- var state: ProgressState
+ @StringRes val titleRes: Int,
+ var state: ProgressState = ProgressState.NotStarted,
+ var errorMessage: SimplifiedErrorMessages? = null,
)
private val progressItems = arrayOf(
- ProgressItem(R.string.download_wizard_progress_step_preparing, ProgressState.NotStarted),
- ProgressItem(R.string.download_wizard_progress_step_connecting, ProgressState.NotStarted),
- ProgressItem(
- R.string.download_wizard_progress_step_authenticating,
- ProgressState.NotStarted
- ),
- ProgressItem(R.string.download_wizard_progress_step_downloading, ProgressState.NotStarted),
- ProgressItem(R.string.download_wizard_progress_step_finalizing, ProgressState.NotStarted)
+ ProgressItem(R.string.download_wizard_progress_step_preparing),
+ ProgressItem(R.string.download_wizard_progress_step_connecting),
+ ProgressItem(R.string.download_wizard_progress_step_authenticating),
+ ProgressItem(R.string.download_wizard_progress_step_downloading),
+ ProgressItem(R.string.download_wizard_progress_step_finalizing)
)
private val adapter = ProgressItemAdapter()
@@ -122,8 +121,13 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep
// Change the state of the last InProgress item to success (or error)
progressItems.forEachIndexed { index, progressItem ->
if (progressItem.state == ProgressState.InProgress) {
- progressItem.state =
- if (state.downloadError == null) ProgressState.Done else ProgressState.Error
+ if (state.downloadError == null) {
+ progressItem.state = ProgressState.Done
+ } else {
+ progressItem.state = ProgressState.Error
+ progressItem.errorMessage =
+ SimplifiedErrorMessages.fromDownloadError(state.downloadError!!)
+ }
}
adapter.notifyItemChanged(index)
@@ -133,9 +137,8 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep
refreshButtons()
}
- is EuiccChannelManagerService.ForegroundTaskState.InProgress -> {
+ is EuiccChannelManagerService.ForegroundTaskState.InProgress ->
updateProgress(it.progress)
- }
else -> {}
}
@@ -197,9 +200,15 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep
private val progressBar =
root.requireViewById(R.id.download_progress_icon_progress)
private val icon = root.requireViewById(R.id.download_progress_icon)
+ private val errorTitle =
+ root.requireViewById(R.id.download_progress_item_error_title)
+ private val errorSuggestion =
+ root.requireViewById(R.id.download_progress_item_error_suggestion)
fun bind(item: ProgressItem) {
title.text = getString(item.titleRes)
+ errorTitle.visibility = View.GONE
+ errorSuggestion.visibility = View.GONE
when (item.state) {
ProgressState.NotStarted -> {
@@ -222,6 +231,15 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep
progressBar.visibility = View.GONE
icon.setImageResource(R.drawable.ic_error_outline)
icon.visibility = View.VISIBLE
+
+ item.errorMessage?.titleResId?.let {
+ errorTitle.visibility = View.VISIBLE
+ errorTitle.text = getString(it)
+ }
+ item.errorMessage?.suggestResId?.let {
+ errorSuggestion.visibility = View.VISIBLE
+ errorSuggestion.text = getString(it)
+ }
}
}
}
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/SimplifiedErrorMessages.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/SimplifiedErrorMessages.kt
new file mode 100644
index 0000000..8ce5740
--- /dev/null
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/SimplifiedErrorMessages.kt
@@ -0,0 +1,154 @@
+package im.angry.openeuicc.ui.wizard
+
+import androidx.annotation.StringRes
+import im.angry.openeuicc.common.R
+import net.typeblog.lpac_jni.LocalProfileAssistant
+import org.json.JSONObject
+import java.net.NoRouteToHostException
+import java.net.PortUnreachableException
+import java.net.SocketException
+import java.net.SocketTimeoutException
+import java.net.UnknownHostException
+import javax.net.ssl.SSLException
+
+enum class SimplifiedErrorMessages(
+ @StringRes val titleResId: Int,
+ @StringRes val suggestResId: Int?
+) {
+ ICCIDAlreadyInUse(
+ R.string.download_wizard_error_iccid_already,
+ R.string.download_wizard_error_suggest_profile_installed
+ ),
+ InsufficientMemory(
+ R.string.download_wizard_error_insufficient_memory,
+ R.string.download_wizard_error_suggest_insufficient_memory
+ ),
+ UnsupportedProfile(
+ R.string.download_wizard_error_unsupported_profile,
+ null
+ ),
+ CardInternalError(
+ R.string.download_wizard_error_card_internal_error,
+ null
+ ),
+ EIDNotSupported(
+ R.string.download_wizard_error_eid_not_supported,
+ R.string.download_wizard_error_suggest_contact_carrier
+ ),
+ EIDMismatch(
+ R.string.download_wizard_error_eid_mismatch,
+ R.string.download_wizard_error_suggest_contact_reissue
+ ),
+ UnreleasedProfile(
+ R.string.download_wizard_error_profile_unreleased,
+ R.string.download_wizard_error_suggest_contact_reissue
+ ),
+ MatchingIDRefused(
+ R.string.download_wizard_error_matching_id_refused,
+ R.string.download_wizard_error_suggest_contact_carrier
+ ),
+ ProfileRetriesExceeded(
+ R.string.download_wizard_error_profile_retries_exceeded,
+ R.string.download_wizard_error_suggest_contact_carrier
+ ),
+ ConfirmationCodeMissing(
+ R.string.download_wizard_error_confirmation_code_missing,
+ R.string.download_wizard_error_suggest_contact_carrier
+ ),
+ ConfirmationCodeRefused(
+ R.string.download_wizard_error_confirmation_code_refused,
+ R.string.download_wizard_error_suggest_contact_carrier
+ ),
+ ConfirmationCodeRetriesExceeded(
+ R.string.download_wizard_error_confirmation_code_retries_exceeded,
+ R.string.download_wizard_error_suggest_contact_carrier
+ ),
+ ProfileExpired(
+ R.string.download_wizard_error_profile_expired,
+ R.string.download_wizard_error_suggest_contact_carrier
+ ),
+ UnknownHost(
+ R.string.download_wizard_error_unknown_hostname,
+ null
+ ),
+ NetworkUnreachable(
+ R.string.download_wizard_error_network_unreachable,
+ R.string.download_wizard_error_suggest_network_unreachable
+ ),
+ TLSError(
+ R.string.download_wizard_error_tls_certificate,
+ null
+ );
+
+ companion object {
+ private val httpErrors = buildMap {
+ // Stage: AuthenticateClient
+ put("8.1" to "4.8", InsufficientMemory)
+ put("8.1.1" to "2.1", EIDNotSupported)
+ put("8.1.1" to "3.8", EIDMismatch)
+ put("8.2" to "1.2", UnreleasedProfile)
+ put("8.2.6" to "3.8", MatchingIDRefused)
+ put("8.8.5" to "6.4", ProfileRetriesExceeded)
+
+ // Stage: GetBoundProfilePackage
+ put("8.2.7" to "2.2", ConfirmationCodeMissing)
+ put("8.2.7" to "3.8", ConfirmationCodeRefused)
+ put("8.2.7" to "6.4", ConfirmationCodeRetriesExceeded)
+
+ // Stage: AuthenticateClient, GetBoundProfilePackage
+ put("8.8.5" to "4.10", ProfileExpired)
+ }
+
+ fun fromDownloadError(exc: LocalProfileAssistant.ProfileDownloadException) = when {
+ exc.lpaErrorReason != "ES10B_ERROR_REASON_UNDEFINED" -> fromLPAErrorReason(exc.lpaErrorReason)
+ exc.lastHttpResponse?.rcode == 200 -> fromHTTPResponse(exc.lastHttpResponse!!)
+ exc.lastHttpException != null -> fromHTTPException(exc.lastHttpException!!)
+ exc.lastApduResponse != null -> fromAPDUResponse(exc.lastApduResponse!!)
+ else -> null
+ }
+
+ private fun fromLPAErrorReason(reason: String) = when (reason) {
+ "ES10B_ERROR_REASON_UNSUPPORTED_CRT_VALUES" -> UnsupportedProfile
+ "ES10B_ERROR_REASON_UNSUPPORTED_REMOTE_OPERATION_TYPE" -> UnsupportedProfile
+ "ES10B_ERROR_REASON_UNSUPPORTED_PROFILE_CLASS" -> UnsupportedProfile
+ "ES10B_ERROR_REASON_INSTALL_FAILED_DUE_TO_ICCID_ALREADY_EXISTS_ON_EUICC" -> ICCIDAlreadyInUse
+ "ES10B_ERROR_REASON_INSTALL_FAILED_DUE_TO_INSUFFICIENT_MEMORY_FOR_PROFILE" -> InsufficientMemory
+ "ES10B_ERROR_REASON_INSTALL_FAILED_DUE_TO_INTERRUPTION" -> CardInternalError
+ "ES10B_ERROR_REASON_INSTALL_FAILED_DUE_TO_PE_PROCESSING_ERROR" -> CardInternalError
+ else -> null
+ }
+
+ private fun fromHTTPResponse(httpResponse: net.typeblog.lpac_jni.HttpInterface.HttpResponse): SimplifiedErrorMessages? {
+ if (httpResponse.data.first().toInt() != '{'.code) return null
+ val response = JSONObject(httpResponse.data.decodeToString())
+ val statusCodeData = response.optJSONObject("header")
+ ?.optJSONObject("functionExecutionStatus")
+ ?.optJSONObject("statusCodeData")
+ ?: return null
+ val subjectCode = statusCodeData.optString("subjectCode")
+ val reasonCode = statusCodeData.optString("reasonCode")
+ return httpErrors[subjectCode to reasonCode]
+ }
+
+ private fun fromHTTPException(exc: Exception) = when (exc) {
+ is SSLException -> TLSError
+ is UnknownHostException -> UnknownHost
+ is NoRouteToHostException -> NetworkUnreachable
+ is PortUnreachableException -> NetworkUnreachable
+ is SocketTimeoutException -> NetworkUnreachable
+ is SocketException -> exc.message
+ ?.contains("Connection reset", ignoreCase = true)
+ ?.let { if (it) NetworkUnreachable else null }
+
+ else -> null
+ }
+
+ private fun fromAPDUResponse(resp: ByteArray): SimplifiedErrorMessages? {
+ val isSuccess = resp.size >= 2 &&
+ resp[resp.size - 2] == 0x90.toByte() &&
+ resp[resp.size - 1] == 0x00.toByte()
+ if (isSuccess) return null
+ return CardInternalError
+ }
+ }
+}
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 464aeee..2fef3db 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
@@ -5,6 +5,7 @@ import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import androidx.fragment.app.Fragment
@@ -38,6 +39,7 @@ internal object PreferenceKeys {
val IGNORE_TLS_CERTIFICATE = booleanPreferencesKey("ignore_tls_certificate")
val EUICC_MEMORY_RESET = booleanPreferencesKey("euicc_memory_reset")
val ISDR_AID_LIST = stringPreferencesKey("isdr_aid_list")
+ val ES10X_MSS = intPreferencesKey("es10x_mss")
}
const val EUICC_DEFAULT_ISDR_AID = "A0000005591010FFFFFFFF8900000100"
@@ -89,6 +91,7 @@ open class PreferenceRepository(private val context: Context) {
PreferenceConstants.DEFAULT_AID_LIST,
{ Base64.getEncoder().encodeToString(it.encodeToByteArray()) },
{ Base64.getDecoder().decode(it).decodeToString() })
+ val es10xMssFlow = bindFlow(PreferenceKeys.ES10X_MSS, 63)
protected fun bindFlow(
key: Preferences.Key,
diff --git a/app-common/src/main/res/layout/download_progress_item.xml b/app-common/src/main/res/layout/download_progress_item.xml
index f1d0852..c59673b 100644
--- a/app-common/src/main/res/layout/download_progress_item.xml
+++ b/app-common/src/main/res/layout/download_progress_item.xml
@@ -1,30 +1,32 @@
+ android:layout_height="wrap_content">
+ app:layout_constraintBottom_toBottomOf="@id/download_progress_icon_container"
+ app:layout_constraintEnd_toStartOf="@id/download_progress_icon_container"
+ app:layout_constraintHorizontal_bias="0.0"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="@id/download_progress_icon_container"
+ app:layout_constraintVertical_bias="0.5" />
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_bias="0.0">
+
+
+
+
\ No newline at end of file
diff --git a/app-common/src/main/res/values-ja/strings.xml b/app-common/src/main/res/values-ja/strings.xml
index 946625f..b171972 100644
--- a/app-common/src/main/res/values-ja/strings.xml
+++ b/app-common/src/main/res/values-ja/strings.xml
@@ -2,13 +2,13 @@
このアプリでアクセスできるリムーバブル eUICC カードがデバイス上で検出されていません。互換性のあるカード挿入または USB リーダーを接続してください。
この eSIM にはプロファイルがありません。
- 不明
- 情報がありません
ヘルプ
スロットを再読み込み
+ 不明
論理スロット %d
- 有効済み
- 無効済み
+
+ 有効化済み
+ 無効化済み
プロバイダー:
クラス:
テスト中
@@ -22,6 +22,8 @@
操作は成功しましたが、デバイスのモデムが更新を拒否しました。新しいプロファイルを使用するには機内モードに切り替えるか、再起動する必要があります。
新しい eSIM プロファイルに切り替えることができません。
確認文字列が一致しません
+ 確認文字列が一致しません
+ このチップは消去されました
ICCID をクリップボードにコピーしました
シリアル番号をクリップボードにコピーしました
EID をクリップボードにコピーしました
@@ -38,14 +40,16 @@
eSIM プロファイルの削除に失敗しました
eSIM プロファイルを切り替え中
eSIM プロファイルの切り替えに失敗しました
+ eSIM チップを消去中
+ eSIM チップの消去に失敗しました
新しい eSIM
サーバー (RSP / SM-DP+)
アクティベーションコード
確認コード (オプション)
確認コード (必須)
IMEI (オプション)
- 残り容量が少ない
- 残り容量が少ないため、ダウンロードに失敗する可能性があります。
+ 残りの容量が少量です
+ 残り容量が少ないため、このプロファイルのダウンロードに失敗する可能性があります。
クリップボードに LPA コードがありません
解析できません
QR コードまたはクリップボードの内容を LPA コードとして解析できませんでした。
@@ -83,6 +87,27 @@
最終の APDU 例外:
保存
「%s」での診断
+ この eSIM プロファイルはすでに eSIM チップに存在します。
+ eSIM チップにはプロファイルをダウンロードするのに必要なメモリが残っていません。
+ この eSIM プロファイルは、eSIM チップではサポートされていません。
+ eSIM チップでエラーが発生しました。
+ 使用しているデバイスまたは eSIM チップの EID は、通信事業者によってサポートされていません。
+ この eSIM プロファイルは、別のデバイスにダウンロードされています。
+ この eSIM プロファイルは取り消されました。
+ アクティベーションコードが無効です。
+ eSIM プロファイルのダウンロード試行回数の上限を超えました。
+ このプロファイルをダウンロードするには確認コードが必要です。
+ 入力した確認コードは無効です。
+ この eSIM プロファイルは有効期限が切れています。
+ 確認コードのダウンロード試行回数の上限を超えました。
+ 不明な SM-DP+ アドレス
+ ネットワークにアクセスできません
+ TLS 証明書エラー、この eSIM プロファイルはサポートされていません
+ ダウンロード済みの eSIM プロファイルを再インストールしようとしています
+ 未使用の eSIM プロファイルをいくつか削除して、再度お試しください
+ サポートについては、通信事業者にお問い合わせください。
+ この eSIM プロファイルを再発行するには、通信事業者にお問い合わせください。
+ 別のネットワークに接続後 (例: Wi-Fi とデータを切り替え) を行った後に再度お試しください。
ログは共有したパスに保存されました。別のアプリで共有しますか?
新しいニックネーム
ニックネームを UTF-8 にエンコードできませんでした
@@ -96,8 +121,8 @@
eSIM プロファイルはダウンロードや削除、有効化や無効化されたときに通信事業者に通知を送信できます。送信されるこれらの通知のキューはここにリストされます。\n\n設定では、各タイプの通知を自動的に送信するかどうかを指定できます。通知が送信された場合でもキューのスペースが不足していない限り、記録から自動的に削除されることはありません。\n\nここでは保留中の各通知を手動で送信または削除できます。
ダウンロードしました
削除しました
- 有効化しました
- 無効化しました
+ 有効化済み
+ 無効化済み
処理
削除
eUICC 情報
@@ -114,17 +139,29 @@
SAS 認定番号
保護されたプロファイルのバージョン
NVRAM の空き容量 (eSIM プロファイルストレージ)
+ (参照用)
証明書発行者 (CI)
GSMA ライブ CI
GSMA テスト CI
不明な eSIM CI
+ eUICC を消去
+ eUICC を消去
+ このチップ上のすべてのプロファイルを削除することを確認してください。この操作は元に戻せないことを理解してください。\n\nEID: %1$s\n\n%2$s
+ 確認のために「%s」を入力してください
+ EID が %s で終わるチップを消去することを確認し、この操作は元に戻せないことを理解してください
+ 消去
+
はい
いいえ
+ 不明
+ 情報がありません
保存
%s のログ
開発者になるまであと %d ステップです。
あなたは開発者になりました!
- カスタム ISD-R AID リストが保存されました
+ ISD-R AID リスト
+ カスタム ISD-R AID リストを保存しました。
+ リセット
設定
通知
eSIM のプロファイル操作により、通信事業者に通知が送信されます。必要に応じてこの動作を微調整できます。
@@ -136,7 +173,7 @@
プロファイルを切り替え中の通知を送信します\nこのタイプの通知は信頼できないことに注意してください。
高度な設定
プロファイルの無効化と削除を許可
- デフォルトでは、このアプリでデバイスに挿入された取り外し可能な eSIM の有効なプロファイルを無効化することを防いでいます。なぜなのかというと時々アクセスができなくなるからです。\nこのチェックボックスを ON にすることで、この保護機能を解除します。
+ デフォルトでは、このアプリでデバイスに挿入されたリムーバブル eSIM の有効なプロファイルを無効化することを防いでいます。なぜなのかというと時々アクセスができなくなるからです。\nこのチェックボックスを ON にすることで、この保護機能を解除します。
詳細ログ
詳細ログを有効化します。これには個人的な情報が含まれている可能性があります。この機能を ON にした後は、信頼できるユーザーとのみログを共有してください。
言語
@@ -144,29 +181,22 @@
ログ
アプリの最新デバッグログを表示します
開発者オプション
- プロファイルを切り替えた後にモデムに更新コマンドを送信するかどうか。クラッシュが発生する場合は、これを無効にしてみてください。
+ モデムに更新コマンドを送信
+ プロファイルを切り替えた後にモデムに更新コマンドを送信するかどうかを設定します。クラッシュが発生する場合は、この機能を無効化してください。
フィルタリングされていないプロファイル一覧を表示
非運用のプロファイルも含めます
SM-DP+ TLS 証明書を無視する
RSP サーバーで使用される TLS 証明書を受け入れます
- 一部のブランドの取り外し可能な eUICC では、独自の非標準 ISD-R AID が使用されている場合があり、サードパーティ アプリからアクセスできなくなります。アプリはこのリストに追加された非標準の AID の使用を試みる可能性がありますが、動作することは保証されません。
+ eUICC の消去を許可
+ これは危険な操作であり、デフォルトでは非表示になっています。代わりとしてすべてのプロファイルを手動で削除することもできます。
+ グローバル ES10x MSS
+
+ - 高速
+ - 互換モード
+
+ ISD-R AID リストをカスタマイズ
+ 一部ブランドのリムーバブル eUICC は独自の非標準な ISD-R AID を使用しているため、サードパーティー製アプリからアクセスできない場合があります。このリストに追加された非標準な AID の使用を試みますが、動作の保証はできません。
情報
アプリバージョン
ソースコード
- 確認文字列が一致しません
- このチップは消去されました
- eSIM チップを消去しています
- eSIM チップの消去は失敗しました
- eSIM を消去する
- eSIM を消去する
- このチップ内のすべてのプロファイルを削除することをご確認してください。この操作は元に戻せないことをご理解してください。\n\nEID: %1$s\n\n%2$s
- 確認のため、ここに「%s」を入力してください
- EID が %s で終わるチップを消去することに同意します。これは元に戻せないことを理解しています。
- 消去する
- eUICC の消去を可能にする
- この操作は、デフォルトでは非表示になっている危険な操作です。代わりに、すべての構成ファイルを手動で削除することもできます。
- モデムに更新コマンドを送信
- ISD-R AID リストのカスタマイズ
- リセット
- ISD-R AID リスト
diff --git a/app-common/src/main/res/values-zh-rCN/strings.xml b/app-common/src/main/res/values-zh-rCN/strings.xml
index e947c6a..7303d2d 100644
--- a/app-common/src/main/res/values-zh-rCN/strings.xml
+++ b/app-common/src/main/res/values-zh-rCN/strings.xml
@@ -5,6 +5,7 @@
未知
帮助
重新加载卡槽
+ 未知
逻辑卡槽 %d
已启用
已禁用
@@ -46,6 +47,7 @@
IMEI (可选)
剩余空间不足
当前芯片的剩余空间不足,可能导致配置下载失败。\n是否继续下载?
+ 请连接到其他网络(例如在 Wi-Fi 和数据之间切换)后重试。
日志已保存到指定路径。需要通过其他 App 分享吗?
新昵称
无法将昵称编码为 UTF-8
@@ -83,6 +85,11 @@
日志
查看应用程序的最新调试日志
某些品牌的可移除 eUICC 可能会使用自己的非标准 ISD-R AID,导致第三方应用无法访问。此 App 可以尝试使用此列表中添加的非标准 AID,但不能保证它们一定有效。
+ 全局 ES10x MSS
+
+ - 最佳效率
+ - 最佳兼容性
+
信息
App 版本
源码
@@ -136,6 +143,7 @@
SAS 认证号码
Protected Profile 版本
NVRAM 剩余空间 (eSIM 存储容量)
+ (仅供参考)
证书签发者 (CI)
GSMA 生产环境 CI
GSMA 测试 CI
@@ -169,4 +177,24 @@
自定义 ISD-R AID 列表
重置
ISD-R AID 列表
+ 此 eSIM 配置文件已存在于您的 eSIM 芯片上。
+ 您的 eSIM 芯片没有足够的空间来下载配置文件。
+ 您的 eSIM 芯片不支持此 eSIM 配置文件。
+ eSIM 芯片错误。
+ 您的设备或 eSIM 芯片的 EID 不受您的运营商支持。
+ 此 eSIM 配置文件已被下载到另一台设备上。
+ 此 eSIM 配置文件已被撤销。
+ 激活码无效。
+ 已超出 eSIM 配置文件的最大下载尝试次数。
+ 下载此配置文件需要确认码。
+ 您输入的确认码无效。
+ 此 eSIM 配置文件已过期。
+ 已超出确认码的最大下载尝试次数。
+ 未知的 SM-DP+ 地址
+ 网络不可达
+ TLS 证书错误,不支持此 eSIM 配置文件
+ 您正在尝试重新安装已下载的 eSIM 配置文件
+ 请删除一些未使用的 eSIM 配置文件,然后重试
+ 请联系您的运营商寻求帮助。
+ 请联系您的运营商重新签发此 eSIM 配置文件。
\ No newline at end of file
diff --git a/app-common/src/main/res/values-zh-rTW/strings.xml b/app-common/src/main/res/values-zh-rTW/strings.xml
index 88cc011..ef6c842 100644
--- a/app-common/src/main/res/values-zh-rTW/strings.xml
+++ b/app-common/src/main/res/values-zh-rTW/strings.xml
@@ -5,6 +5,7 @@
未知
幫助
重新載入卡槽
+ 未知
虛擬卡槽 %d
已啟用
已停用
@@ -46,6 +47,7 @@
IMEI (可選)
剩餘空間不足
目前晶片的剩餘空間不足,可能導致配置下載失敗。\n是否繼續下載?
+ 請連接到其他網路(例如在 Wi-Fi 和資料之間切換)後重試。
日誌已儲存到指定路徑。需要透過其他 App 分享嗎?
新名稱
無法將名稱編碼為 UTF-8
@@ -83,6 +85,7 @@
允許 停用/刪除 已啟用的設定檔
預設情況下,此應用程式會阻止您停用可插拔 eSIM 中已啟用的設定檔。\n因為這樣做 有時 會導致無法存取。\n勾選此框以 移除 此保護措施。
某些品牌的可移除 eUICC 可能會使用自己的非標準 ISD-R AID,導致第三方應用程式無法存取。此 App 可以嘗試使用此清單中新增的非標準 AID,但不能保證它們一定有效。
+ 全局 ES10x MSS
資訊
App 版本
原始碼
@@ -136,6 +139,7 @@
SAS 認證號碼
Protected Profile 版本
NVRAM 剩餘空間 (eSIM 儲存容量)
+ (僅供參考)
證書簽發者 (CI)
GSMA 生產環境 CI
GSMA 測試 CI
@@ -169,4 +173,24 @@
自訂 ISD-R AID 列表
重置
ISD-R AID 列表
+ 此 eSIM 設定檔已存在於您的 eSIM 晶片上。
+ 您的 eSIM 晶片沒有足夠的空間來下載設定檔。
+ 您的 eSIM 晶片不支援此 eSIM 設定檔。
+ eSIM 晶片錯誤。
+ 您的裝置或 eSIM 晶片的 EID 不受您的電信業者支援。
+ 此 eSIM 設定檔已被下載到另一台裝置上。
+ 此 eSIM 設定檔已被撤銷。
+ 啟用碼無效。
+ 已超出 eSIM 設定檔的最大下載嘗試次數。
+ 下載此設定檔需要確認碼。
+ 您輸入的確認碼無效。
+ 此 eSIM 設定檔已過期。
+ 已超出確認碼的最大下載嘗試次數。
+ 未知的 SM-DP+ 位址
+ 網路不可達
+ TLS 憑證錯誤,不支援此 eSIM 設定檔
+ 您正在嘗試重新安裝已下載的 eSIM 設定文件
+ 請刪除一些未使用的 eSIM 設定文件,然後重試
+ 請聯絡您的電信業者尋求協助。
+ 請聯絡您的電信業者重新簽發此 eSIM 設定檔。
\ No newline at end of file
diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml
index e09da9f..39a762f 100644
--- a/app-common/src/main/res/values/strings.xml
+++ b/app-common/src/main/res/values/strings.xml
@@ -104,6 +104,27 @@
Last APDU exception:
Save
Diagnostics at %s
+ This eSIM profile is already present on your eSIM chip.
+ Your eSIM chip does not have sufficient memory left to download the profile.
+ This eSIM profile is unsupported by your eSIM chip.
+ An error occurred in your eSIM chip.
+ The EID of your device or eSIM chip is unsupported by your carrier.
+ This eSIM profile has been downloaded on another device.
+ This eSIM profile has been revoked.
+ The activation code is invalid.
+ The maximum number of download attempts for the eSIM profile has been exceeded.
+ Confirmation code is required to download this profile.
+ The confirmation code you entered is invalid.
+ This eSIM profile has expired.
+ The maximum number of download attempts for the confirmation code has been exceeded.
+ Unknown SM-DP+ address
+ Network is unreachable
+ TLS certificate error, this eSIM profile is not supported
+ You are trying to reinstall an already downloaded eSIM profile
+ Please delete some unused eSIM profiles and try again
+ Please contact your carrier for assistance.
+ Please contact your carrier to reissue this eSIM profile.
+ Please try again after connecting to a different network (e.g. switching between Wi-Fi and data).
Logs have been saved to the selected path. Would you like to share the log through another app?
@@ -144,6 +165,7 @@
SAS Accreditation Number
Protected Profile Version
Free NVRAM (eSIM profile storage)
+ (for reference only)
Certificate Issuer (CI)
GSMA Live CI
GSMA Test CI
@@ -200,6 +222,16 @@
Accept any TLS certificate used by the RSP server
Allow erasing eUICC
This is a dangerous operation and hidden by default. As an alternative, you can delete all profiles manually.
+ ES10x MSS
+ Global ES10x MSS
+
+ - High Efficiency
+ - Most Compatible
+
+
+ - 255
+ - 63
+
Customize ISD-R AID list
Some brands of removable eUICCs may use their own non-standard ISD-R AID, rendering them inaccessible to third-party apps. We can attempt to use non-standard AIDs added in this list, but there is no guarantee that they will work.
Info
diff --git a/app-common/src/main/res/xml/pref_settings.xml b/app-common/src/main/res/xml/pref_settings.xml
index 17505e1..831b04d 100644
--- a/app-common/src/main/res/xml/pref_settings.xml
+++ b/app-common/src/main/res/xml/pref_settings.xml
@@ -81,6 +81,14 @@
app:summary="@string/pref_developer_euicc_memory_reset_desc"
app:title="@string/pref_developer_euicc_memory_reset" />
+
+
- 互換性のチェック
+ 互換性の確認
SIM ツールキットを開く
ARA-M SHA-1 をクリップボードにコピーしました
「%s」アプリを有効化してください
- 互換性のチェック
- お使いのスマートフォンは %s 対応 SIM カードを管理できます
- お使いのスマートフォンは %s と互換性がありません
- お使いのスマートフォンは %s と完全な互換性がありません。ただし、USBスマートカードリーダーを使用する場合、ほぼすべての機能を利用できます。
- アクセス可能なスロット: %s
+
+ クイックで互換性を確認
+ このデバイスで「%s」の対応カードを管理できます
+ 使用しているデバイスは「%s」と互換性がありません
+ 使用しているデバイスは「%s」と完全に互換性がありません。ただし、USB スマートカードリーダーを使用すればほぼすべての機能を利用できます。
+ アクセス可能スロット: %s
ISD-R アクセス: %s
- 注: これらの結果は参考用です。以上に記載されていない SIM スロットでも、SIM カードを挿入すれば利用できる可能性があります。
- 注:現在 SIM カードが挿入されていない場合は、SIM カードを挿入してから再度互換性チェックをお試しください。どの SIM カードでも構いません。
- つづく
+ 注意: これらの結果は参考値です。上記に記載されていない SIM スロットでも、SIM カードを挿入すれば互換性がある場合があります。
+ 注意: 現在 SIM カードが挿入されていない場合は、SIM カードを挿入してから再度互換性の確認をお試しください。どの SIM カードでも構いません。
+ 続行
このメッセージを再度表示しない
不明
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 69e0db5..4012227 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,3 +1,4 @@
+import com.android.build.gradle.internal.api.ApkVariantOutputImpl
import im.angry.openeuicc.build.*
plugins {
@@ -48,4 +49,63 @@ dependencies {
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
+}
+
+val modulePropsTemplate = mutableMapOf(
+ "id" to android.defaultConfig.applicationId!!,
+ "name" to "OpenEUICC",
+ "version" to android.defaultConfig.versionName!!,
+ "versionCode" to "${android.defaultConfig.versionCode}",
+ "author" to "OpenEUICC authors",
+ "description" to "OpenEUICC is an open-source app that provides system-level eSIM integration."
+)
+
+val moduleCustomizeScript = project.file("magisk/customize.sh").readText()
+ .replace("{APK_NAME}", "OpenEUICC")
+ .replace("{PKG_NAME}", android.defaultConfig.applicationId!!)
+
+val moduleUninstallScript = project.file("magisk/uninstall.sh").readText()
+ .replace("{PKG_NAME}", android.defaultConfig.applicationId!!)
+
+tasks.register("assembleDebugMagiskModuleDir") {
+ variant = "debug"
+ appName = "OpenEUICC"
+ permsFile = project.rootProject.file("privapp_whitelist_im.angry.openeuicc.xml")
+ moduleInstaller = project.file("magisk/module_installer.sh")
+ moduleCustomizeScriptText = moduleCustomizeScript
+ moduleUninstallScriptText = moduleUninstallScript
+ moduleProp = modulePropsTemplate.let {
+ it["description"] = "(debug build) ${it["description"]}"
+ it["versionCode"] = "${(android.applicationVariants.find { it.name == "debug" }!!.outputs.first() as ApkVariantOutputImpl).versionCodeOverride}"
+ it["updateJson"] = "https://openeuicc.com/magisk/magisk-debug.json"
+ it
+ }
+ dependsOn("assembleDebug")
+}
+
+tasks.register("assembleDebugMagiskModule") {
+ dependsOn("assembleDebugMagiskModuleDir")
+ from((tasks.getByName("assembleDebugMagiskModuleDir") as MagiskModuleDirTask).outputDir)
+ archiveFileName = "magisk-debug.zip"
+ destinationDirectory = project.layout.buildDirectory.dir("magisk")
+ entryCompression = ZipEntryCompression.STORED
+}
+
+tasks.register("assembleReleaseMagiskModuleDir") {
+ variant = "release"
+ appName = "OpenEUICC"
+ permsFile = project.rootProject.file("privapp_whitelist_im.angry.openeuicc.xml")
+ moduleInstaller = project.file("magisk/module_installer.sh")
+ moduleCustomizeScriptText = moduleCustomizeScript
+ moduleUninstallScriptText = moduleUninstallScript
+ moduleProp = modulePropsTemplate
+ dependsOn("assembleRelease")
+}
+
+tasks.register("assembleReleaseMagiskModule") {
+ dependsOn("assembleReleaseMagiskModuleDir")
+ from((tasks.getByName("assembleReleaseMagiskModuleDir") as MagiskModuleDirTask).outputDir)
+ archiveFileName = "magisk-release.zip"
+ destinationDirectory = project.layout.buildDirectory.dir("magisk")
+ entryCompression = ZipEntryCompression.STORED
}
\ No newline at end of file
diff --git a/app/magisk/customize.sh b/app/magisk/customize.sh
new file mode 100644
index 0000000..3b57a55
--- /dev/null
+++ b/app/magisk/customize.sh
@@ -0,0 +1,9 @@
+TMP_FILE="$TMPDIR/{APK_NAME}"
+
+chmod u+x "$MODPATH/uninstall.sh"
+cp "$MODPATH/system/system_ext/priv-app/{APK_NAME}/{APK_NAME}.apk" "$TMP_FILE"
+
+pm install -r "$TMP_FILE"
+rm -f "$TMP_FILE"
+
+pm grant "{PKG_NAME}" android.permission.READ_PHONE_STATE
\ No newline at end of file
diff --git a/app/magisk/module_installer.sh b/app/magisk/module_installer.sh
new file mode 100644
index 0000000..28b48e5
--- /dev/null
+++ b/app/magisk/module_installer.sh
@@ -0,0 +1,33 @@
+#!/sbin/sh
+
+#################
+# Initialization
+#################
+
+umask 022
+
+# echo before loading util_functions
+ui_print() { echo "$1"; }
+
+require_new_magisk() {
+ ui_print "*******************************"
+ ui_print " Please install Magisk v20.4+! "
+ ui_print "*******************************"
+ exit 1
+}
+
+#########################
+# Load util_functions.sh
+#########################
+
+OUTFD=$2
+ZIPFILE=$3
+
+mount /data 2>/dev/null
+
+[ -f /data/adb/magisk/util_functions.sh ] || require_new_magisk
+. /data/adb/magisk/util_functions.sh
+[ $MAGISK_VER_CODE -lt 20400 ] && require_new_magisk
+
+install_module
+exit 0
diff --git a/app/magisk/uninstall.sh b/app/magisk/uninstall.sh
new file mode 100644
index 0000000..1eb0200
--- /dev/null
+++ b/app/magisk/uninstall.sh
@@ -0,0 +1 @@
+pm uninstall "{PKG_NAME}"
\ No newline at end of file
diff --git a/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt b/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt
index 978e886..876387f 100644
--- a/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt
+++ b/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt
@@ -42,6 +42,7 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto
isdrAid,
context.preferenceRepository.verboseLoggingFlow,
context.preferenceRepository.ignoreTLSCertificateFlow,
+ context.preferenceRepository.es10xMssFlow,
)
} catch (_: IllegalArgumentException) {
// Failed
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index fbf5c53..920801c 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -17,6 +17,7 @@
使用しているデバイスは eSIM をサポートしています。モバイルネットワークに接続するには通信事業者が発行した eSIM をダウンロードするか、物理 SIM を挿入してください。
スキップ
eSIM をダウンロード
- TelephonyManagerをどこでも使用
- デフォルトでは、非特権モード (EasyEUICC) と一致するように、取り外し可能な eUICC に対して OMAPI のみが試行されます。これは、一部のデバイスではうまく機能しない可能性があります。このオプションを選択する場合、取り外し可能な eUICC でも TelephonyManager を使用することになります。
+
+ TelephonyManager を常に使用可能にする
+ デフォルトでは、非特権モード (EasyEUICC など) での動作と一致させるためにリムーバブル eUICC に対しては OMAPI のみを試行します。ただし、一部デバイスでは正常に動作しない場合があります。このオプションを選択するとリムーバブル eUICC でも TelephonyManager の使用が強制されます。
diff --git a/buildSrc/src/main/kotlin/im/angry/openeuicc/build/Magisk.kt b/buildSrc/src/main/kotlin/im/angry/openeuicc/build/Magisk.kt
new file mode 100644
index 0000000..e5ccfa3
--- /dev/null
+++ b/buildSrc/src/main/kotlin/im/angry/openeuicc/build/Magisk.kt
@@ -0,0 +1,74 @@
+package im.angry.openeuicc.build
+
+import org.gradle.api.DefaultTask
+import org.gradle.api.provider.MapProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputDirectory
+import org.gradle.api.tasks.InputFile
+import org.gradle.api.tasks.OutputDirectory
+import org.gradle.api.tasks.TaskAction
+import java.io.File
+
+abstract class MagiskModuleDirTask : DefaultTask() {
+ @get:Input
+ abstract val variant : Property
+
+ @get:Input
+ abstract val appName : Property
+
+ @get:InputFile
+ abstract val permsFile : Property
+
+ @get:InputFile
+ abstract val moduleInstaller : Property
+
+ @get:Input
+ abstract val moduleCustomizeScriptText : Property
+
+ @get:Input
+ abstract val moduleUninstallScriptText : Property
+
+ @get:Input
+ abstract val moduleProp : MapProperty
+
+ @InputDirectory
+ val inputDir = variant.map { project.layout.buildDirectory.dir("outputs/apk/${it}") }
+
+ @OutputDirectory
+ val outputDir = variant.map { project.layout.buildDirectory.dir("magisk/${it}") }
+
+ @TaskAction
+ fun build() {
+ val dir = outputDir.get().get()
+ project.mkdir(dir)
+ val systemExtDir = dir.dir("system/system_ext")
+ val permDir = dir.dir("system/system_ext/etc/permissions")
+ val appDir = systemExtDir.dir("priv-app/${appName.get()}")
+ val metaInfDir = dir.dir("META-INF/com/google/android")
+ project.mkdir(systemExtDir)
+ project.mkdir(metaInfDir)
+ project.mkdir(appDir)
+ project.mkdir(permDir)
+ project.copy {
+ into(appDir)
+ from(inputDir) {
+ include("app-${variant.get()}.apk")
+ rename("app-${variant.get()}.apk", "${appName.get()}.apk")
+ }
+ }
+ project.copy {
+ from(permsFile)
+ into(permDir)
+ }
+ project.copy {
+ from(moduleInstaller)
+ into(metaInfDir)
+ rename(".*", "update-binary")
+ }
+ dir.file("customize.sh").asFile.writeText(moduleCustomizeScriptText.get())
+ dir.file("uninstall.sh").asFile.writeText(moduleUninstallScriptText.get())
+ metaInfDir.file("updater-script").asFile.writeText("#MAGISK\n")
+ dir.file("module.prop").asFile.writeText(moduleProp.get().map { (k, v) -> "$k=$v" }.joinToString("\n"))
+ }
+}
\ No newline at end of file
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..ce09717 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
@@ -11,12 +11,14 @@ import net.typeblog.lpac_jni.LocalProfileInfo
import net.typeblog.lpac_jni.LocalProfileNotification
import net.typeblog.lpac_jni.ProfileDownloadCallback
import net.typeblog.lpac_jni.Version
+import java.util.concurrent.locks.ReentrantLock
+import kotlin.concurrent.withLock
class LocalProfileAssistantImpl(
isdrAid: ByteArray,
rawApduInterface: ApduInterface,
rawHttpInterface: HttpInterface
-): LocalProfileAssistant {
+) : LocalProfileAssistant {
companion object {
private const val TAG = "LocalProfileAssistantImpl"
}
@@ -74,6 +76,10 @@ class LocalProfileAssistantImpl(
}
}
+ // Controls concurrency of every single method in this class, since
+ // the C-side is explicitly NOT thread-safe
+ private val lock = ReentrantLock()
+
private val apduInterface = ApduInterfaceWrapper(rawApduInterface)
private val httpInterface = HttpInterfaceWrapper(rawHttpInterface)
@@ -105,23 +111,24 @@ class LocalProfileAssistantImpl(
}
override val profiles: List
- @Synchronized
- get() {
+ get() = lock.withLock {
val head = LpacJni.es10cGetProfilesInfo(contextHandle)
var curr = head
val ret = mutableListOf()
while (curr != 0L) {
val state = LocalProfileInfo.State.fromString(LpacJni.profileGetStateString(curr))
val clazz = LocalProfileInfo.Clazz.fromString(LpacJni.profileGetClassString(curr))
- ret.add(LocalProfileInfo(
- LpacJni.profileGetIccid(curr),
- state,
- LpacJni.profileGetName(curr),
- LpacJni.profileGetNickname(curr),
- LpacJni.profileGetServiceProvider(curr),
- LpacJni.profileGetIsdpAid(curr),
- clazz
- ))
+ ret.add(
+ LocalProfileInfo(
+ LpacJni.profileGetIccid(curr),
+ state,
+ LpacJni.profileGetName(curr),
+ LpacJni.profileGetNickname(curr),
+ LpacJni.profileGetServiceProvider(curr),
+ LpacJni.profileGetIsdpAid(curr),
+ clazz
+ )
+ )
curr = LpacJni.profilesNext(curr)
}
@@ -130,79 +137,87 @@ class LocalProfileAssistantImpl(
}
override val notifications: List
- @Synchronized
- get() {
+ get() = lock.withLock {
val head = LpacJni.es10bListNotification(contextHandle)
var curr = head
- val ret = mutableListOf()
- while (curr != 0L) {
- ret.add(LocalProfileNotification(
- LpacJni.notificationGetSeq(curr),
- LocalProfileNotification.Operation.fromString(LpacJni.notificationGetOperationString(curr)),
- LpacJni.notificationGetAddress(curr),
- LpacJni.notificationGetIccid(curr),
- ))
- curr = LpacJni.notificationsNext(curr)
+
+ try {
+ val ret = mutableListOf()
+ while (curr != 0L) {
+ ret.add(
+ LocalProfileNotification(
+ LpacJni.notificationGetSeq(curr),
+ LocalProfileNotification.Operation.fromString(
+ LpacJni.notificationGetOperationString(
+ curr
+ )
+ ),
+ LpacJni.notificationGetAddress(curr),
+ LpacJni.notificationGetIccid(curr),
+ )
+ )
+ curr = LpacJni.notificationsNext(curr)
+ }
+ return ret.sortedBy { it.seqNumber }.reversed()
+ } finally {
+ LpacJni.notificationsFree(head)
}
- LpacJni.notificationsFree(head)
- return ret.sortedBy { it.seqNumber }.reversed()
}
override val eID: String
- @Synchronized
- get() = LpacJni.es10cGetEid(contextHandle)!!
+ get() = lock.withLock { LpacJni.es10cGetEid(contextHandle)!! }
override val euiccInfo2: EuiccInfo2?
- @Synchronized
- get() {
+ get() = lock.withLock {
val cInfo = LpacJni.es10cexGetEuiccInfo2(contextHandle)
if (cInfo == 0L) return null
- val ret = EuiccInfo2(
- Version(LpacJni.euiccInfo2GetSGP22Version(cInfo)),
- Version(LpacJni.euiccInfo2GetProfileVersion(cInfo)),
- Version(LpacJni.euiccInfo2GetEuiccFirmwareVersion(cInfo)),
- Version(LpacJni.euiccInfo2GetGlobalPlatformVersion(cInfo)),
- LpacJni.euiccInfo2GetSasAcreditationNumber(cInfo),
- Version(LpacJni.euiccInfo2GetPpVersion(cInfo)),
- LpacJni.euiccInfo2GetFreeNonVolatileMemory(cInfo).toInt(),
- LpacJni.euiccInfo2GetFreeVolatileMemory(cInfo).toInt(),
- buildSet {
- var cursor = LpacJni.euiccInfo2GetEuiccCiPKIdListForSigning(cInfo)
- while (cursor != 0L) {
- add(LpacJni.stringDeref(cursor))
- cursor = LpacJni.stringArrNext(cursor)
- }
- },
- buildSet {
- var cursor = LpacJni.euiccInfo2GetEuiccCiPKIdListForVerification(cInfo)
- while (cursor != 0L) {
- add(LpacJni.stringDeref(cursor))
- cursor = LpacJni.stringArrNext(cursor)
- }
- },
- )
-
- LpacJni.euiccInfo2Free(cInfo)
-
- return ret
+ try {
+ return EuiccInfo2(
+ Version(LpacJni.euiccInfo2GetSGP22Version(cInfo)),
+ Version(LpacJni.euiccInfo2GetProfileVersion(cInfo)),
+ Version(LpacJni.euiccInfo2GetEuiccFirmwareVersion(cInfo)),
+ Version(LpacJni.euiccInfo2GetGlobalPlatformVersion(cInfo)),
+ LpacJni.euiccInfo2GetSasAcreditationNumber(cInfo),
+ Version(LpacJni.euiccInfo2GetPpVersion(cInfo)),
+ LpacJni.euiccInfo2GetFreeNonVolatileMemory(cInfo).toInt(),
+ LpacJni.euiccInfo2GetFreeVolatileMemory(cInfo).toInt(),
+ buildSet {
+ var cursor = LpacJni.euiccInfo2GetEuiccCiPKIdListForSigning(cInfo)
+ while (cursor != 0L) {
+ add(LpacJni.stringDeref(cursor))
+ cursor = LpacJni.stringArrNext(cursor)
+ }
+ },
+ buildSet {
+ var cursor = LpacJni.euiccInfo2GetEuiccCiPKIdListForVerification(cInfo)
+ while (cursor != 0L) {
+ add(LpacJni.stringDeref(cursor))
+ cursor = LpacJni.stringArrNext(cursor)
+ }
+ },
+ )
+ } finally {
+ LpacJni.euiccInfo2Free(cInfo)
+ }
}
- @Synchronized
- override fun enableProfile(iccid: String, refresh: Boolean): Boolean =
+ override fun enableProfile(iccid: String, refresh: Boolean): Boolean = lock.withLock {
LpacJni.es10cEnableProfile(contextHandle, iccid, refresh) == 0
+ }
- @Synchronized
- override fun disableProfile(iccid: String, refresh: Boolean): Boolean =
+ override fun disableProfile(iccid: String, refresh: Boolean): Boolean = lock.withLock {
LpacJni.es10cDisableProfile(contextHandle, iccid, refresh) == 0
+ }
- @Synchronized
- override fun deleteProfile(iccid: String): Boolean =
+ override fun deleteProfile(iccid: String): Boolean = lock.withLock {
LpacJni.es10cDeleteProfile(contextHandle, iccid) == 0
+ }
- @Synchronized
- override fun downloadProfile(smdp: String, matchingId: String?, imei: String?,
- confirmationCode: String?, callback: ProfileDownloadCallback) {
+ override fun downloadProfile(
+ smdp: String, matchingId: String?, imei: String?,
+ confirmationCode: String?, callback: ProfileDownloadCallback
+ ) = lock.withLock {
val res = LpacJni.downloadProfile(
contextHandle,
smdp,
@@ -229,18 +244,17 @@ class LocalProfileAssistantImpl(
}
}
- @Synchronized
- override fun deleteNotification(seqNumber: Long): Boolean =
+ override fun deleteNotification(seqNumber: Long): Boolean = lock.withLock {
LpacJni.es10bDeleteNotification(contextHandle, seqNumber) == 0
+ }
- @Synchronized
- override fun handleNotification(seqNumber: Long): Boolean =
+ override fun handleNotification(seqNumber: Long): Boolean = lock.withLock {
LpacJni.handleNotification(contextHandle, seqNumber).also {
Log.d(TAG, "handleNotification $seqNumber = $it")
} == 0
+ }
- @Synchronized
- override fun setNickname(iccid: String, nickname: String) {
+ override fun setNickname(iccid: String, nickname: String) = lock.withLock {
val encoded = try {
Charsets.UTF_8.encode(nickname).array()
} catch (e: CharacterCodingException) {
@@ -259,11 +273,12 @@ class LocalProfileAssistantImpl(
}
override fun euiccMemoryReset() {
- LpacJni.es10cEuiccMemoryReset(contextHandle)
+ lock.withLock {
+ LpacJni.es10cEuiccMemoryReset(contextHandle)
+ }
}
- @Synchronized
- override fun close() {
+ override fun close() = lock.withLock {
if (!finalized) {
LpacJni.euiccFini(contextHandle)
LpacJni.destroyContext(contextHandle)