diff --git a/.forgejo/workflows/build-debug.yml b/.forgejo/workflows/build-debug.yml
index 51e802ca..0818b8b5 100644
--- a/.forgejo/workflows/build-debug.yml
+++ b/.forgejo/workflows/build-debug.yml
@@ -1,7 +1,7 @@
on:
push:
branches:
- - 'master'
+ - '*'
jobs:
build-debug:
@@ -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/.gitmodules b/.gitmodules
index f888959b..863f1850 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,3 @@
[submodule "libs/lpac-jni/src/main/jni/lpac"]
path = libs/lpac-jni/src/main/jni/lpac
- url = https://github.com/estkme/lpac
+ url = https://github.com/estkme-group/lpac.git
diff --git a/.idea/.gitignore b/.idea/.gitignore
index b7c2402a..d2293f69 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 e40be604..00000000
--- 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 f953f9ea..740478af 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@ There are two variants of this project, OpenEUICC and EasyEUICC:
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 +74,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/AndroidManifest.xml b/app-common/src/main/AndroidManifest.xml
index b0324dce..44c82c01 100644
--- a/app-common/src/main/AndroidManifest.xml
+++ b/app-common/src/main/AndroidManifest.xml
@@ -45,8 +45,9 @@
-
-
+
+
+
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 0de99b56..78a8c3f3 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.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.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 2a33c206..eaec5227 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/di/DefaultCustomizableTextProvider.kt b/app-common/src/main/java/im/angry/openeuicc/di/DefaultCustomizableTextProvider.kt
index b4936112..76227fd3 100644
--- a/app-common/src/main/java/im/angry/openeuicc/di/DefaultCustomizableTextProvider.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/di/DefaultCustomizableTextProvider.kt
@@ -8,7 +8,7 @@ open class DefaultCustomizableTextProvider(private val context: Context) : Custo
get() = context.getString(R.string.no_euicc)
override val profileSwitchingTimeoutMessage: String
- get() = context.getString(R.string.enable_disable_timeout)
+ get() = context.getString(R.string.profile_switch_timeout)
override fun formatInternalChannelName(logicalSlotId: Int): String =
context.getString(R.string.channel_name_format, logicalSlotId)
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 1d5f37ff..248afaf5 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
@@ -27,9 +27,16 @@ import kotlinx.coroutines.launch
import net.typeblog.lpac_jni.impl.PKID_GSMA_LIVE_CI
import net.typeblog.lpac_jni.impl.PKID_GSMA_TEST_CI
+// https://euicc-manual.osmocom.org/docs/pki/eum/accredited.json
+// ref:
+private val RE_SAS = Regex(
+ """^[A-Z]{2}-[A-Z]{2}(?:-UP)?-\d{4}T?(?:-\d+)?T?$""",
+ setOf(RegexOption.IGNORE_CASE),
+)
+
class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
companion object {
- private val YES_NO = Pair(R.string.yes, R.string.no)
+ private val YES_NO = Pair(R.string.euicc_info_yes, R.string.euicc_info_no)
}
private lateinit var swipeRefresh: SwipeRefreshLayout
@@ -62,7 +69,7 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
logicalSlotId = intent.getIntExtra("logicalSlotId", 0)
val channelTitle = if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
- getString(R.string.usb)
+ getString(R.string.channel_type_usb)
} else {
appContainer.customizableTextProvider.formatInternalChannelName(logicalSlotId)
}
@@ -109,13 +116,20 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
vendorInfo.firmwareVersion?.let { add(Item(R.string.euicc_info_fw_ver, it)) }
vendorInfo.bootloaderVersion?.let { add(Item(R.string.euicc_info_bl_ver, it)) }
}
- channel.lpa.euiccInfo2.let { info ->
- add(Item(R.string.euicc_info_sgp22_version, info?.sgp22Version.toString()))
- add(Item(R.string.euicc_info_firmware_version, info?.euiccFirmwareVersion.toString()))
- add(Item(R.string.euicc_info_globalplatform_version, info?.globalPlatformVersion.toString()))
- add(Item(R.string.euicc_info_pp_version, info?.ppVersion.toString()))
- add(Item(R.string.euicc_info_sas_accreditation_number, info?.sasAccreditationNumber))
- add(Item(R.string.euicc_info_free_nvram, info?.freeNvram?.let(::formatFreeSpace)))
+ channel.lpa.euiccInfo2?.let { info ->
+ add(Item(R.string.euicc_info_sgp22_version, info.sgp22Version.toString()))
+ add(Item(R.string.euicc_info_firmware_version, info.euiccFirmwareVersion.toString()))
+ add(Item(R.string.euicc_info_gp_version, info.globalPlatformVersion.toString()))
+ 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())) }
+
+ 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)
@@ -123,25 +137,20 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
// FS.27 v2.0, Security Guidelines for UICC Profiles (Page 25 of 27, 2024-01-30)
// https://www.gsma.com/solutions-and-impact/technologies/security/wp-content/uploads/2024/01/FS.27-Security-Guidelines-for-UICC-Credentials-v2.0-FINAL-23-July.pdf#page=25
val resId = when {
- signers.isEmpty() -> R.string.unknown // the case is not mp, but it's is not common
+ signers.isEmpty() -> R.string.euicc_info_unknown // the case is not mp, but it's is not common
PKID_GSMA_LIVE_CI.any(signers::contains) -> R.string.euicc_info_ci_gsma_live
PKID_GSMA_TEST_CI.any(signers::contains) -> R.string.euicc_info_ci_gsma_test
else -> R.string.euicc_info_ci_unknown
}
add(Item(R.string.euicc_info_ci_type, getString(resId)))
}
- val atr = channel.atr?.encodeHex() ?: getString(R.string.information_unavailable)
+ val atr = channel.atr?.encodeHex() ?: getString(R.string.euicc_info_unavailable)
add(Item(R.string.euicc_info_atr, atr, copiedToastResId = R.string.toast_atr_copied))
}
+ @Suppress("SameParameterValue")
private fun formatByBoolean(b: Boolean, res: Pair): String =
- getString(
- if (b) {
- res.first
- } else {
- res.second
- }
- )
+ getString(if (b) res.first else res.second)
inner class EuiccInfoViewHolder(root: View) : ViewHolder(root) {
private val title: TextView = root.requireViewById(R.id.euicc_info_title)
@@ -168,7 +177,7 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
fun bind(item: Item) {
copiedToastResId = item.copiedToastResId
title.setText(item.titleResId)
- content.text = item.content ?: getString(R.string.unknown)
+ content.text = item.content ?: getString(R.string.euicc_info_unknown)
}
}
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt
index 12995ff7..016e96f8 100644
--- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt
@@ -253,7 +253,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
if (!isUsb) {
withContext(Dispatchers.Main) {
AlertDialog.Builder(requireContext()).apply {
- setMessage(R.string.switch_did_not_refresh)
+ setMessage(R.string.profile_switch_did_not_refresh)
setPositiveButton(android.R.string.ok) { dialog, _ ->
dialog.dismiss()
requireActivity().finish()
@@ -347,6 +347,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
private val profileClassLabel: TextView = root.requireViewById(R.id.profile_class_label)
private val profileClass: TextView = root.requireViewById(R.id.profile_class)
private val profileMenu: ImageButton = root.requireViewById(R.id.profile_menu)
+ private val profileSeqNumber: TextView = root.requireViewById(R.id.profile_sequence_number)
init {
iccid.setOnClickListener {
@@ -366,7 +367,9 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
true
}
- profileMenu.setOnClickListener { showOptionsMenu() }
+ profileMenu.setOnClickListener {
+ showOptionsMenu()
+ }
}
private lateinit var profile: LocalProfileInfo
@@ -377,9 +380,9 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
state.setText(
if (profile.isEnabled) {
- R.string.enabled
+ R.string.profile_state_enabled
} else {
- R.string.disabled
+ R.string.profile_state_disabled
}
)
provider.text = profile.providerName
@@ -396,6 +399,13 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
iccid.transformationMethod = PasswordTransformationMethod.getInstance()
}
+ fun setProfileSequenceNumber(index: Int) {
+ profileSeqNumber.text = root.context.getString(
+ R.string.profile_sequence_number_format,
+ index,
+ )
+ }
+
private fun showOptionsMenu() {
// Prevent users from doing multiple things at once
if (invalid || swipeRefresh.isRefreshing) return
@@ -461,6 +471,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
when (holder) {
is ProfileViewHolder -> {
holder.setProfile(profiles[position])
+ holder.setProfileSequenceNumber(position + 1)
}
is FooterViewHolder -> {
holder.attach(footerViews[position - profiles.size])
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt
index 01d0ab2b..b42f4cf1 100644
--- a/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt
@@ -174,7 +174,7 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
// If USB readers exist, add them at the very last
// We use a wrapper fragment to handle logic specific to USB readers
usbDevice?.let {
- val productName = it.productName ?: getString(R.string.usb)
+ val productName = it.productName ?: getString(R.string.channel_type_usb)
newPages.add(Page(EuiccChannelManager.USB_CHANNEL_ID, productName) {
UsbCcidReaderFragment()
})
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt
index 21a2d405..07d5f132 100644
--- a/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt
@@ -60,7 +60,7 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
// This is slightly different from the MainActivity logic
// due to the length (we don't want to display the full USB product name)
val channelTitle = if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
- getString(R.string.usb)
+ getString(R.string.channel_type_usb)
} else {
appContainer.customizableTextProvider.formatInternalChannelName(logicalSlotId)
}
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt
index c588254c..281e6253 100644
--- a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt
@@ -65,7 +65,7 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
super.onViewCreated(view, savedInstanceState)
profileRenameNewName.editText!!.setText(currentName)
toolbar.apply {
- setTitle(R.string.rename)
+ setTitle(R.string.profile_rename)
setNavigationOnClickListener {
if (!renaming) dismiss()
}
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 65541428..7a717ac1 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/DownloadWizardActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt
index 9e312d4e..6574645a 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
@@ -123,8 +123,8 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
// If we get an LPA string from deep-link intents, extract from there.
// Note that `onRestoreInstanceState` could override this with user input,
// but that _is_ the desired behavior.
- val uri = intent.data
- if (uri?.scheme == "lpa") {
+ val uri = intent.data ?: return
+ if (uri.scheme.contentEquals("lpa", ignoreCase = true)) {
val parsed = LPAString.parse(uri.schemeSpecificPart)
state.smdp = parsed.address
state.matchingId = parsed.matchingId
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt
index e282196a..38418684 100644
--- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt
@@ -8,6 +8,7 @@ import android.view.ViewGroup
import android.widget.TextView
import im.angry.openeuicc.common.R
import im.angry.openeuicc.util.*
+import org.json.JSONObject
import java.util.Date
class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardStepFragment() {
@@ -86,9 +87,10 @@ class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardS
ret.appendLine()
val str = resp.data.decodeToString(throwOnInvalidSequence = false)
+
ret.appendLine(
if (str.startsWith('{')) {
- str.prettyPrintJson()
+ JSONObject(str).toString(2)
} else {
str
}
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 342a687f..0048190b 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/DownloadWizardSlotSelectFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt
index 28bc9f00..8097058d 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,6 @@ 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
class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardStepFragment() {
companion object {
@@ -187,12 +186,12 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
}
title.text = if (item.logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
- item.intrinsicChannelName ?: root.context.getString(R.string.usb)
+ item.intrinsicChannelName ?: root.context.getString(R.string.channel_type_usb)
} else {
appContainer.customizableTextProvider.formatInternalChannelName(item.logicalSlotId)
}
eID.text = item.eID
- activeProfile.text = item.enabledProfileName ?: root.context.getString(R.string.unknown)
+ activeProfile.text = item.enabledProfileName ?: root.context.getString(R.string.profile_no_enabled_profile)
freeSpace.text = formatFreeSpace(item.freeSpace)
checkBox.isChecked = adapter.currentSelectedIdx == idx
}
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 00000000..8ce5740b
--- /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 5f4aec44..2fef3db2 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"
@@ -50,9 +52,12 @@ internal object PreferenceConstants {
# eUICC standard
$EUICC_DEFAULT_ISDR_AID
- # eSTK.me
+ # ESTKme AUX (deprecated, use SE0 instead)
A06573746B6D65FFFFFFFF4953442D52
+ # ESTKme SE0
+ A06573746B6D65FFFF4953442D522030
+
# eSIM.me
A0000005591010000000008900000300
@@ -86,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/java/im/angry/openeuicc/util/StringUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt
index 079853eb..57d150b3 100644
--- a/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt
@@ -41,73 +41,3 @@ fun parseIsdrAidList(s: String): List =
.filter(String::isNotEmpty)
.mapNotNull { runCatching(it::decodeHex).getOrNull() }
.ifEmpty { listOf(EUICC_DEFAULT_ISDR_AID.decodeHex()) }
-
-fun String.prettyPrintJson(): String {
- val ret = StringBuilder()
- var inQuotes = false
- var escaped = false
- val indentSymbolStack = ArrayDeque()
-
- val addNewLine = {
- ret.append('\n')
- repeat(indentSymbolStack.size) {
- ret.append('\t')
- }
- }
-
- var lastChar = ' '
-
- for (c in this) {
- when {
- !inQuotes && (c == '{' || c == '[') -> {
- ret.append(c)
- indentSymbolStack.addLast(c)
- addNewLine()
- }
-
- !inQuotes && (c == '}' || c == ']') -> {
- indentSymbolStack.removeLast()
- if (lastChar != ',') {
- addNewLine()
- }
- ret.append(c)
- }
-
- !inQuotes && c == ',' -> {
- ret.append(c)
- addNewLine()
- }
-
- !inQuotes && c == ':' -> {
- ret.append(c)
- ret.append(' ')
- }
-
- inQuotes && c == '\\' -> {
- ret.append(c)
- escaped = true
- continue
- }
-
- !escaped && c == '"' -> {
- ret.append(c)
- inQuotes = !inQuotes
- }
-
- !inQuotes && c == ' ' -> {
- // Do nothing -- we ignore spaces outside of quotes by default
- // This is to ensure predictable formatting
- }
-
- else -> ret.append(c)
- }
-
- if (escaped) {
- escaped = false
- }
-
- lastChar = c
- }
-
- return ret.toString()
-}
\ No newline at end of file
diff --git a/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt
index a73d7fe0..c7c859d9 100644
--- a/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt
@@ -102,8 +102,8 @@ fun T.setupLogSaving(
AlertDialog.Builder(context).apply {
setMessage(R.string.logs_saved_message)
- setNegativeButton(R.string.no) { _, _ -> }
- setPositiveButton(R.string.yes) { _, _ ->
+ setNegativeButton(android.R.string.cancel) { _, _ -> }
+ setPositiveButton(android.R.string.ok) { _, _ ->
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
clipData = ClipData.newUri(context.contentResolver, lastFileName, uri)
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 f1d0852e..c59673b0 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/layout/euicc_profile.xml b/app-common/src/main/res/layout/euicc_profile.xml
index 58d55ab1..021c53bd 100644
--- a/app-common/src/main/res/layout/euicc_profile.xml
+++ b/app-common/src/main/res/layout/euicc_profile.xml
@@ -54,7 +54,7 @@
+
+
diff --git a/app-common/src/main/res/layout/fragment_euicc.xml b/app-common/src/main/res/layout/fragment_euicc.xml
index 4ae7523a..c5fde7bc 100644
--- a/app-common/src/main/res/layout/fragment_euicc.xml
+++ b/app-common/src/main/res/layout/fragment_euicc.xml
@@ -27,6 +27,7 @@
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
+ android:contentDescription="@string/profile_download"
android:src="@drawable/ic_add"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
diff --git a/app-common/src/main/res/menu/activity_isdr_aid_list.xml b/app-common/src/main/res/menu/activity_isdr_aid_list.xml
index 32f178a9..99492d64 100644
--- a/app-common/src/main/res/menu/activity_isdr_aid_list.xml
+++ b/app-common/src/main/res/menu/activity_isdr_aid_list.xml
@@ -9,7 +9,7 @@
\ No newline at end of file
diff --git a/app-common/src/main/res/menu/activity_main.xml b/app-common/src/main/res/menu/activity_main.xml
index 0e002928..c15663f8 100644
--- a/app-common/src/main/res/menu/activity_main.xml
+++ b/app-common/src/main/res/menu/activity_main.xml
@@ -3,7 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
diff --git a/app-common/src/main/res/menu/activity_notifications.xml b/app-common/src/main/res/menu/activity_notifications.xml
index 87f96a66..b80e06e7 100644
--- a/app-common/src/main/res/menu/activity_notifications.xml
+++ b/app-common/src/main/res/menu/activity_notifications.xml
@@ -4,6 +4,6 @@
\ No newline at end of file
diff --git a/app-common/src/main/res/menu/fragment_profile_rename.xml b/app-common/src/main/res/menu/fragment_profile_rename.xml
index bde850f1..f55c56c0 100644
--- a/app-common/src/main/res/menu/fragment_profile_rename.xml
+++ b/app-common/src/main/res/menu/fragment_profile_rename.xml
@@ -4,6 +4,6 @@
\ No newline at end of file
diff --git a/app-common/src/main/res/menu/profile_options.xml b/app-common/src/main/res/menu/profile_options.xml
index 6add53d5..60722d61 100644
--- a/app-common/src/main/res/menu/profile_options.xml
+++ b/app-common/src/main/res/menu/profile_options.xml
@@ -2,18 +2,18 @@
\ 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 d51e2c79..da9d7085 100644
--- a/app-common/src/main/res/values-ja/strings.xml
+++ b/app-common/src/main/res/values-ja/strings.xml
@@ -2,24 +2,25 @@
このアプリでアクセスできるリムーバブル eUICC カードがデバイス上で検出されていません。互換性のあるカード挿入または USB リーダーを接続してください。
この eSIM にはプロファイルがありません。
- 不明
- 情報がありません
- ヘルプ
- スロットを再読み込み
+ 不明
+ 情報がありません
+ ヘルプ
+ スロットを再読み込み
+ 未知
論理スロット %d
- 有効済み
- 無効済み
- プロバイダー:
+ 有効済み
+ 無効済み
+ プロバイダー:
クラス:
テスト中
プロビジョニング
稼働中
- 有効化
- 無効化
- 削除
- 名前を変更
- eSIM チップがプロファイルの切り替えの待機中にタイムアウトしました。これはデバイスのモデムファームウェアのバグの可能性があります。機内モードに切り替えるかアプリを再起動、デバイスを再起動してください。
- 操作は成功しましたが、デバイスのモデムが更新を拒否しました。新しいプロファイルを使用するには機内モードに切り替えるか、再起動する必要があります。
+ 有効化
+ 無効化
+ 削除
+ 名前を変更
+ eSIM チップがプロファイルの切り替えの待機中にタイムアウトしました。これはデバイスのモデムファームウェアのバグの可能性があります。機内モードに切り替えるかアプリを再起動、デバイスを再起動してください。
+ 操作は成功しましたが、デバイスのモデムが更新を拒否しました。新しいプロファイルを使用するには機内モードに切り替えるか、再起動する必要があります。
新しい eSIM プロファイルに切り替えることができません。
確認文字列が一致しません
ICCID をクリップボードにコピーしました
@@ -83,6 +84,7 @@
最終の APDU 例外:
保存
「%s」での診断
+ 別のネットワークに接続し(例:Wi-Fi とデータを切り替える)、もう一度お試しください。
ログは共有したパスに保存されました。別のアプリで共有しますか?
新しいニックネーム
ニックネームを UTF-8 にエンコードできませんでした
@@ -110,16 +112,17 @@
製品ファームウェアバージョン
SGP.22 バージョン
eUICC OS バージョン
- グローバルプラットフォームのバージョン
+ グローバルプラットフォームのバージョン
SAS 認定番号
保護されたプロファイルのバージョン
NVRAM の空き容量 (eSIM プロファイルストレージ)
+ (目安)
証明書発行者 (CI)
GSMA ライブ CI
GSMA テスト CI
不明な eSIM CI
- はい
- いいえ
+ はい
+ いいえ
保存
%s のログ
開発者になるまであと %d ステップです。
@@ -150,6 +153,7 @@
SM-DP+ TLS 証明書を無視する
RSP サーバーで使用される TLS 証明書を受け入れます
一部のブランドの取り外し可能な eUICC では、独自の非標準 ISD-R AID が使用されている場合があり、サードパーティ アプリからアクセスできなくなります。アプリはこのリストに追加された非標準の AID の使用を試みる可能性がありますが、動作することは保証されません。
+ グローバル ES10x MSS
情報
アプリバージョン
ソースコード
@@ -167,6 +171,26 @@
この操作は、デフォルトでは非表示になっている危険な操作です。代わりに、すべての構成ファイルを手動で削除することもできます。
モデムに更新コマンドを送信
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 プロファイルを再発行するには、通信事業者にお問い合わせください。
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 32ced907..4457e6d9 100644
--- a/app-common/src/main/res/values-zh-rCN/strings.xml
+++ b/app-common/src/main/res/values-zh-rCN/strings.xml
@@ -2,20 +2,21 @@
在此设备上未检测到此应用程序可访问的可插拔 eUICC 卡。请插入兼容卡或 USB 读卡器。
此 eSIM 上还没有配置文件
- 未知
- 帮助
- 重新加载卡槽
+ 未知
+ 帮助
+ 重新加载卡槽
+ 未知
逻辑卡槽 %d
- 已启用
- 已禁用
- 提供商:
+ 已启用
+ 已禁用
+ 提供商:
类型:
- 启用
- 禁用
- 删除
- 重命名
- 等待 eSIM 芯片切换配置文件时超时。这可能是您手机基带固件中的一个错误。请尝试切换飞行模式、重新启动应用程序或重新启动手机
- 操作成功, 但是您手机的基带拒绝刷新。您可能需要切换飞行模式或重新启动,以便使用新的配置文件。
+ 启用
+ 禁用
+ 删除
+ 重命名
+ 等待 eSIM 芯片切换配置文件时超时。这可能是您手机基带固件中的一个错误。请尝试切换飞行模式、重新启动应用程序或重新启动手机
+ 操作成功, 但是您手机的基带拒绝刷新。您可能需要切换飞行模式或重新启动,以便使用新的配置文件。
无法切换到新的 eSIM 配置文件。
输入的确认文本不匹配
已复制 ICCID 到剪贴板
@@ -46,6 +47,7 @@
IMEI (可选)
剩余空间不足
当前芯片的剩余空间不足,可能导致配置下载失败。\n是否继续下载?
+ 请连接到其他网络(例如在 Wi-Fi 和数据之间切换)后重试。
日志已保存到指定路径。需要通过其他 App 分享吗?
新昵称
无法将昵称编码为 UTF-8
@@ -83,6 +85,7 @@
日志
查看应用程序的最新调试日志
某些品牌的可移除 eUICC 可能会使用自己的非标准 ISD-R AID,导致第三方应用无法访问。此 App 可以尝试使用此列表中添加的非标准 AID,但不能保证它们一定有效。
+ 全局 ES10x MSS
信息
App 版本
源码
@@ -132,16 +135,17 @@
可插拔
SGP.22 版本
eUICC OS 版本
- GlobalPlatform 版本
+ GlobalPlatform 版本
SAS 认证号码
Protected Profile 版本
NVRAM 剩余空间 (eSIM 存储容量)
+ (仅供参考)
证书签发者 (CI)
GSMA 生产环境 CI
GSMA 测试 CI
未知 eSIM CI
- 是
- 否
+ 是
+ 否
还有 %d 步成为开发者
你现在是开发者了!
语言
@@ -152,7 +156,7 @@
在配置文件列表中包括非生产环境的配置文件
无视 SM-DP+ 的 TLS 证书
允许 RSP 服务器使用任意证书
- 无信息
+ 无信息
输入的确认文本不匹配
此芯片已被擦除
正在擦除 eSIM 芯片
@@ -167,6 +171,26 @@
此操作是默认隐藏的危险操作。作为替代方案,您可以手动删除所有配置文件。
向基带发送刷新命令
自定义 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 5136bf73..ef6c842b 100644
--- a/app-common/src/main/res/values-zh-rTW/strings.xml
+++ b/app-common/src/main/res/values-zh-rTW/strings.xml
@@ -2,20 +2,21 @@
在此裝置上未檢測到此應用程式可訪問的可插拔 eUICC 卡。請插入相容卡或 USB 晶片讀卡機。
此 eSIM 上還沒有設定檔
- 未知
- 幫助
- 重新載入卡槽
+ 未知
+ 幫助
+ 重新載入卡槽
+ 未知
虛擬卡槽 %d
- 已啟用
- 已停用
- 電信業者:
+ 已啟用
+ 已停用
+ 電信業者:
類型:
- 啟用
- 停用
- 刪除
- 重新命名
- 等待 eSIM 切換設定檔時逾時。這可能是您手機基頻處理器韌體中的一個錯誤。請嘗試切換飛航模式、重新啟動應用程式或重新啟動手機
- 操作成功, 但是您手機的基頻處理器沒有重新整理。您可能需要切換飛航模式或重新啟動,以便使用新的設定檔。
+ 啟用
+ 停用
+ 刪除
+ 重新命名
+ 等待 eSIM 切換設定檔時逾時。這可能是您手機基頻處理器韌體中的一個錯誤。請嘗試切換飛航模式、重新啟動應用程式或重新啟動手機
+ 操作成功, 但是您手機的基頻處理器沒有重新整理。您可能需要切換飛航模式或重新啟動,以便使用新的設定檔。
無法切換到新的 eSIM 設定檔。
輸入的確認文字不匹配
已複製 ICCID 到剪貼簿
@@ -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 版本
原始碼
@@ -132,16 +135,17 @@
可插拔
SGP.22 版本
eUICC OS 版本
- GlobalPlatform 版本
+ GlobalPlatform 版本
SAS 認證號碼
Protected Profile 版本
NVRAM 剩餘空間 (eSIM 儲存容量)
+ (僅供參考)
證書簽發者 (CI)
GSMA 生產環境 CI
GSMA 測試 CI
未知 eSIM CI
- 是
- 否
+ 是
+ 否
還有 %d 步成為開發者
您現在是開發者了!
語言
@@ -152,7 +156,7 @@
在設定檔列表中包括非生產環境的設定檔
忽略 SM-DP+ 的 TLS 證書
允許 RSP 伺服器使用任意證書
- 無資訊
+ 無資訊
輸入的確認文字不匹配
此晶片已被擦除
正在擦除 eSIM 晶片
@@ -167,6 +171,26 @@
此操作是預設隱藏的危險操作。作為替代方案,您可以手動刪除所有設定檔。
向基帶發送刷新命令
自訂 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 05ca15a1..a7f2c10e 100644
--- a/app-common/src/main/res/values/strings.xml
+++ b/app-common/src/main/res/values/strings.xml
@@ -2,31 +2,34 @@
No removable eUICC card accessible by this app is detected on this device. Insert a compatible card or a USB reader.
No profiles (yet) on this eSIM.
- Unknown
- Information Unavailable
- Help
- Reload Slots
+
+ Help
+
+ Reload Slots
+ Unknown
Logical Slot %d
- USB
- OpenMobile API (OMAPI)
+ USB
+ OpenMobile API (OMAPI)
- Enabled
- Disabled
- Provider:
+
+ Enabled
+ Disabled
+ Provider:
Class:
Testing
Provisioning
Operational
- ICCID:
+ ICCID:
+ #%d
- Enable
- Disable
- Delete
- Rename
+ Enable
+ Disable
+ Delete
+ Rename
- Timed out waiting for the eSIM chip to switch profiles. This may be a bug in your phone\'s modem firmware. Try toggling airplane mode, restarting the application, or rebooting the phone.
- The operation was successful, but your phone\'s modem refused to refresh. You might need to toggle airplane mode or reboot in order to use the new profile.
+ Timed out waiting for the eSIM chip to switch profiles. This may be a bug in your phone\'s modem firmware. Try toggling airplane mode, restarting the application, or rebooting the phone.
+ The operation was successful, but your phone\'s modem refused to refresh. You might need to toggle airplane mode or reboot in order to use the new profile.
Cannot switch to new eSIM profile.
Confirmation string mismatch
@@ -101,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?
@@ -137,10 +161,11 @@
ISD-R AID
SGP.22 Version
eUICC OS Version
- GlobalPlatform Version
+ GlobalPlatform Version
SAS Accreditation Number
Protected Profile Version
Free NVRAM (eSIM profile storage)
+ (for reference only)
Certificate Issuer (CI)
GSMA Live CI
GSMA Test CI
@@ -154,8 +179,11 @@
I CONFIRM TO ERASE THE CHIP WHOSE EID ENDS WITH %s AND UNDERSTAND THAT THIS IS IRREVERSIBLE
Erase
- Yes
- No
+
+ Yes
+ No
+ Unknown
+ Information Unavailable
Save
Logs at %s
@@ -163,10 +191,9 @@
You are %d steps away from being a developer.
You are now a developer!
- Reset
-
ISD-R AID List
Saved custom ISD-R AID list.
+ Reset
Settings
Notifications
@@ -195,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 Speed
+ - Compatibility Mode
+
+
+ - 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 690a1201..831b04db 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" />
+
+
+ android:label="@string/quick_compatibility" />
by lazy { getCompatibilityChecks(this) }
- private val adapter = CompatibilityChecksAdapter()
-
- override fun onCreate(savedInstanceState: Bundle?) {
- enableEdgeToEdge()
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_compatibility_check)
- setSupportActionBar(requireViewById(im.angry.openeuicc.common.R.id.toolbar))
- setupToolbarInsets()
- supportActionBar!!.setDisplayHomeAsUpEnabled(true)
-
- compatibilityCheckList = requireViewById(R.id.recycler_view).also {
- it.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
- it.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
- it.adapter = adapter
- }
-
- setupRootViewInsets(compatibilityCheckList)
- }
-
- @SuppressLint("NotifyDataSetChanged")
- override fun onStart() {
- super.onStart()
- lifecycleScope.launch {
- compatibilityChecks.executeAll { adapter.notifyDataSetChanged() }
- }
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean =
- when (item.itemId) {
- android.R.id.home -> {
- finish()
- true
- }
- else -> super.onOptionsItemSelected(item)
- }
-
- inner class ViewHolder(private val root: View): RecyclerView.ViewHolder(root) {
- private val titleView: TextView = root.requireViewById(R.id.compatibility_check_title)
- private val descView: TextView = root.requireViewById(R.id.compatibility_check_desc)
- private val statusContainer: ViewGroup = root.requireViewById(R.id.compatibility_check_status_container)
-
- fun bindItem(item: CompatibilityCheck) {
- titleView.text = item.title
- descView.text = Html.fromHtml(item.description, Html.FROM_HTML_MODE_COMPACT)
-
- statusContainer.children.forEach {
- it.isVisible = false
- }
-
- val viewId = when (item.state) {
- CompatibilityCheck.State.SUCCESS -> R.id.compatibility_check_checkmark
- CompatibilityCheck.State.FAILURE -> R.id.compatibility_check_error
- CompatibilityCheck.State.FAILURE_UNKNOWN -> R.id.compatibility_check_unknown
- else -> R.id.compatibility_check_progress_bar
- }
- root.requireViewById(viewId).isVisible = true
- }
- }
-
- inner class CompatibilityChecksAdapter: RecyclerView.Adapter() {
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
- ViewHolder(layoutInflater.inflate(R.layout.compatibility_check_item, parent, false))
-
- override fun getItemCount(): Int =
- compatibilityChecks.indexOfLast { it.state != CompatibilityCheck.State.NOT_STARTED } + 1
-
- override fun onBindViewHolder(holder: ViewHolder, position: Int) {
- holder.bindItem(compatibilityChecks[position])
- }
- }
-}
\ No newline at end of file
diff --git a/app-unpriv/src/main/java/im/angry/openeuicc/ui/QuickCompatibilityActivity.kt b/app-unpriv/src/main/java/im/angry/openeuicc/ui/QuickCompatibilityActivity.kt
new file mode 100644
index 00000000..d5e599fd
--- /dev/null
+++ b/app-unpriv/src/main/java/im/angry/openeuicc/ui/QuickCompatibilityActivity.kt
@@ -0,0 +1,24 @@
+package im.angry.openeuicc.ui
+
+import android.os.Bundle
+import androidx.activity.enableEdgeToEdge
+import androidx.appcompat.app.AppCompatActivity
+import im.angry.easyeuicc.R
+import im.angry.openeuicc.di.UnprivilegedUiComponentFactory
+import im.angry.openeuicc.util.OpenEuiccContextMarker
+
+class QuickCompatibilityActivity : AppCompatActivity(), OpenEuiccContextMarker {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ setContentView(R.layout.activity_quick_compatibility)
+
+ val quickCompatibilityFragment =
+ (appContainer.uiComponentFactory as UnprivilegedUiComponentFactory)
+ .createQuickCompatibilityFragment()
+
+ supportFragmentManager.beginTransaction()
+ .replace(R.id.quick_compatibility_container, quickCompatibilityFragment)
+ .commit()
+ }
+}
diff --git a/app-unpriv/src/main/java/im/angry/openeuicc/ui/QuickCompatibilityFragment.kt b/app-unpriv/src/main/java/im/angry/openeuicc/ui/QuickCompatibilityFragment.kt
new file mode 100644
index 00000000..9b417306
--- /dev/null
+++ b/app-unpriv/src/main/java/im/angry/openeuicc/ui/QuickCompatibilityFragment.kt
@@ -0,0 +1,186 @@
+package im.angry.openeuicc.ui
+
+import android.content.pm.PackageManager
+import android.icu.text.ListFormatter
+import android.os.Build
+import android.os.Bundle
+import android.se.omapi.Reader
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.CheckBox
+import android.widget.TextView
+import androidx.core.view.isVisible
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.lifecycleScope
+import im.angry.easyeuicc.R
+import im.angry.openeuicc.util.EUICC_DEFAULT_ISDR_AID
+import im.angry.openeuicc.util.UnprivilegedEuiccContextMarker
+import im.angry.openeuicc.util.connectSEService
+import im.angry.openeuicc.util.decodeHex
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+
+open class QuickCompatibilityFragment : Fragment(), UnprivilegedEuiccContextMarker {
+ companion object {
+ enum class Compatibility {
+ COMPATIBLE,
+ NOT_COMPATIBLE,
+ }
+
+ data class CompatibilityResult(
+ val compatibility: Compatibility,
+ val slotsOmapi: List = emptyList(),
+ val slotsIsdr: List = emptyList()
+ )
+ }
+
+ private val conclusion: TextView by lazy {
+ requireView().requireViewById(R.id.quick_compatibility_conclusion)
+ }
+
+ private val resultSlots: TextView by lazy {
+ requireView().requireViewById(R.id.quick_compatibility_result_slots)
+ }
+
+ private val resultSlotsIsdr: TextView by lazy {
+ requireView().requireViewById(R.id.quick_compatibility_result_slots_isdr)
+ }
+
+ private val resultNotes: TextView by lazy {
+ requireView().requireViewById(R.id.quick_compatibility_result_notes)
+ }
+
+ private val skipCheckBox: CheckBox by lazy {
+ requireView().requireViewById(R.id.quick_compatibility_skip)
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View = inflater.inflate(R.layout.fragment_quick_compatibility, container, false).apply {
+ requireViewById(R.id.quick_compatibility_device_information)
+ .text = formatDeviceInformation()
+ requireViewById
diff --git a/app-unpriv/src/main/res/values-zh-rCN/strings.xml b/app-unpriv/src/main/res/values-zh-rCN/strings.xml
index 8d3060d6..cd66a435 100644
--- a/app-unpriv/src/main/res/values-zh-rCN/strings.xml
+++ b/app-unpriv/src/main/res/values-zh-rCN/strings.xml
@@ -1,32 +1,17 @@
兼容性检查
打开 SIM 卡应用程序
- 系统功能
- 您的设备是否具有管理可插拔 eUICC 卡所需的所有功能。例如,基本的电话功能和 OMAPI 支持。
- 您的设备没有电话功能。
- 您的设备/系统未声明支持 OMAPI。这可能是由于缺少硬件支持,或者可能仅仅是由于缺少标志。请参阅以下两项检查以确定 OMAPI 是否确实受支持。
- OMAPI 连接
- 您的设备是否允许通过 OMAPI 访问 SIM 卡上的安全元件?
- 无法通过 OMAPI 检测到 SIM 卡的 Secure Element。如果您尚未在此设备中插入 SIM 卡,请尝试插入一张 SIM 卡并重试此检查。
- 已成功检测到可访问 Secure Element 的卡槽,但仅限于以下 SIM 卡槽:SIM%s。
- ISD-R 通道访问
- 您的设备是否支持通过 OMAPI 打开 eSIM 的 ISD-R (管理) 通道?
- 无法确定是否支持通过 OMAPI 进行 ISD-R 访问。如果尚未插入,您可能需要插入 SIM 卡 (任何 SIM 卡都可以) 重试。
- OMAPI 只能在以下 SIM 插槽上访问 ISD-R:SIM%s。
- 不在已知的 BUG 名单中
- 确保您的设备不存在与可插拔 eSIM 相关的错误。
- 糟糕,您的设备在访问可插拔 eSIM 时存在错误。这并不表示完全无法使用,但我们不保证该应用在您设备上的行为。
- USB 读卡器支持
- 您的设备是否支持通过 USB 读卡器管理 eSIM?
- 您可以通过此设备上的标准 USB CCID 读取器管理 eSIM (即使您在这里有任何其他检查项失败)。请插入读卡器,然后打开此应用程序以这种方式管理 eSIM。
- 您的设备不支持 USB 读卡器。
- 结论 (USB 读卡器以外)
- 根据之前的所有检查,您的设备与可插拔 eSIM 卡兼容的可能性有多大?
- 您可以使用和管理插入此设备的可插拔 eSIM 卡。
- 已知您的设备在访问可插拔 eSIM 卡时存在问题。\n%s
- 我们无法确定是否可以在您的设备上管理可插拔 eSIM 卡。不过,您的设备确实声明支持 OMAPI,因此它工作的可能性略高。\n%s
- 我们无法确定是否可以在您的设备上管理可插拔 eSIM 卡。由于您的设备未声明支持OMAPI,因此更有可能不支持在此设备上管理可插拔 eSIM。\n%s
- 我们无法确定是否可以在您的设备上管理可插拔 eSIM 卡。\n%s
- 然而,已经加载了eSIM配置文件的可插拔 eSIM 卡仍然可以工作; 即使无法在装置上直接管理可插拔 eSIM 卡中的配置文件,您仍然可以使用 USB 卡读卡器来管理配置文件。
ARA-M SHA-1 已拷贝到剪贴板
+ 请启用您的“%s”应用程序
+ 简易兼容性检测
+ 您的手机可以管理兼容 %s 的卡片
+ 您的手机与 %s 不兼容
+ 您的手机与 %s 不完全兼容,但可以使用 USB 读卡器获得几乎全部功能
+ 可读取的卡槽: %s
+ ISD-R 访问: %s
+ 注意:以上结果仅供参考。即使某些卡槽没有被列举出来,插卡后也可能可用。
+ 注意:如果您目前没有插卡,请插任意 SIM 卡后重试兼容性检测。
+ 继续
+ 不再显示此消息
+ 未知
\ No newline at end of file
diff --git a/app-unpriv/src/main/res/values-zh-rTW/strings.xml b/app-unpriv/src/main/res/values-zh-rTW/strings.xml
index b8d0eb83..ab64d6bd 100644
--- a/app-unpriv/src/main/res/values-zh-rTW/strings.xml
+++ b/app-unpriv/src/main/res/values-zh-rTW/strings.xml
@@ -1,32 +1,17 @@
相容性檢查
啟動 SIM 卡應用程式
- 系統功能
- 您的裝置是否具有管理可插拔 eUICC 卡所需的所有功能。例如,基本的電話功能和 OMAPI 支援。
- 您的裝置沒有電話功能。
- 您的裝置/系統未宣告支援 OMAPI。這可能是由於缺少硬體支援,或者可能僅僅是由於缺少標誌。請參閱以下兩項檢查以確定 OMAPI 是否確實受支援。
- OMAPI 連線
- 您的裝置是否允許透過 OMAPI 存取 SIM 卡上的安全元件?
- 無法透過 OMAPI 偵測到 SIM 卡的 Secure Element。如果您尚未在此裝置中插入 SIM 卡,請嘗試插入一張 SIM 卡並重試此檢查。
- 已成功檢測到可存取 Secure Element 的卡槽,但僅限於以下 SIM 卡槽:SIM%s。
- ISD-R 通道存取
- 您的裝置是否支援透過 OMAPI 開啟 eSIM 的 ISD-R (管理) 通道?
- 無法確定是否支援透過 OMAPI 進行 ISD-R 的存取。如果尚未插入,您可能需要插入 SIM 卡 (任何 SIM 卡都可以) 重試。
- OMAPI 只能在以下 SIM 插槽上存取 ISD-R:SIM%s。
- 不在已知錯誤清單中
- 確保您的裝置不存在與可插拔 eSIM 相關的錯誤。
- 很抱歉,您的裝置在存取可插拔 eSIM 時存在錯誤。這並不表示完全無法使用,但我們不保證該應用在您裝置上的行為。
- USB 晶片讀卡機支援
- 您的裝置是否支援透過 USB 晶片讀卡機管理 eSIM?
- 您可以透過此裝置上的標準 USB CCID 讀卡機管理 eSIM (即使您在這裡有任何其他檢查項失敗)。請插入讀卡機,然後開啟此應用程式以這種方式管理 eSIM。
- 您的裝置不支援 USB 晶片讀卡機。
- 結論 (USB 晶片讀卡機以外)
- 根據之前的所有檢查,您的裝置與可插拔 eSIM 卡相容的可能性有多大?
- 您可以使用和管理插入此裝置的可插拔 eSIM 卡。
- 已知您的裝置在存取可插拔 eSIM 卡時存在問題。\n%s
- 我們無法確定是否可以在您的裝置上管理可插拔 eSIM 卡。不過,您的裝置確實宣告支援 OMAPI,因此它工作的可能性略高。\n%s
- 我們無法確定是否可以在您的裝置上管理可插拔 eSIM 卡。由於您的裝置未宣告支援OMAPI,因此更有可能不支援在此裝置上管理可插拔 eSIM。\n%s
- 我們無法確定是否可以在您的裝置上管理可插拔 eSIM 卡。\n%s
- 然而,已經載入了eSIM設定檔的可插拔 eSIM 卡仍然可以工作; 即使無法在裝置上直接管理可插拔 eSIM 卡中的設定檔,您仍然可以使用 USB 卡讀卡機來管理設定檔。
ARA-M SHA-1 已複製到剪貼簿
+ 請啟用您的“%s”應用程式
+ 簡易相容性檢測
+ 您的手機可以管理相容 %s 的卡片
+ 您的手機與 %s 不相容
+ 您的手機與 %s 不完全相容,但可以使用 USB 讀卡機獲得幾乎全部功能
+ 可讀取的卡槽: %s
+ ISD-R 訪問: %s
+ 注意:以上結果僅供參考。即使某些卡槽沒有被列舉出來,插卡後也可能可用。
+ 注意:如果您目前沒有插卡,請插任何 SIM 卡後重試相容性檢測。
+ 繼續
+ 不再顯示此訊息
+ 未知
\ No newline at end of file
diff --git a/app-unpriv/src/main/res/values/strings.xml b/app-unpriv/src/main/res/values/strings.xml
index 43bf44fa..76d1c76d 100644
--- a/app-unpriv/src/main/res/values/strings.xml
+++ b/app-unpriv/src/main/res/values/strings.xml
@@ -11,32 +11,16 @@
ARA-M SHA-1 copied to clipboard
Please ENABLE your \"%s\" application
-
- System Features
- Whether your device has all the required features for managing removable eUICC cards. For example, basic telephony and OMAPI support.
- Your device has no telephony features.
- Your device / system does not declare support for OMAPI. This could be due to missing support from hardware, or it could be simply due to a missing flag. See the following two checks to determine whether OMAPI is actually supported or not.
- OMAPI Connectivity
- Does your device allow access to Secure Elements on SIM cards via OMAPI?
- 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.
- Successfully detected Secure Element access, but only for the following SIM slots: <b>SIM%s</b>.
- ISD-R Channel Access
- Does your device support opening an ISD-R (management) channel to eSIMs via OMAPI?
- 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.
- OMAPI access to ISD-R is only possible on the following SIM slots: <b>SIM%s</b>.
- Not on the Known Broken List
- Making sure your device is not known to have bugs associated with removable eSIMs.
- 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.
- USB Card Reader Support
- Does your device support managing eSIMs via USB card readers?
- You can manage eSIMs through standard USB CCID readers on this device (even if you had any other check items fail here). Insert the card reader and then open this app to manage eSIMs in this way.
- Your device does not support acting as a USB host.
- Verdict (non-USB)
- Based on all previous checks, how likely is your device to be compatible with managing inserted removable eSIMs?
- You can likely use and manage removable eSIMs inserted into this device.
- Your device is known to be buggy when accessing inserted removable eSIMs.\n%s
- We cannot determine whether inserted removable eSIMs can be managed on your device. Your device does declare support for OMAPI, though, so it is slightly more likely that it will work.\n%s
- We cannot determine whether inserted removable eSIMs can be managed on your device. Since your device does not declare support for OMAPI, it is more likely that managing removable eSIMs on this device is unsupported.\n%s
- We cannot determine whether inserted removable eSIMs can be managed on your device.\n%s
- However, a removable eSIM that has already been loaded with an eSIM profile will still work; you can also most likely use a USB card reader plugged into this device to manage profiles, even if you cannot manage one inserted into your device.
+
+ Quick Compatibility Check
+ Your smartphone can manage %s-compatible cards
+ Your smartphone is not compatible with %s
+ Your smartphone is not fully compatible with %s. However, you can still use a USB smart card reader for near-full functionality.
+ Accessible slots: %s
+ ISD-R access: %s
+ Note: these results are for reference only. Even if a SIM slot is not listed above, it may be compatible as well once a SIM card is inserted.
+ Note: if you currently do not have any SIM card inserted, try the compatibility check again after inserting one. Any SIM card will do.
+ Continue
+ Don\'t show this message again
+ Unknown
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index b9c2100f..40122270 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 {
@@ -23,6 +24,9 @@ android {
}
buildTypes {
+ defaultConfig {
+ versionNameSuffix = "-priv"
+ }
release {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
@@ -45,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 00000000..707b401c
--- /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/{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 00000000..28b48e58
--- /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 00000000..1eb02001
--- /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 68eddefd..876387fc 100644
--- a/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt
+++ b/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt
@@ -2,7 +2,6 @@ package im.angry.openeuicc.core
import android.content.Context
import android.util.Log
-import im.angry.openeuicc.OpenEuiccApplication
import im.angry.openeuicc.R
import im.angry.openeuicc.util.*
import kotlinx.coroutines.flow.first
@@ -32,7 +31,7 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto
)
try {
return EuiccChannelImpl(
- context.getString(R.string.telephony_manager),
+ context.getString(R.string.channel_type_telephony_manager),
port,
intrinsicChannelName = null,
TelephonyManagerApduInterface(
@@ -43,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 acc1728f..fbf5c53d 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -1,7 +1,7 @@
このデバイスで eUICC が見つかりません。\nデバイスによってはアプリのメニューからデュアル SIM を有効化する必要があります。
- TelephonyManager (特権)
+ TelephonyManager (特権)
デュアル SIM
DSDS の状態が切り替わりました。モデムが再起動するまでお待ちください。
このスロットは MEP (Multiple Enabled Profiles) をサポートしています。この機能を有効化または無効化するには「スロットマッピングツール」を使用してください。
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index d6befc2d..acd9a610 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -16,7 +16,7 @@
您的设备支持 eSIM。要连接到移动网络,请下载运营商发布的 eSIM,或插入物理 SIM 卡。
跳过
下载 eSIM
- TelephonyManager (特权)
+ TelephonyManager (特权)
全局使用 TelephonyManager
在默认情况下,可移除 eUICC 将仅使用 OMAPI。这与非特权模式 (EasyEUICC) 一致。在某些设备上 OMAPI 可能存在问题 -- 选择此选项以强制使用 TelephonyManager。
\ No newline at end of file
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index 10285d3c..52b5aa84 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -16,7 +16,7 @@
您的裝置支援 eSIM。要連線到行動網路,請下載電信業者釋出的 eSIM,或插入實體 SIM 卡。
跳過
下載 eSIM
- TelephonyManager (特權)
+ TelephonyManager (特權)
全域使用 TelephonyManager
在預設情況下,可移除 eUICC 將僅使用 OMAPI。這與非特權模式 (EasyEUICC) 一致。在某些裝置上 OMAPI 可能有問題 -- 選擇此選項以強制使用 TelephonyManager。
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ddf17e40..2c26bc3f 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,7 +1,7 @@
OpenEUICC
No eUICC found on this device.\nOn some devices, you may need to enable dual SIM first in the menu of this app.
- TelephonyManager (Privileged)
+ TelephonyManager (Privileged)
Dual SIM
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 00000000..6245d8c9
--- /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")
+ dir.file("module.prop").asFile.writeText(moduleProp.get().map { (k, v) -> "$k=$v" }.joinToString("\n"))
+ }
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/im/angry/openeuicc/build/Versioning.kt b/buildSrc/src/main/kotlin/im/angry/openeuicc/build/Versioning.kt
index 24f02358..ef07c430 100644
--- a/buildSrc/src/main/kotlin/im/angry/openeuicc/build/Versioning.kt
+++ b/buildSrc/src/main/kotlin/im/angry/openeuicc/build/Versioning.kt
@@ -16,7 +16,7 @@ val Project.gitVersionCode: Int
standardOutput = stdout
}
stdout.toString("utf-8").trim('\n').toInt()
- } catch (e: Exception) {
+ } catch (_: Exception) {
0
}
@@ -29,7 +29,7 @@ val Project.gitVersionName: String
standardOutput = stdout
}
stdout.toString("utf-8").trim('\n')
- } catch (e: Exception) {
+ } catch (_: Exception) {
"Unknown"
}
@@ -38,7 +38,7 @@ class MyVersioningPlugin: Plugin {
target.configure {
defaultConfig {
versionCode = target.gitVersionCode
- versionName = target.gitVersionName
+ versionName = target.gitVersionName.removePrefix("unpriv-")
}
applicationVariants.all {
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 3674f4f1..ce097176 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)
diff --git a/libs/lpac-jni/src/main/jni/Application.mk b/libs/lpac-jni/src/main/jni/Application.mk
index 21121965..c1d3766d 100644
--- a/libs/lpac-jni/src/main/jni/Application.mk
+++ b/libs/lpac-jni/src/main/jni/Application.mk
@@ -1,4 +1,5 @@
APP_ABI := all
APP_SHORT_COMMANDS := true
APP_CFLAGS := -Wno-compound-token-split-by-macro
-APP_LDFLAGS := -Wl,--build-id=none -z muldefs
\ No newline at end of file
+APP_LDFLAGS := -Wl,--build-id=none -z muldefs
+APP_SUPPORT_FLEXIBLE_PAGE_SIZES := true
diff --git a/libs/lpac-jni/src/main/jni/lpac-jni.mk b/libs/lpac-jni/src/main/jni/lpac-jni.mk
index c0bcee72..dad173c3 100644
--- a/libs/lpac-jni/src/main/jni/lpac-jni.mk
+++ b/libs/lpac-jni/src/main/jni/lpac-jni.mk
@@ -1,4 +1,5 @@
LOCAL_PATH := $(call my-dir)
+LOCAL_LDFLAGS += "-Wl,-z,max-page-size=16384"
# function to find all *.c files under a directory
define all-c-files-under
diff --git a/libs/lpac-jni/src/main/jni/lpac-jni/interface-wrapper.c b/libs/lpac-jni/src/main/jni/lpac-jni/interface-wrapper.c
index a61fc967..007e80de 100644
--- a/libs/lpac-jni/src/main/jni/lpac-jni/interface-wrapper.c
+++ b/libs/lpac-jni/src/main/jni/lpac-jni/interface-wrapper.c
@@ -80,7 +80,7 @@ apdu_interface_transmit(struct euicc_ctx *ctx, uint8_t **rx, uint32_t *rx_len, c
LPAC_JNI_EXCEPTION_RETURN;
*rx_len = (*env)->GetArrayLength(env, ret);
*rx = calloc(*rx_len, sizeof(uint8_t));
- (*env)->GetByteArrayRegion(env, ret, 0, *rx_len, *rx);
+ (*env)->GetByteArrayRegion(env, ret, 0, *rx_len, (jbyte *) *rx);
(*env)->DeleteLocalRef(env, txArr);
(*env)->DeleteLocalRef(env, ret);
return 0;
@@ -113,7 +113,7 @@ http_interface_transmit(struct euicc_ctx *ctx, const char *url, uint32_t *rcode,
jbyteArray rxArr = (jbyteArray) (*env)->GetObjectField(env, ret, field_resp_data);
*rx_len = (*env)->GetArrayLength(env, rxArr);
*rx = calloc(*rx_len, sizeof(uint8_t));
- (*env)->GetByteArrayRegion(env, rxArr, 0, *rx_len, *rx);
+ (*env)->GetByteArrayRegion(env, rxArr, 0, *rx_len, (jbyte *) *rx);
(*env)->DeleteLocalRef(env, txArr);
(*env)->DeleteLocalRef(env, rxArr);
(*env)->DeleteLocalRef(env, headersArr);