Compare commits

...

1 commit

Author SHA1 Message Date
07f57d5c7f
feat: euicc memory reset 2025-03-06 21:05:29 +08:00
8 changed files with 218 additions and 21 deletions

View file

@ -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()
}
}
}

View file

@ -31,6 +31,7 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.material.floatingactionbutton.FloatingActionButton
import net.typeblog.lpac_jni.LocalProfileInfo
import im.angry.openeuicc.common.R
import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.service.EuiccChannelManagerService
import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
import im.angry.openeuicc.ui.wizard.DownloadWizardActivity
@ -55,6 +56,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,31 +133,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 =
logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID
}
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<LocalProfileInfo>
@ -192,6 +205,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

View file

@ -0,0 +1,136 @@
package im.angry.openeuicc.ui
import android.graphics.Typeface
import android.os.Bundle
import android.text.Editable
import android.text.InputType
import android.util.Log
import android.view.View
import android.view.inputmethod.EditorInfo
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()
}
}

View file

@ -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<CheckBoxPreference>("pref_developer_ignore_tls_certificate")
.bindBooleanFlow(preferenceRepository.ignoreTLSCertificateFlow)
requirePreference<CheckBoxPreference>("pref_developer_euicc_memory_reset").apply {
isVisible = context.packageManager.hasSystemFeature(PackageManager.FEATURE_USB_HOST)
bindBooleanFlow(preferenceRepository.euiccMemoryResetFlow)
}
}
protected fun <T : Preference> requirePreference(key: CharSequence) =

View file

@ -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 <T> bindFlow(key: Preferences.Key<T>, defaultValue: T): PreferenceFlowWrapper<T> =
PreferenceFlowWrapper(context, key, defaultValue)

View file

@ -10,4 +10,9 @@
android:id="@+id/euicc_info"
android:title="@string/euicc_info"
app:showAsAction="never" />
<item
android:id="@+id/euicc_memory_reset"
android:title="@string/euicc_memory_reset"
app:showAsAction="never" />
</menu>

View file

@ -30,6 +30,8 @@
<string name="toast_profile_enable_failed">Cannot switch to new eSIM profile.</string>
<string name="toast_profile_delete_confirm_text_mismatched">Confirmation string mismatch</string>
<string name="toast_euicc_memory_reset_confirm_text_mismatched">Confirmation string mismatch</string>
<string name="toast_euicc_memory_reset_finitshed">This eSIM chip has been erased</string>
<string name="toast_iccid_copied">ICCID copied to clipboard</string>
<string name="toast_sn_copied">Serial Number copied to clipboard</string>
<string name="toast_eid_copied">EID copied to clipboard</string>
@ -48,6 +50,8 @@
<string name="task_profile_delete_failure">Failed to delete eSIM profile</string>
<string name="task_profile_switch">Switching eSIM profile</string>
<string name="task_profile_switch_failure">Failed to switch eSIM profile</string>
<string name="task_euicc_memory_reset">Erasing eSIM chip</string>
<string name="task_euicc_memory_reset_failure">Failed to erase eSIM chip</string>
<string name="profile_download">New eSIM</string>
<string name="profile_download_server">Server (RSP / SM-DP+)</string>
@ -143,6 +147,13 @@
<string name="euicc_info_ci_unknown">Unknown eSIM CI</string>
<string name="euicc_info_atr" translatable="false">Answer To Reset (ATR)</string>
<string name="euicc_memory_reset">Erase the Chip</string>
<string name="euicc_memory_reset_title">Erase the Chip</string>
<string name="euicc_memory_reset_message">I confirm to delete all profiles on this chip and understand that this operation is irreversible.\n\nEID: %s\n\n%s</string>
<string name="euicc_memory_reset_hint_text">Type \'%s\' here to confirm erase the chip</string>
<string name="euicc_memory_reset_confirm_text">I CONFIRM TO ERASE ALL PROFILES WITH EID ENDING WITH %s AND UNDERSTAND THIS IRREVERSIBLE</string>
<string name="euicc_memory_reset_invoke_button">Erase</string>
<string name="yes">Yes</string>
<string name="no">No</string>
@ -175,6 +186,8 @@
<string name="pref_developer_unfiltered_profile_list_desc">Include non-production profiles in the list</string>
<string name="pref_developer_ignore_tls_certificate">Ignore SM-DP+ TLS certificate</string>
<string name="pref_developer_ignore_tls_certificate_desc">Accept any TLS certificate used by the RSP server</string>
<string name="pref_developer_euicc_memory_reset">Allow Erase Chip (USB only)</string>s
<string name="pref_developer_euicc_memory_reset_desc">Don\'t erase your eSIM as a troubleshooting step unless directed to by your carrier.</string>
<string name="pref_info">Info</string>
<string name="pref_info_app_version">App Version</string>
<string name="pref_info_source_code">Source Code</string>

View file

@ -69,6 +69,12 @@
app:summary="@string/pref_developer_ignore_tls_certificate_desc"
app:title="@string/pref_developer_ignore_tls_certificate" />
<CheckBoxPreference
app:iconSpaceReserved="false"
app:isPreferenceVisible="false"
app:key="pref_developer_euicc_memory_reset"
app:summary="@string/pref_developer_euicc_memory_reset_desc"
app:title="@string/pref_developer_euicc_memory_reset" />
</PreferenceCategory>
<PreferenceCategory