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..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 @@ -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_euicc_memory_reset + ) { + 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..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 @@ -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,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..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..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/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/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" /> +