瀏覽代碼

feat: euicc memory reset

peter: Adjusted strings and i18n translation. Also removed the arbitrary
limit on USB channels -- this is a developer option anyway.
septs 11 月之前
父節點
當前提交
5dd9eed4fe

+ 15 - 0
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()
+            }
+        }
 }

+ 34 - 19
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() }
+    }
+
+    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 -> {
-                if (logicalSlotId != -1) {
-                    Intent(requireContext(), EuiccInfoActivity::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
+        }
 
-            else -> super.onOptionsItemSelected(item)
+        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 +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

+ 126 - 0
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
+    }
+}

+ 6 - 0
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<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) =

+ 2 - 0
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")
 }
 
 open class PreferenceRepository(private val context: Context) {
@@ -50,6 +51,7 @@ open 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)
 
     protected fun <T> bindFlow(key: Preferences.Key<T>, defaultValue: T): PreferenceFlowWrapper<T> =
         PreferenceFlowWrapper(context, key, defaultValue)

+ 18 - 0
app-common/src/main/res/drawable/ic_euicc_memory_reset.xml

@@ -0,0 +1,18 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="21"
+    android:viewportHeight="21">
+    <path
+        android:pathData="m3.578,6.487c1.385,-2.384 3.966,-3.987 6.922,-3.987 4.418,0 8,3.582 8,8s-3.582,8 -8,8 -8,-3.582 -8,-8"
+        android:strokeWidth="1"
+        android:strokeColor="@android:color/white"
+        android:strokeLineCap="round"
+        android:strokeLineJoin="round" />
+    <path
+        android:pathData="m7.5,6.5l-4,0l-0,-4"
+        android:strokeWidth="1"
+        android:strokeColor="@android:color/white"
+        android:strokeLineCap="round"
+        android:strokeLineJoin="round" />
+</vector>

+ 5 - 0
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" />
+
+    <item
+        android:id="@+id/euicc_memory_reset"
+        android:title="@string/euicc_memory_reset"
+        app:showAsAction="never" />
 </menu>

+ 12 - 0
app-common/src/main/res/values-ja/strings.xml

@@ -150,4 +150,16 @@
     <string name="pref_info">情報</string>
     <string name="pref_info_app_version">アプリバージョン</string>
     <string name="pref_info_source_code">ソースコード</string>
+    <string name="toast_euicc_memory_reset_confirm_text_mismatched">確認文字列が一致しません</string>
+    <string name="toast_euicc_memory_reset_finitshed">このチップは消去されました</string>
+    <string name="task_euicc_memory_reset">eSIM チップを消去しています</string>
+    <string name="task_euicc_memory_reset_failure">eSIM チップの消去は失敗しました</string>
+    <string name="euicc_memory_reset">eSIM を消去する</string>
+    <string name="euicc_memory_reset_title">eSIM を消去する</string>
+    <string name="euicc_memory_reset_message">このチップ内のすべてのプロファイルを削除することをご確認してください。この操作は元に戻せないことをご理解してください。\n\nEID: %s\n\n%s</string>
+    <string name="euicc_memory_reset_hint_text">確認のため、ここに「%s」を入力してください</string>
+    <string name="euicc_memory_reset_confirm_text">EID が %s で終わるチップを消去することに同意します。これは元に戻せないことを理解しています。</string>
+    <string name="euicc_memory_reset_invoke_button">消去する</string>
+    <string name="pref_developer_euicc_memory_reset">eUICC の消去を可能にする</string>
+    <string name="pref_developer_euicc_memory_reset_desc">この操作は、デフォルトでは非表示になっている危険な操作です。代わりに、すべての構成ファイルを手動で削除することもできます。</string>
 </resources>

+ 12 - 0
app-common/src/main/res/values-zh-rCN/strings.xml

@@ -150,4 +150,16 @@
     <string name="pref_developer_ignore_tls_certificate">无视 SM-DP+ 的 TLS 证书</string>
     <string name="pref_developer_ignore_tls_certificate_desc">允许 RSP 服务器使用任意证书</string>
     <string name="information_unavailable">无信息</string>
