diff --git a/app-common/src/main/AndroidManifest.xml b/app-common/src/main/AndroidManifest.xml
index f53e6ff..6edaaf1 100644
--- a/app-common/src/main/AndroidManifest.xml
+++ b/app-common/src/main/AndroidManifest.xml
@@ -30,7 +30,20 @@
+ android:label="@string/download_wizard">
+
+
+
+
+
+
+
+
+
+
+
+
+
- if (!channel.lpa.switchProfile(iccid, enable, refresh = true)) {
- // Sometimes, we *can* enable or disable the profile, but we cannot
- // send the refresh command to the modem because the profile somehow
- // makes the modem "busy". In this case, we can still switch by setting
- // refresh to false, but then the switch cannot take effect until the
- // user resets the modem manually by toggling airplane mode or rebooting.
- Pair(channel.lpa.switchProfile(iccid, enable, refresh = false), false)
- } else {
- Pair(true, true)
- }
- }
+ suspend fun onSwitch(channel: EuiccChannel): Pair {
+ val refresh = preferenceRepository.refreshAfterSwitchFlow.first()
+ val response = channel.lpa.switchProfile(iccid, enable, refresh)
+ if (response || !refresh) return Pair(response, refresh)
+ // refresh failed, but refresh was requested
+ // Sometimes, we *can* enable or disable the profile, but we cannot
+ // send the refresh command to the modem because the profile somehow
+ // makes the modem "busy". In this case, we can still switch by setting
+ // refresh to false, but then the switch cannot take effect until the
+ // user resets the modem manually by toggling airplane mode or rebooting.
+ return Pair(channel.lpa.switchProfile(iccid, enable, refresh = false), false)
+ }
- if (!res) {
+ euiccChannelManager.beginTrackedOperation(slotId, portId) {
+ val (response, refreshed) =
+ euiccChannelManager.withEuiccChannel(slotId, portId, ::onSwitch)
+
+ if (!response) {
throw RuntimeException("Could not switch profile")
}
@@ -495,4 +496,15 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
preferenceRepository.notificationSwitchFlow.first()
}
}
+
+ fun launchMemoryReset(slotId: Int, portId: Int): ForegroundTaskSubscriberFlow =
+ launchForegroundTask(
+ getString(R.string.task_euicc_memory_reset),
+ getString(R.string.task_euicc_memory_reset_failure),
+ R.drawable.ic_euicc_memory_reset
+ ) {
+ euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
+ channel.lpa.euiccMemoryReset()
+ }
+ }
}
\ No newline at end of file
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 842f4ec..12995ff 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
@@ -38,8 +38,10 @@ import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
@@ -55,6 +57,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
private lateinit var fab: FloatingActionButton
private lateinit var profileList: RecyclerView
private var logicalSlotId: Int = -1
+ private lateinit var eid: String
private val adapter = EuiccProfileAdapter()
@@ -131,31 +134,42 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
inflater.inflate(R.menu.fragment_euicc, menu)
}
- override fun onOptionsItemSelected(item: MenuItem): Boolean =
- when (item.itemId) {
- R.id.show_notifications -> {
- if (logicalSlotId != -1) {
- Intent(requireContext(), NotificationsActivity::class.java).apply {
- putExtra("logicalSlotId", logicalSlotId)
- startActivity(this)
- }
- }
- true
- }
+ override fun onPrepareOptionsMenu(menu: Menu) {
+ super.onPrepareOptionsMenu(menu)
+ menu.findItem(R.id.show_notifications).isVisible =
+ logicalSlotId != -1
+ menu.findItem(R.id.euicc_info).isVisible =
+ logicalSlotId != -1
+ menu.findItem(R.id.euicc_memory_reset).isVisible =
+ runBlocking { preferenceRepository.euiccMemoryResetFlow.first() }
+ }
- R.id.euicc_info -> {
- if (logicalSlotId != -1) {
- Intent(requireContext(), EuiccInfoActivity::class.java).apply {
- putExtra("logicalSlotId", logicalSlotId)
- startActivity(this)
- }
- }
- true
+ override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
+ R.id.show_notifications -> {
+ Intent(requireContext(), NotificationsActivity::class.java).apply {
+ putExtra("logicalSlotId", logicalSlotId)
+ startActivity(this)
}
-
- else -> super.onOptionsItemSelected(item)
+ true
}
+ R.id.euicc_info -> {
+ Intent(requireContext(), EuiccInfoActivity::class.java).apply {
+ putExtra("logicalSlotId", logicalSlotId)
+ startActivity(this)
+ }
+ true
+ }
+
+ R.id.euicc_memory_reset -> {
+ EuiccMemoryResetFragment.newInstance(slotId, portId, eid)
+ .show(childFragmentManager, EuiccMemoryResetFragment.TAG)
+ true
+ }
+
+ else -> super.onOptionsItemSelected(item)
+ }
+
protected open suspend fun onCreateFooterViews(
parent: ViewGroup,
profiles: List
@@ -192,6 +206,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
val profiles = withEuiccChannel { channel ->
logicalSlotId = channel.logicalSlotId
+ eid = channel.lpa.eID
euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId)
if (unfilteredProfileListFlow.value)
channel.lpa.profiles
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccMemoryResetFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccMemoryResetFragment.kt
new file mode 100644
index 0000000..086a849
--- /dev/null
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccMemoryResetFragment.kt
@@ -0,0 +1,126 @@
+package im.angry.openeuicc.ui
+
+import android.graphics.Typeface
+import android.os.Bundle
+import android.text.Editable
+import android.util.Log
+import android.widget.EditText
+import android.widget.Toast
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.lifecycleScope
+import im.angry.openeuicc.common.R
+import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
+import im.angry.openeuicc.util.EuiccChannelFragmentMarker
+import im.angry.openeuicc.util.EuiccProfilesChangedListener
+import im.angry.openeuicc.util.ensureEuiccChannelManager
+import im.angry.openeuicc.util.euiccChannelManagerService
+import im.angry.openeuicc.util.newInstanceEuicc
+import im.angry.openeuicc.util.notifyEuiccProfilesChanged
+import im.angry.openeuicc.util.portId
+import im.angry.openeuicc.util.slotId
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.launch
+
+class EuiccMemoryResetFragment : DialogFragment(), EuiccChannelFragmentMarker {
+ companion object {
+ const val TAG = "EuiccMemoryResetFragment"
+
+ private const val FIELD_EID = "eid"
+
+ fun newInstance(slotId: Int, portId: Int, eid: String) =
+ newInstanceEuicc(EuiccMemoryResetFragment::class.java, slotId, portId) {
+ putString(FIELD_EID, eid)
+ }
+ }
+
+ private val eid: String by lazy { requireArguments().getString(FIELD_EID)!! }
+
+ private val confirmText: String by lazy {
+ getString(R.string.euicc_memory_reset_confirm_text, eid.takeLast(8))
+ }
+
+ private inline val isMatched: Boolean
+ get() = editText.text.toString() == confirmText
+
+ private var confirmed = false
+
+ private var toast: Toast? = null
+ set(value) {
+ toast?.cancel()
+ field = value
+ value?.show()
+ }
+
+ private val editText by lazy {
+ EditText(requireContext()).apply {
+ isLongClickable = false
+ typeface = Typeface.MONOSPACE
+ hint = Editable.Factory.getInstance()
+ .newEditable(getString(R.string.euicc_memory_reset_hint_text, confirmText))
+ }
+ }
+
+ private inline val alertDialog: AlertDialog
+ get() = requireDialog() as AlertDialog
+
+ override fun onCreateDialog(savedInstanceState: Bundle?) =
+ AlertDialog.Builder(requireContext(), R.style.AlertDialogTheme)
+ .setTitle(R.string.euicc_memory_reset_title)
+ .setMessage(getString(R.string.euicc_memory_reset_message, eid, confirmText))
+ .setView(editText)
+ // Set listener to null to prevent auto closing
+ .setNegativeButton(android.R.string.cancel, null)
+ .setPositiveButton(R.string.euicc_memory_reset_invoke_button, null)
+ .create()
+
+ override fun onResume() {
+ super.onResume()
+ alertDialog.setCanceledOnTouchOutside(false)
+ alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
+ .setOnClickListener { if (!confirmed) confirmation() }
+ alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE)
+ .setOnClickListener { if (!confirmed) dismiss() }
+ }
+
+ private fun confirmation() {
+ toast?.cancel()
+ if (!isMatched) {
+ Log.d(TAG, buildString {
+ appendLine("User input is mismatch:")
+ appendLine(editText.text)
+ appendLine(confirmText)
+ })
+ val resId = R.string.toast_euicc_memory_reset_confirm_text_mismatched
+ toast = Toast.makeText(requireContext(), resId, Toast.LENGTH_LONG)
+ return
+ }
+ confirmed = true
+ preventUserAction()
+
+ requireParentFragment().lifecycleScope.launch {
+ ensureEuiccChannelManager()
+ euiccChannelManagerService.waitForForegroundTask()
+
+ euiccChannelManagerService.launchMemoryReset(slotId, portId)
+ .onStart {
+ parentFragment?.notifyEuiccProfilesChanged()
+
+ val resId = R.string.toast_euicc_memory_reset_finitshed
+ toast = Toast.makeText(requireContext(), resId, Toast.LENGTH_LONG)
+
+ runCatching(::dismiss)
+ }
+ .waitDone()
+ }
+ }
+
+ private fun preventUserAction() {
+ editText.isEnabled = false
+ alertDialog.setCancelable(false)
+ alertDialog.setCanceledOnTouchOutside(false)
+ alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
+ alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).isEnabled = false
+ }
+}
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 fab680f..d137e90 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
@@ -77,6 +77,12 @@ open class SettingsFragment: PreferenceFragmentCompat() {
requirePreference("pref_developer_ignore_tls_certificate")
.bindBooleanFlow(preferenceRepository.ignoreTLSCertificateFlow)
+
+ requirePreference("pref_developer_refresh_after_switch")
+ .bindBooleanFlow(preferenceRepository.refreshAfterSwitchFlow)
+
+ requirePreference("pref_developer_euicc_memory_reset")
+ .bindBooleanFlow(preferenceRepository.euiccMemoryResetFlow)
}
protected fun requirePreference(key: CharSequence) =
@@ -122,7 +128,7 @@ open class SettingsFragment: PreferenceFragmentCompat() {
return true
}
- private fun CheckBoxPreference.bindBooleanFlow(flow: PreferenceFlowWrapper) {
+ protected fun CheckBoxPreference.bindBooleanFlow(flow: PreferenceFlowWrapper) {
lifecycleScope.launch {
flow.collect { isChecked = it }
}
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 8cab18b..a9f868f 100644
--- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt
@@ -21,7 +21,6 @@ import im.angry.openeuicc.ui.BaseEuiccAccessActivity
import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
import net.typeblog.lpac_jni.LocalProfileAssistant
class DownloadWizardActivity: BaseEuiccAccessActivity() {
@@ -35,6 +34,8 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
var downloadStarted: Boolean,
var downloadTaskID: Long,
var downloadError: LocalProfileAssistant.ProfileDownloadException?,
+ var skipMethodSelect: Boolean,
+ var confirmationCodeRequired: Boolean,
)
private lateinit var state: DownloadWizardState
@@ -63,17 +64,21 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
})
state = DownloadWizardState(
- null,
- intent.getIntExtra("selectedLogicalSlot", 0),
- "",
- null,
- null,
- null,
- false,
- -1,
- null
+ currentStepFragmentClassName = null,
+ selectedLogicalSlot = intent.getIntExtra("selectedLogicalSlot", 0),
+ smdp = "",
+ matchingId = null,
+ confirmationCode = null,
+ imei = null,
+ downloadStarted = false,
+ downloadTaskID = -1,
+ downloadError = null,
+ skipMethodSelect = false,
+ confirmationCodeRequired = false,
)
+ handleDeepLink()
+
progressBar = requireViewById(R.id.progress)
nextButton = requireViewById(R.id.download_wizard_next)
prevButton = requireViewById(R.id.download_wizard_back)
@@ -113,10 +118,24 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
}
}
+ private fun handleDeepLink() {
+ // 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 parsed = LPAString.parse(uri.schemeSpecificPart)
+ state.smdp = parsed.address
+ state.matchingId = parsed.matchingId
+ state.confirmationCodeRequired = parsed.confirmationCodeRequired
+ state.skipMethodSelect = true
+ }
+ }
+
override fun onProvideAssistContent(outContent: AssistContent?) {
super.onProvideAssistContent(outContent)
outContent?.webUri = try {
- val activationCode = ActivationCode(
+ val activationCode = LPAString(
state.smdp,
state.matchingId,
null,
@@ -138,6 +157,7 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
outState.putString("imei", state.imei)
outState.putBoolean("downloadStarted", state.downloadStarted)
outState.putLong("downloadTaskID", state.downloadTaskID)
+ outState.putBoolean("confirmationCodeRequired", state.confirmationCodeRequired)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
@@ -154,6 +174,8 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
state.downloadStarted =
savedInstanceState.getBoolean("downloadStarted", state.downloadStarted)
state.downloadTaskID = savedInstanceState.getLong("downloadTaskID", state.downloadTaskID)
+ state.confirmationCode = savedInstanceState.getString("confirmationCode", state.confirmationCode)
+ state.confirmationCodeRequired = savedInstanceState.getBoolean("confirmationCodeRequired", state.confirmationCodeRequired)
}
private fun onPrevPressed() {
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt
index 5fa8002..402e7a5 100644
--- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt
@@ -5,6 +5,7 @@ import android.util.Patterns
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.widget.Toast
import androidx.core.widget.addTextChangedListener
import com.google.android.material.textfield.TextInputLayout
import im.angry.openeuicc.common.R
@@ -36,7 +37,11 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF
DownloadWizardProgressFragment()
override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment =
- DownloadWizardMethodSelectFragment()
+ if (state.skipMethodSelect) {
+ DownloadWizardSlotSelectFragment()
+ } else {
+ DownloadWizardMethodSelectFragment()
+ }
override fun onCreateView(
inflater: LayoutInflater,
@@ -51,6 +56,9 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF
smdp.editText!!.addTextChangedListener {
updateInputCompleteness()
}
+ confirmationCode.editText!!.addTextChangedListener {
+ updateInputCompleteness()
+ }
return view
}
@@ -61,6 +69,15 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF
confirmationCode.editText!!.setText(state.confirmationCode)
imei.editText!!.setText(state.imei)
updateInputCompleteness()
+
+ if (state.confirmationCodeRequired) {
+ confirmationCode.editText!!.requestFocus()
+ confirmationCode.editText!!.hint =
+ getString(R.string.profile_download_confirmation_code_required)
+ } else {
+ confirmationCode.editText!!.hint =
+ getString(R.string.profile_download_confirmation_code)
+ }
}
override fun onPause() {
@@ -70,6 +87,9 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF
private fun updateInputCompleteness() {
inputComplete = Patterns.DOMAIN_NAME.matcher(smdp.editText!!.text).matches()
+ if (state.confirmationCodeRequired) {
+ inputComplete = inputComplete && confirmationCode.editText!!.text.isNotEmpty()
+ }
refreshButtons()
}
}
\ No newline at end of file
diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt
index 2846fd7..4b02b7a 100644
--- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt
@@ -42,21 +42,16 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
registerForActivityResult(ActivityResultContracts.GetContent()) { result ->
if (result == null) return@registerForActivityResult
- lifecycleScope.launch(Dispatchers.IO) {
- runCatching {
- requireContext().contentResolver.openInputStream(result)?.let { input ->
- val bmp = BitmapFactory.decodeStream(input)
- input.close()
-
- decodeQrFromBitmap(bmp)?.let {
- withContext(Dispatchers.Main) {
- processLpaString(it)
- }
+ lifecycleScope.launch {
+ val decoded = withContext(Dispatchers.IO) {
+ runCatching {
+ requireContext().contentResolver.openInputStream(result)?.use { input ->
+ BitmapFactory.decodeStream(input).use(::decodeQrFromBitmap)
}
-
- bmp.recycle()
}
}
+
+ decoded.getOrNull()?.let { processLpaString(it) }
}
}
@@ -126,18 +121,10 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
private fun processLpaString(input: String) {
try {
- val parsed = ActivationCode.fromString(input)
+ val parsed = LPAString.parse(input)
state.smdp = parsed.address
state.matchingId = parsed.matchingId
- if (parsed.confirmationCodeRequired) {
- AlertDialog.Builder(requireContext()).apply {
- setTitle(R.string.profile_download_required_confirmation_code)
- setMessage(R.string.profile_download_required_confirmation_code_message)
- setCancelable(true)
- setPositiveButton(android.R.string.ok, null)
- show()
- }
- }
+ state.confirmationCodeRequired = parsed.confirmationCodeRequired
gotoNextFragment(DownloadWizardDetailsFragment())
} catch (e: IllegalArgumentException) {
AlertDialog.Builder(requireContext()).apply {
@@ -150,14 +137,19 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
}
}
- private class DownloadMethodViewHolder(private val root: View) : ViewHolder(root) {
+ private inner class DownloadMethodViewHolder(private val root: View) : ViewHolder(root) {
private val icon = root.requireViewById(R.id.download_method_icon)
private val title = root.requireViewById(R.id.download_method_title)
fun bind(item: DownloadMethod) {
icon.setImageResource(item.iconRes)
title.setText(item.titleRes)
- root.setOnClickListener { item.onClick() }
+ root.setOnClickListener {
+ // If the user elected to use another download method, reset the confirmation code flag
+ // too
+ state.confirmationCodeRequired = false
+ item.onClick()
+ }
}
}
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 5510fb0..28bc9f0 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
@@ -49,7 +49,11 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
get() = true
override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment =
- DownloadWizardMethodSelectFragment()
+ if (state.skipMethodSelect) {
+ DownloadWizardDetailsFragment()
+ } else {
+ DownloadWizardMethodSelectFragment()
+ }
override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment? = null
diff --git a/app-common/src/main/java/im/angry/openeuicc/util/ActivationCode.kt b/app-common/src/main/java/im/angry/openeuicc/util/LPAString.kt
similarity index 79%
rename from app-common/src/main/java/im/angry/openeuicc/util/ActivationCode.kt
rename to app-common/src/main/java/im/angry/openeuicc/util/LPAString.kt
index c21e837..20956fb 100644
--- a/app-common/src/main/java/im/angry/openeuicc/util/ActivationCode.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/util/LPAString.kt
@@ -1,18 +1,18 @@
package im.angry.openeuicc.util
-data class ActivationCode(
+data class LPAString(
val address: String,
- val matchingId: String? = null,
- val oid: String? = null,
- val confirmationCodeRequired: Boolean = false,
+ val matchingId: String?,
+ val oid: String?,
+ val confirmationCodeRequired: Boolean,
) {
companion object {
- fun fromString(input: String): ActivationCode {
+ fun parse(input: String): LPAString {
val components = input.removePrefix("LPA:").split('$')
if (components.size < 2 || components[0] != "1") {
throw IllegalArgumentException("Invalid activation code format")
}
- return ActivationCode(
+ return LPAString(
address = components[1].trim(),
matchingId = components.getOrNull(2)?.trim()?.ifBlank { null },
oid = components.getOrNull(3)?.trim()?.ifBlank { null },
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 f5e3ca2..6ea342c 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
@@ -26,6 +26,7 @@ internal object PreferenceKeys {
val NOTIFICATION_SWITCH = booleanPreferencesKey("notification_switch")
// ---- Advanced ----
+ val REFRESH_AFTER_SWITCH = booleanPreferencesKey("refresh_after_switch")
val DISABLE_SAFEGUARD_REMOVABLE_ESIM = booleanPreferencesKey("disable_safeguard_removable_esim")
val VERBOSE_LOGGING = booleanPreferencesKey("verbose_logging")
@@ -33,9 +34,10 @@ internal object PreferenceKeys {
val DEVELOPER_OPTIONS_ENABLED = booleanPreferencesKey("developer_options_enabled")
val UNFILTERED_PROFILE_LIST = booleanPreferencesKey("unfiltered_profile_list")
val IGNORE_TLS_CERTIFICATE = booleanPreferencesKey("ignore_tls_certificate")
+ val EUICC_MEMORY_RESET = booleanPreferencesKey("euicc_memory_reset")
}
-class PreferenceRepository(private val context: Context) {
+open class PreferenceRepository(private val context: Context) {
// Expose flows so that we can also handle default values
// ---- Profile Notifications ----
val notificationDownloadFlow = bindFlow(PreferenceKeys.NOTIFICATION_DOWNLOAD, true)
@@ -47,26 +49,25 @@ class PreferenceRepository(private val context: Context) {
val verboseLoggingFlow = bindFlow(PreferenceKeys.VERBOSE_LOGGING, false)
// ---- Developer Options ----
+ val refreshAfterSwitchFlow = bindFlow(PreferenceKeys.REFRESH_AFTER_SWITCH, true)
val developerOptionsEnabledFlow = bindFlow(PreferenceKeys.DEVELOPER_OPTIONS_ENABLED, false)
val unfilteredProfileListFlow = bindFlow(PreferenceKeys.UNFILTERED_PROFILE_LIST, false)
val ignoreTLSCertificateFlow = bindFlow(PreferenceKeys.IGNORE_TLS_CERTIFICATE, false)
+ val euiccMemoryResetFlow = bindFlow(PreferenceKeys.EUICC_MEMORY_RESET, false)
- private fun bindFlow(key: Preferences.Key, defaultValue: T): PreferenceFlowWrapper =
+ protected fun bindFlow(key: Preferences.Key, defaultValue: T): PreferenceFlowWrapper =
PreferenceFlowWrapper(context, key, defaultValue)
}
class PreferenceFlowWrapper private constructor(
private val context: Context,
private val key: Preferences.Key,
- inner: Flow
+ inner: Flow,
) : Flow by inner {
- internal constructor(context: Context, key: Preferences.Key, defaultValue: T) : this(
- context,
- key,
- context.dataStore.data.map { it[key] ?: defaultValue }
- )
+ internal constructor(context: Context, key: Preferences.Key, defaultValue: T) :
+ this(context, key, context.dataStore.data.map { it[key] ?: defaultValue })
suspend fun updatePreference(value: T) {
context.dataStore.edit { it[key] = value }
}
-}
\ No newline at end of file
+}
diff --git a/app-common/src/main/java/im/angry/openeuicc/util/Utils.kt b/app-common/src/main/java/im/angry/openeuicc/util/Utils.kt
index 444c176..5a559f9 100644
--- a/app-common/src/main/java/im/angry/openeuicc/util/Utils.kt
+++ b/app-common/src/main/java/im/angry/openeuicc/util/Utils.kt
@@ -86,6 +86,13 @@ suspend fun connectSEService(context: Context): SEService = suspendCoroutine { c
}
}
+inline fun Bitmap.use(f: (Bitmap) -> T): T =
+ try {
+ f(this)
+ } finally {
+ recycle()
+ }
+
fun decodeQrFromBitmap(bmp: Bitmap): String? =
runCatching {
val pixels = IntArray(bmp.width * bmp.height)
diff --git a/app-common/src/main/res/drawable/ic_euicc_memory_reset.xml b/app-common/src/main/res/drawable/ic_euicc_memory_reset.xml
new file mode 100644
index 0000000..f1ca8c1
--- /dev/null
+++ b/app-common/src/main/res/drawable/ic_euicc_memory_reset.xml
@@ -0,0 +1,18 @@
+
+
+
+
diff --git a/app-common/src/main/res/menu/fragment_euicc.xml b/app-common/src/main/res/menu/fragment_euicc.xml
index b54eaf1..6e2dfbe 100644
--- a/app-common/src/main/res/menu/fragment_euicc.xml
+++ b/app-common/src/main/res/menu/fragment_euicc.xml
@@ -10,4 +10,9 @@
android:id="@+id/euicc_info"
android:title="@string/euicc_info"
app:showAsAction="never" />
+
+
\ 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 e4969c1..1fd361c 100644
--- a/app-common/src/main/res/values-ja/strings.xml
+++ b/app-common/src/main/res/values-ja/strings.xml
@@ -42,12 +42,11 @@
サーバー (RSP / SM-DP+)
アクティベーションコード
確認コード (オプション)
+ 確認コード (必須)
IMEI (オプション)
- ダウンロードに失敗する可能性があります
+ 残り容量が少ない
残り容量が少ないため、ダウンロードに失敗する可能性があります。
クリップボードに LPA コードがありません
- 確認コードが必要です
- クリップボードからスキャンした QR コードまたは LPA コードに必要な確認コードを入力してください。
解析できません
QR コードまたはクリップボードの内容を LPA コードとして解析できませんでした。
ダウンロードウィザード
@@ -151,4 +150,16 @@
情報
アプリバージョン
ソースコード
+ 確認文字列が一致しません
+ このチップは消去されました
+ eSIM チップを消去しています
+ eSIM チップの消去は失敗しました
+ eSIM を消去する
+ eSIM を消去する
+ このチップ内のすべてのプロファイルを削除することをご確認してください。この操作は元に戻せないことをご理解してください。\n\nEID: %s\n\n%s
+ 確認のため、ここに「%s」を入力してください
+ EID が %s で終わるチップを消去することに同意します。これは元に戻せないことを理解しています。
+ 消去する
+ eUICC の消去を可能にする
+ この操作は、デフォルトでは非表示になっている危険な操作です。代わりに、すべての構成ファイルを手動で削除することもできます。
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 2cadc03..8c36091 100644
--- a/app-common/src/main/res/values-zh-rCN/strings.xml
+++ b/app-common/src/main/res/values-zh-rCN/strings.xml
@@ -37,8 +37,14 @@
服务器 (RSP / SM-DP+)
激活码
确认码 (可选)
+ 已复制序列号到剪贴板
+ 产品名称
+ 产品序列号
+ 产品 Bootloader 版本
+ 产品固件版本
+ 确认码 (必需)
IMEI (可选)
- 本次下载可能会失败
+ 剩余空间不足
当前芯片的剩余空间不足,可能导致配置下载失败。\n是否继续下载?
日志已保存到指定路径。需要通过其他 App 分享吗?
新昵称
@@ -144,6 +150,16 @@
无视 SM-DP+ 的 TLS 证书
允许 RSP 服务器使用任意证书
无信息
- 需要确认码
- 您扫描的二维码或粘贴的 LPA 码需要一个额外的确认码
+ 输入的确认文本不匹配
+ 此芯片已被擦除
+ 正在擦除 eSIM 芯片
+ eSIM 芯片擦除失败
+ 擦除 eSIM 芯片
+ 擦除 eSIM 芯片
+ 请确认删除此芯片上的所有配置文件,并了解此操作不可逆。\n\nEID: %s\n\n%s
+ 请在此处输入「%s」以确认
+ 我确认擦除 EID 以 %s 结尾的芯片,并了解此操作不可逆
+ 擦除
+ 允许擦除 eUICC
+ 此操作是默认隐藏的危险操作。作为替代方案,您可以手动删除所有配置文件。
\ 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 28691a0..3d8270d 100644
--- a/app-common/src/main/res/values-zh-rTW/strings.xml
+++ b/app-common/src/main/res/values-zh-rTW/strings.xml
@@ -37,8 +37,14 @@
伺服器 (RSP / SM-DP+)
啟用碼
確認碼 (可選)
+ 已複製序號到剪貼簿
+ 產品名稱
+ 產品序號
+ 產品引導程式版本
+ 產品韌體版本
+ 確認碼 (必需)
IMEI (可選)
- 本次下載可能會失敗
+ 剩餘空間不足
目前晶片的剩餘空間不足,可能導致配置下載失敗。\n是否繼續下載?
日誌已儲存到指定路徑。需要透過其他 App 分享嗎?
新名稱
@@ -144,4 +150,16 @@
忽略 SM-DP+ 的 TLS 證書
允許 RSP 伺服器使用任意證書
無資訊
+ 輸入的確認文字不匹配
+ 此晶片已被擦除
+ 正在擦除 eSIM 晶片
+ eSIM 晶片擦除失敗
+ 擦除 eSIM 晶片
+ 擦除 eSIM 晶片
+ 請確認刪除此晶片上的所有配置文件,並了解此操作不可逆。\n\nEID: %s\n\n%s
+ 請在此輸入「%s」以確認
+ 我確認擦除 EID 以 %s 結尾的晶片,並了解此操作不可逆
+ 擦除
+ 允許擦除 eUICC
+ 此操作是預設隱藏的危險操作。作為替代方案,您可以手動刪除所有設定檔。
\ 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 a45ce1f..5964c32 100644
--- a/app-common/src/main/res/values/strings.xml
+++ b/app-common/src/main/res/values/strings.xml
@@ -30,8 +30,10 @@
Cannot switch to new eSIM profile.
Confirmation string mismatch
+ Confirmation string mismatch
+ This chip has been erased
ICCID copied to clipboard
- Serial Number copied to clipboard
+ Serial number copied to clipboard
EID copied to clipboard
ATR copied to clipboard
@@ -48,18 +50,19 @@
Failed to delete eSIM profile
Switching eSIM profile
Failed to switch eSIM profile
+ Erasing eSIM chip
+ Failed to erase eSIM chip
New eSIM
Server (RSP / SM-DP+)
Activation Code
Confirmation Code (Optional)
+ Confirmation Code (Required)
IMEI (Optional)
- This download may fail
- This download may fail due to low remaining capacity.
+ Low remaining capacity
+ This profile may fail to download due to low remaining capacity.
No LPA code found in clipboard
- Confirmation Code Required
- Please provide a confirmation code as required by the scanned QR code or LPA code from clipboard.
Unable to parse
Could not parse QR code or clipboard content as a LPA code.
@@ -143,6 +146,13 @@
Unknown eSIM CI
Answer To Reset (ATR)
+ Erase eUICC
+ Erase eUICC
+ Please confirm to delete all profiles on this chip and understand that this operation is irreversible.\n\nEID: %s\n\n%s
+ Type \'%s\' here to confirm
+ I CONFIRM TO ERASE THE CHIP WHOSE EID ENDS WITH %s AND UNDERSTAND THAT THIS IS IRREVERSIBLE
+ Erase
+
Yes
No
@@ -162,6 +172,8 @@
Switching
Send notifications for switching profiles\nNote that this type of notification is unreliable.
Advanced
+ Refresh status after switching
+ If crash after switch, Please trying disable the function
Allow Disabling / Deleting Active Profile
By default, this app prevents you from disabling the active profile on a removable eSIM inserted in the device, because doing so may sometimes render it inaccessible.\nCheck this box to remove this safeguard.
Verbose Logging
@@ -175,6 +187,8 @@
Include non-production profiles in the list
Ignore SM-DP+ TLS certificate
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.
Info
App Version
Source Code
diff --git a/app-common/src/main/res/xml/pref_settings.xml b/app-common/src/main/res/xml/pref_settings.xml
index bb5bd50..f719ef5 100644
--- a/app-common/src/main/res/xml/pref_settings.xml
+++ b/app-common/src/main/res/xml/pref_settings.xml
@@ -52,11 +52,17 @@
-
+
+
-
+
+
+
@@ -13,5 +19,9 @@ class PrivilegedSettingsFragment : SettingsFragment() {
// eventually work for platform-signed apps. Or, at some point we might introduce our own
// locale picker, which hopefully works whether privileged or not.
requirePreference("pref_advanced_language").isVisible = false
+
+ // Force use TelephonyManager API
+ requirePreference("pref_developer_tmapi_removable")
+ .bindBooleanFlow((preferenceRepository as PrivilegedPreferenceRepository).removableTelephonyManagerFlow)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/im/angry/openeuicc/util/PrivilegedPreferenceRepository.kt b/app/src/main/java/im/angry/openeuicc/util/PrivilegedPreferenceRepository.kt
new file mode 100644
index 0000000..9e1ffcc
--- /dev/null
+++ b/app/src/main/java/im/angry/openeuicc/util/PrivilegedPreferenceRepository.kt
@@ -0,0 +1,15 @@
+package im.angry.openeuicc.util
+
+import android.content.Context
+import androidx.datastore.preferences.core.booleanPreferencesKey
+
+internal object PrivilegedPreferenceKeys {
+ // ---- Developer Options ----
+ val REMOVABLE_TELEPHONY_MANAGER = booleanPreferencesKey("removable_telephony_manager")
+}
+
+class PrivilegedPreferenceRepository(context: Context) : PreferenceRepository(context) {
+ // ---- Developer Options ----
+ val removableTelephonyManagerFlow =
+ bindFlow(PrivilegedPreferenceKeys.REMOVABLE_TELEPHONY_MANAGER, false)
+}
\ No newline at end of file
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index 29f3ef6..acc1728 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -17,4 +17,6 @@
使用しているデバイスは eSIM をサポートしています。モバイルネットワークに接続するには通信事業者が発行した eSIM をダウンロードするか、物理 SIM を挿入してください。
スキップ
eSIM をダウンロード
+ TelephonyManagerをどこでも使用
+ デフォルトでは、非特権モード (EasyEUICC) と一致するように、取り外し可能な eUICC に対して OMAPI のみが試行されます。これは、一部のデバイスではうまく機能しない可能性があります。このオプションを選択する場合、取り外し可能な eUICC でも TelephonyManager を使用することになります。
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 18497b2..d6befc2 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -17,4 +17,6 @@
跳过
下载 eSIM
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 368efbc..10285d3 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -17,4 +17,6 @@
跳過
下載 eSIM
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 47c88bd..ddf17e4 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -22,4 +22,8 @@
Your device supports eSIMs. To connect to mobile network, download your eSIM issued by a carrier, or insert a physical SIM.
Skip
Download eSIM
+
+
+ Use TelephonyManager everywhere
+ By default, only OMAPI is attempted for removable eUICCs to match what is done in unprivileged mode (i.e. EasyEUICC). This may not work well on some devices. Select this option to force the use of TelephonyManager even for removable eUICCs.
\ No newline at end of file
diff --git a/app/src/main/res/xml/pref_privileged_settings.xml b/app/src/main/res/xml/pref_privileged_settings.xml
new file mode 100644
index 0000000..339233b
--- /dev/null
+++ b/app/src/main/res/xml/pref_privileged_settings.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-download.c b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-download.c
index 028e30d..bae2ee8 100644
--- a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-download.c
+++ b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-download.c
@@ -126,8 +126,11 @@ Java_net_typeblog_lpac_1jni_LpacJni_downloadProfile(JNIEnv *env, jobject thiz, j
syslog(LOG_INFO, "es10b_load_bound_profile_package %d, reason %d", ret, es10b_load_bound_profile_package_result.errorReason);
if (ret < 0) {
ret = - (int) es10b_load_bound_profile_package_result.errorReason;
+ goto out;
}
+ euicc_http_cleanup(ctx);
+
out:
// We expect Java side to call cancelSessions after any error -- thus, `euicc_http_cleanup` is done there
// This is so that Java side can access the last HTTP and/or APDU errors when we return.