From 059ee64a5dcc501986d9bc68163cc463063f3c6c Mon Sep 17 00:00:00 2001 From: septs Date: Thu, 6 Mar 2025 17:46:44 +0800 Subject: [PATCH 1/5] feat: euicc memory reset --- .../service/EuiccChannelManagerService.kt | 15 ++ .../openeuicc/ui/EuiccManagementFragment.kt | 63 ++++++--- .../openeuicc/ui/EuiccMemoryResetFragment.kt | 133 ++++++++++++++++++ .../im/angry/openeuicc/ui/SettingsFragment.kt | 6 + .../angry/openeuicc/util/PreferenceUtils.kt | 2 + .../src/main/res/menu/fragment_euicc.xml | 5 + app-common/src/main/res/values/strings.xml | 13 ++ app-common/src/main/res/xml/pref_settings.xml | 6 + 8 files changed, 220 insertions(+), 23 deletions(-) create mode 100644 app-common/src/main/java/im/angry/openeuicc/ui/EuiccMemoryResetFragment.kt diff --git a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt index 760f1af..9e900b3 100644 --- a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt +++ b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt @@ -495,4 +495,19 @@ 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_task_delete + ) { + euiccChannelManager.beginTrackedOperation(slotId, portId) { + euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> + channel.lpa.euiccMemoryReset() + } + + preferenceRepository.euiccMemoryResetFlow.first() + } + } } \ 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..40bdbb3 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 var eid: String? = null private val adapter = EuiccProfileAdapter() @@ -131,30 +134,43 @@ 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 - } - - R.id.euicc_info -> { - if (logicalSlotId != -1) { - Intent(requireContext(), EuiccInfoActivity::class.java).apply { - putExtra("logicalSlotId", logicalSlotId) - startActivity(this) - } - } - true - } - - else -> super.onOptionsItemSelected(item) + 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).apply { + isEnabled = runBlocking { preferenceRepository.euiccMemoryResetFlow.first() } + isVisible = isEnabled and isUsb } + } + + override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { + R.id.show_notifications -> { + Intent(requireContext(), NotificationsActivity::class.java).apply { + putExtra("logicalSlotId", logicalSlotId) + startActivity(this) + } + 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, @@ -192,6 +208,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..00e0785 --- /dev/null +++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccMemoryResetFragment.kt @@ -0,0 +1,133 @@ +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.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).apply { + requireArguments().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 { + notifyChanged(parentFragment) + + 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 + } +} + +private fun notifyChanged(fragment: Fragment?) { + if (fragment is EuiccProfilesChangedListener) { + // Trigger a refresh in the parent fragment -- it should wait until + // any foreground task is completed before actually doing a refresh + fragment.onEuiccProfilesChanged() + } +} \ No newline at end of file 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..126e7ea 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 @@ -1,6 +1,7 @@ package im.angry.openeuicc.ui import android.content.Intent +import android.content.pm.PackageManager import android.net.Uri import android.os.Build import android.os.Bundle @@ -77,6 +78,11 @@ open class SettingsFragment: PreferenceFragmentCompat() { requirePreference("pref_developer_ignore_tls_certificate") .bindBooleanFlow(preferenceRepository.ignoreTLSCertificateFlow) + + requirePreference("pref_developer_euicc_memory_reset").apply { + isVisible = context.packageManager.hasSystemFeature(PackageManager.FEATURE_USB_HOST) + bindBooleanFlow(preferenceRepository.euiccMemoryResetFlow) + } } protected fun requirePreference(key: CharSequence) = 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..25f3e86 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt @@ -33,6 +33,7 @@ internal object PreferenceKeys { val DEVELOPER_OPTIONS_ENABLED = booleanPreferencesKey("developer_options_enabled") val UNFILTERED_PROFILE_LIST = booleanPreferencesKey("unfiltered_profile_list") val IGNORE_TLS_CERTIFICATE = booleanPreferencesKey("ignore_tls_certificate") + val EUICC_MEMORY_RESET = booleanPreferencesKey("euicc_memory_reset") } class PreferenceRepository(private val context: Context) { @@ -50,6 +51,7 @@ class PreferenceRepository(private val context: Context) { val developerOptionsEnabledFlow = bindFlow(PreferenceKeys.DEVELOPER_OPTIONS_ENABLED, false) val unfilteredProfileListFlow = bindFlow(PreferenceKeys.UNFILTERED_PROFILE_LIST, false) val ignoreTLSCertificateFlow = bindFlow(PreferenceKeys.IGNORE_TLS_CERTIFICATE, false) + val euiccMemoryResetFlow = bindFlow(PreferenceKeys.EUICC_MEMORY_RESET, false) private fun bindFlow(key: Preferences.Key, defaultValue: T): PreferenceFlowWrapper = PreferenceFlowWrapper(context, key, defaultValue) 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/strings.xml b/app-common/src/main/res/values/strings.xml index a45ce1f..b4a092f 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -30,6 +30,8 @@ Cannot switch to new eSIM profile. Confirmation string mismatch + Confirmation string mismatch + This eSIM chip has been erased ICCID copied to clipboard Serial Number copied to clipboard EID copied to clipboard @@ -48,6 +50,8 @@ 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+) @@ -143,6 +147,13 @@ Unknown eSIM CI Answer To Reset (ATR) + Erase the Chip + Erase the Chip + I 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 erase the chip + I CONFIRM TO ERASE ALL PROFILES WITH EID ENDING WITH %s AND UNDERSTAND THIS IRREVERSIBLE + Erase + Yes No @@ -175,6 +186,8 @@ Include non-production profiles in the list Ignore SM-DP+ TLS certificate Accept any TLS certificate used by the RSP server + Allow Erase Chip (USB only)s + Don\'t erase your eSIM as a troubleshooting step unless directed to by your carrier. 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..709e25c 100644 --- a/app-common/src/main/res/xml/pref_settings.xml +++ b/app-common/src/main/res/xml/pref_settings.xml @@ -69,6 +69,12 @@ app:summary="@string/pref_developer_ignore_tls_certificate_desc" app:title="@string/pref_developer_ignore_tls_certificate" /> + Date: Fri, 7 Mar 2025 07:20:19 +0800 Subject: [PATCH 2/5] chore: improve eid handling --- .../java/im/angry/openeuicc/ui/EuiccManagementFragment.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 40bdbb3..6ab6528 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 @@ -57,7 +57,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, private lateinit var fab: FloatingActionButton private lateinit var profileList: RecyclerView private var logicalSlotId: Int = -1 - private var eid: String? = null + private lateinit var eid: String private val adapter = EuiccProfileAdapter() @@ -164,7 +164,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, } R.id.euicc_memory_reset -> { - EuiccMemoryResetFragment.newInstance(slotId, portId, eid!!) + EuiccMemoryResetFragment.newInstance(slotId, portId, eid) .show(childFragmentManager, EuiccMemoryResetFragment.TAG) true } -- 2.45.3 From f5f41086930b0861005188d1e7abd0033e09b326 Mon Sep 17 00:00:00 2001 From: septs Date: Fri, 7 Mar 2025 19:35:06 +0800 Subject: [PATCH 3/5] feat: update euicc memory reset icon and refactor fragment instantiation --- .../service/EuiccChannelManagerService.kt | 2 +- .../openeuicc/ui/EuiccMemoryResetFragment.kt | 4 ++-- .../res/drawable/ic_euicc_memory_reset.xml | 18 ++++++++++++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 app-common/src/main/res/drawable/ic_euicc_memory_reset.xml diff --git a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt index 9e900b3..52943d8 100644 --- a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt +++ b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt @@ -500,7 +500,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { launchForegroundTask( getString(R.string.task_euicc_memory_reset), getString(R.string.task_euicc_memory_reset_failure), - R.drawable.ic_task_delete + R.drawable.ic_euicc_memory_reset ) { euiccChannelManager.beginTrackedOperation(slotId, portId) { euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> 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 index 00e0785..54cf9ff 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccMemoryResetFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccMemoryResetFragment.kt @@ -29,8 +29,8 @@ class EuiccMemoryResetFragment : DialogFragment(), EuiccChannelFragmentMarker { private const val FIELD_EID = "eid" fun newInstance(slotId: Int, portId: Int, eid: String) = - newInstanceEuicc(EuiccMemoryResetFragment::class.java, slotId, portId).apply { - requireArguments().putString(FIELD_EID, eid) + newInstanceEuicc(EuiccMemoryResetFragment::class.java, slotId, portId) { + putString(FIELD_EID, eid) } } 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..3c7f551 --- /dev/null +++ b/app-common/src/main/res/drawable/ic_euicc_memory_reset.xml @@ -0,0 +1,18 @@ + + + + -- 2.45.3 From 977a369dd18fe54553a38aa615156c7c748e2772 Mon Sep 17 00:00:00 2001 From: septs Date: Fri, 7 Mar 2025 19:36:58 +0800 Subject: [PATCH 4/5] fix: update stroke color to use system color reference for euicc memory reset icon --- app-common/src/main/res/drawable/ic_euicc_memory_reset.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 3c7f551..f1ca8c1 100644 --- a/app-common/src/main/res/drawable/ic_euicc_memory_reset.xml +++ b/app-common/src/main/res/drawable/ic_euicc_memory_reset.xml @@ -6,13 +6,13 @@ -- 2.45.3 From 2d175a8ee605125f3c962d8cfe9c8f488628677b Mon Sep 17 00:00:00 2001 From: septs Date: Sun, 9 Mar 2025 01:27:11 +0800 Subject: [PATCH 5/5] refactor: update memory reset notification handling --- .../im/angry/openeuicc/ui/EuiccMemoryResetFragment.kt | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) 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 index 54cf9ff..086a849 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccMemoryResetFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccMemoryResetFragment.kt @@ -17,6 +17,7 @@ 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 @@ -104,7 +105,7 @@ class EuiccMemoryResetFragment : DialogFragment(), EuiccChannelFragmentMarker { euiccChannelManagerService.launchMemoryReset(slotId, portId) .onStart { - notifyChanged(parentFragment) + parentFragment?.notifyEuiccProfilesChanged() val resId = R.string.toast_euicc_memory_reset_finitshed toast = Toast.makeText(requireContext(), resId, Toast.LENGTH_LONG) @@ -123,11 +124,3 @@ class EuiccMemoryResetFragment : DialogFragment(), EuiccChannelFragmentMarker { alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).isEnabled = false } } - -private fun notifyChanged(fragment: Fragment?) { - if (fragment is EuiccProfilesChangedListener) { - // Trigger a refresh in the parent fragment -- it should wait until - // any foreground task is completed before actually doing a refresh - fragment.onEuiccProfilesChanged() - } -} \ No newline at end of file -- 2.45.3