+    <string name="toast_euicc_memory_reset_confirm_text_mismatched">输入的确认文本不匹配</string>
+    <string name="toast_euicc_memory_reset_finitshed">此芯片已被擦除</string>
+    <string name="task_euicc_memory_reset">正在擦除 eSIM 芯片</string>
+    <string name="task_euicc_memory_reset_failure">eSIM 芯片擦除失败</string>
+    <string name="euicc_memory_reset">擦除 eSIM 芯片</string>
+    <string name="euicc_memory_reset_title">擦除 eSIM 芯片</string>
+    <string name="euicc_memory_reset_message">请确认删除此芯片上的所有配置文件,并了解此操作不可逆。\n\nEID: %s\n\n%s</string>
+    <string name="euicc_memory_reset_hint_text">请在此处输入「%s」以确认</string>
+    <string name="euicc_memory_reset_confirm_text">我确认擦除 EID 以 %s 结尾的芯片,并了解此操作不可逆</string>
+    <string name="euicc_memory_reset_invoke_button">擦除</string>
+    <string name="pref_developer_euicc_memory_reset">允许擦除 eUICC</string>
+    <string name="pref_developer_euicc_memory_reset_desc">此操作是默认隐藏的危险操作。作为替代方案,您可以手动删除所有配置文件。</string>
 </resources>

+ 12 - 0
app-common/src/main/res/values-zh-rTW/strings.xml

@@ -150,4 +150,16 @@
     <string name="pref_developer_ignore_tls_certificate">忽略 SM-DP+ 的 TLS 證書</string>
     <string name="pref_developer_ignore_tls_certificate_desc">允許 RSP 伺服器使用任意證書</string>
     <string name="information_unavailable">無資訊</string>
+    <string name="toast_euicc_memory_reset_confirm_text_mismatched">輸入的確認文字不匹配</string>
+    <string name="toast_euicc_memory_reset_finitshed">此晶片已被擦除</string>
+    <string name="task_euicc_memory_reset">正在擦除 eSIM 晶片</string>
+    <string name="task_euicc_memory_reset_failure">eSIM 晶片擦除失敗</string>
+    <string name="euicc_memory_reset">擦除 eSIM 晶片</string>
+    <string name="euicc_memory_reset_title">擦除 eSIM 晶片</string>
+    <string name="euicc_memory_reset_message">請確認刪除此晶片上的所有配置文件,並了解此操作不可逆。\n\nEID: %s\n\n%s</string>
+    <string name="euicc_memory_reset_hint_text">請在此輸入「%s」以確認</string>
+    <string name="euicc_memory_reset_confirm_text">我確認擦除 EID 以 %s 結尾的晶片,並了解此操作不可逆</string>
+    <string name="euicc_memory_reset_invoke_button">擦除</string>
+    <string name="pref_developer_euicc_memory_reset">允許擦除 eUICC</string>
+    <string name="pref_developer_euicc_memory_reset_desc">此操作是預設隱藏的危險操作。作為替代方案,您可以手動刪除所有設定檔。</string>
 </resources>

+ 13 - 0
app-common/src/main/res/values/strings.xml

@@ -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 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>
@@ -142,6 +146,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 eUICC</string>
+    <string name="euicc_memory_reset_title">Erase eUICC</string>
+    <string name="euicc_memory_reset_message">Please 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</string>
+    <string name="euicc_memory_reset_confirm_text">I CONFIRM TO ERASE THE CHIP WHOSE EID ENDS WITH %s AND UNDERSTAND THAT THIS IS IRREVERSIBLE</string>
+    <string name="euicc_memory_reset_invoke_button">Erase</string>
+
     <string name="yes">Yes</string>
     <string name="no">No</string>
 
@@ -174,6 +185,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 erasing eUICC</string>
+    <string name="pref_developer_euicc_memory_reset_desc">This is a dangerous operation and hidden by default. As an alternative, you can delete all profiles manually.</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>

+ 7 - 0
app-common/src/main/res/xml/pref_settings.xml

@@ -69,6 +69,13 @@
             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" />
+
     </im.angry.openeuicc.ui.preference.LongSummaryPreferenceCategory>
 
     <PreferenceCategory