From aefa79b18b93536834f97a338fc4645558a838d8 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 14 Dec 2024 13:47:23 -0500 Subject: [PATCH 01/11] refactor: Channel format should use DI instead of resource overriding i18n makes resource overriding unreliable. --- .../main/java/im/angry/openeuicc/di/AppContainer.kt | 1 + .../im/angry/openeuicc/di/CustomizableTextProvider.kt | 9 +++++++++ .../java/im/angry/openeuicc/di/DefaultAppContainer.kt | 4 ++++ .../openeuicc/di/DefaultCustomizableTextProvider.kt | 9 +++++++++ .../main/java/im/angry/openeuicc/ui/MainActivity.kt | 3 ++- .../ui/wizard/DownloadWizardSlotSelectFragment.kt | 2 +- app-common/src/main/res/values-ja/strings.xml | 1 - app-common/src/main/res/values-zh-rCN/strings.xml | 1 - app-common/src/main/res/values/strings.xml | 1 - .../im/angry/openeuicc/di/UnprivilegedAppContainer.kt | 4 ++++ .../di/UnprivilegedCustomizableTextProvider.kt | 10 ++++++++++ app-unpriv/src/main/res/values/strings.xml | 2 +- 12 files changed, 41 insertions(+), 6 deletions(-) create mode 100644 app-common/src/main/java/im/angry/openeuicc/di/CustomizableTextProvider.kt create mode 100644 app-common/src/main/java/im/angry/openeuicc/di/DefaultCustomizableTextProvider.kt create mode 100644 app-unpriv/src/main/java/im/angry/openeuicc/di/UnprivilegedCustomizableTextProvider.kt diff --git a/app-common/src/main/java/im/angry/openeuicc/di/AppContainer.kt b/app-common/src/main/java/im/angry/openeuicc/di/AppContainer.kt index 4b3c3cd..cae7e2e 100644 --- a/app-common/src/main/java/im/angry/openeuicc/di/AppContainer.kt +++ b/app-common/src/main/java/im/angry/openeuicc/di/AppContainer.kt @@ -15,4 +15,5 @@ interface AppContainer { val preferenceRepository: PreferenceRepository val uiComponentFactory: UiComponentFactory val euiccChannelFactory: EuiccChannelFactory + val customizableTextProvider: CustomizableTextProvider } \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/di/CustomizableTextProvider.kt b/app-common/src/main/java/im/angry/openeuicc/di/CustomizableTextProvider.kt new file mode 100644 index 0000000..bb8e696 --- /dev/null +++ b/app-common/src/main/java/im/angry/openeuicc/di/CustomizableTextProvider.kt @@ -0,0 +1,9 @@ +package im.angry.openeuicc.di + +interface CustomizableTextProvider { + /** + * Format the name of a logical slot; internal only -- not intended for + * other channels such as USB. + */ + fun formatInternalChannelName(logicalSlotId: Int): String +} \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/di/DefaultAppContainer.kt b/app-common/src/main/java/im/angry/openeuicc/di/DefaultAppContainer.kt index 93fd8b8..9b70099 100644 --- a/app-common/src/main/java/im/angry/openeuicc/di/DefaultAppContainer.kt +++ b/app-common/src/main/java/im/angry/openeuicc/di/DefaultAppContainer.kt @@ -38,4 +38,8 @@ open class DefaultAppContainer(context: Context) : AppContainer { override val euiccChannelFactory by lazy { DefaultEuiccChannelFactory(context) } + + override val customizableTextProvider by lazy { + DefaultCustomizableTextProvider(context) + } } \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/di/DefaultCustomizableTextProvider.kt b/app-common/src/main/java/im/angry/openeuicc/di/DefaultCustomizableTextProvider.kt new file mode 100644 index 0000000..d8f4b2c --- /dev/null +++ b/app-common/src/main/java/im/angry/openeuicc/di/DefaultCustomizableTextProvider.kt @@ -0,0 +1,9 @@ +package im.angry.openeuicc.di + +import android.content.Context +import im.angry.openeuicc.common.R + +open class DefaultCustomizableTextProvider(private val context: Context) : CustomizableTextProvider { + override fun formatInternalChannelName(logicalSlotId: Int): String = + context.getString(R.string.channel_name_format, logicalSlotId) +} \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt index 767e7e0..01d0ab2 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt @@ -163,7 +163,8 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { // but it could change in the future euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId) - val channelName = getString(R.string.channel_name_format, channel.logicalSlotId) + val channelName = + appContainer.customizableTextProvider.formatInternalChannelName(channel.logicalSlotId) newPages.add(Page(channel.logicalSlotId, channelName) { appContainer.uiComponentFactory.createEuiccManagementFragment(slotId, portId) }) 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 54dbc08..8879878 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 @@ -179,7 +179,7 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt title.text = if (item.logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { root.context.getString(R.string.usb) } else { - root.context.getString(R.string.download_wizard_slot_title, item.logicalSlotId) + appContainer.customizableTextProvider.formatInternalChannelName(item.logicalSlotId) } eID.text = item.eID activeProfile.text = item.enabledProfileName ?: root.context.getString(R.string.unknown) diff --git a/app-common/src/main/res/values-ja/strings.xml b/app-common/src/main/res/values-ja/strings.xml index d25af49..7d99cb7 100644 --- a/app-common/src/main/res/values-ja/strings.xml +++ b/app-common/src/main/res/values-ja/strings.xml @@ -43,7 +43,6 @@ 戻る 次へ ダウンロードする eSIM を選択または確認: - 論理スロット %d タイプ: リムーバブル 内部 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 3bb0d04..551435b 100644 --- a/app-common/src/main/res/values-zh-rCN/strings.xml +++ b/app-common/src/main/res/values-zh-rCN/strings.xml @@ -81,7 +81,6 @@ 返回 下一步 请选择或确认下载目标 eSIM 卡槽: - 逻辑卡槽 %d 类型: 可插拔 内置 diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index 61823f7..eec59c0 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -64,7 +64,6 @@ Back Next Select or confirm the eSIM you would like to download to: - Logical slot %d Type: Removable Internal diff --git a/app-unpriv/src/main/java/im/angry/openeuicc/di/UnprivilegedAppContainer.kt b/app-unpriv/src/main/java/im/angry/openeuicc/di/UnprivilegedAppContainer.kt index 22d5a62..4dbfe41 100644 --- a/app-unpriv/src/main/java/im/angry/openeuicc/di/UnprivilegedAppContainer.kt +++ b/app-unpriv/src/main/java/im/angry/openeuicc/di/UnprivilegedAppContainer.kt @@ -6,4 +6,8 @@ class UnprivilegedAppContainer(context: Context) : DefaultAppContainer(context) override val uiComponentFactory by lazy { UnprivilegedUiComponentFactory() } + + override val customizableTextProvider by lazy { + UnprivilegedCustomizableTextProvider(context) + } } \ No newline at end of file diff --git a/app-unpriv/src/main/java/im/angry/openeuicc/di/UnprivilegedCustomizableTextProvider.kt b/app-unpriv/src/main/java/im/angry/openeuicc/di/UnprivilegedCustomizableTextProvider.kt new file mode 100644 index 0000000..929ce84 --- /dev/null +++ b/app-unpriv/src/main/java/im/angry/openeuicc/di/UnprivilegedCustomizableTextProvider.kt @@ -0,0 +1,10 @@ +package im.angry.openeuicc.di + +import android.content.Context +import im.angry.easyeuicc.R + +class UnprivilegedCustomizableTextProvider(private val context: Context) : + DefaultCustomizableTextProvider(context) { + override fun formatInternalChannelName(logicalSlotId: Int): String = + context.getString(R.string.channel_name_format_unpriv, logicalSlotId) +} \ No newline at end of file diff --git a/app-unpriv/src/main/res/values/strings.xml b/app-unpriv/src/main/res/values/strings.xml index 9d80b0e..afed295 100644 --- a/app-unpriv/src/main/res/values/strings.xml +++ b/app-unpriv/src/main/res/values/strings.xml @@ -1,6 +1,6 @@ EasyEUICC - SIM %d + SIM %d Compatibility Check Open SIM Toolkit From 14ea84c36e208815196982f80ec8d5778cef8910 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 14 Dec 2024 14:21:05 -0500 Subject: [PATCH 02/11] ui: Add placeholder text for when no eUICC is found to TextProvider --- .../im/angry/openeuicc/di/CustomizableTextProvider.kt | 6 ++++++ .../openeuicc/di/DefaultCustomizableTextProvider.kt | 3 +++ .../angry/openeuicc/ui/NoEuiccPlaceholderFragment.kt | 9 +++++++-- .../im/angry/openeuicc/di/PrivilegedAppContainer.kt | 4 ++++ .../openeuicc/di/PrivilegedCustomizableTextProvider.kt | 10 ++++++++++ app/src/main/res/values-ja/strings.xml | 2 +- app/src/main/res/values-zh-rCN/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 8 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/im/angry/openeuicc/di/PrivilegedCustomizableTextProvider.kt diff --git a/app-common/src/main/java/im/angry/openeuicc/di/CustomizableTextProvider.kt b/app-common/src/main/java/im/angry/openeuicc/di/CustomizableTextProvider.kt index bb8e696..91dac65 100644 --- a/app-common/src/main/java/im/angry/openeuicc/di/CustomizableTextProvider.kt +++ b/app-common/src/main/java/im/angry/openeuicc/di/CustomizableTextProvider.kt @@ -1,6 +1,12 @@ package im.angry.openeuicc.di interface CustomizableTextProvider { + /** + * Explanation string for when no eUICC is found on the device. + * This could be different depending on whether the app is privileged or not. + */ + val noEuiccExplanation: String + /** * Format the name of a logical slot; internal only -- not intended for * other channels such as USB. diff --git a/app-common/src/main/java/im/angry/openeuicc/di/DefaultCustomizableTextProvider.kt b/app-common/src/main/java/im/angry/openeuicc/di/DefaultCustomizableTextProvider.kt index d8f4b2c..f4d526e 100644 --- a/app-common/src/main/java/im/angry/openeuicc/di/DefaultCustomizableTextProvider.kt +++ b/app-common/src/main/java/im/angry/openeuicc/di/DefaultCustomizableTextProvider.kt @@ -4,6 +4,9 @@ import android.content.Context import im.angry.openeuicc.common.R open class DefaultCustomizableTextProvider(private val context: Context) : CustomizableTextProvider { + override val noEuiccExplanation: String + get() = context.getString(R.string.no_euicc) + override fun formatInternalChannelName(logicalSlotId: Int): String = context.getString(R.string.channel_name_format, logicalSlotId) } \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/NoEuiccPlaceholderFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/NoEuiccPlaceholderFragment.kt index e9e44b1..7e96af3 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/NoEuiccPlaceholderFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/NoEuiccPlaceholderFragment.kt @@ -4,15 +4,20 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.TextView import androidx.fragment.app.Fragment import im.angry.openeuicc.common.R +import im.angry.openeuicc.util.* -class NoEuiccPlaceholderFragment : Fragment() { +class NoEuiccPlaceholderFragment : Fragment(), OpenEuiccContextMarker { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - return inflater.inflate(R.layout.fragment_no_euicc_placeholder, container, false) + val view = inflater.inflate(R.layout.fragment_no_euicc_placeholder, container, false) + val textView = view.requireViewById(R.id.no_euicc_placeholder) + textView.text = appContainer.customizableTextProvider.noEuiccExplanation + return view } } \ No newline at end of file diff --git a/app/src/main/java/im/angry/openeuicc/di/PrivilegedAppContainer.kt b/app/src/main/java/im/angry/openeuicc/di/PrivilegedAppContainer.kt index c5896f2..d821e68 100644 --- a/app/src/main/java/im/angry/openeuicc/di/PrivilegedAppContainer.kt +++ b/app/src/main/java/im/angry/openeuicc/di/PrivilegedAppContainer.kt @@ -23,4 +23,8 @@ class PrivilegedAppContainer(context: Context) : DefaultAppContainer(context) { override val euiccChannelFactory by lazy { PrivilegedEuiccChannelFactory(context) } + + override val customizableTextProvider by lazy { + PrivilegedCustomizableTextProvider(context) + } } \ No newline at end of file diff --git a/app/src/main/java/im/angry/openeuicc/di/PrivilegedCustomizableTextProvider.kt b/app/src/main/java/im/angry/openeuicc/di/PrivilegedCustomizableTextProvider.kt new file mode 100644 index 0000000..e53832f --- /dev/null +++ b/app/src/main/java/im/angry/openeuicc/di/PrivilegedCustomizableTextProvider.kt @@ -0,0 +1,10 @@ +package im.angry.openeuicc.di + +import android.content.Context +import im.angry.openeuicc.R + +class PrivilegedCustomizableTextProvider(private val context: Context) : + DefaultCustomizableTextProvider(context) { + override val noEuiccExplanation: String + get() = context.getString(R.string.no_euicc_priv) +} \ 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 5ff6740..29f3ef6 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -1,6 +1,6 @@ - このデバイスで eUICC が見つかりません。\nデバイスによってはアプリのメニューからデュアル SIM を有効化する必要があります。 + このデバイスで eUICC が見つかりません。\nデバイスによってはアプリのメニューからデュアル SIM を有効化する必要があります。 TelephonyManager (特権) デュアル SIM DSDS の状態が切り替わりました。モデムが再起動するまでお待ちください。 diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 9b34bb8..18497b2 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -1,6 +1,6 @@ - 在此设备上找不到 eUICC 芯片。\n在某些设备上,您可能需要先在此应用的菜单中启用双卡支持。 + 在此设备上找不到 eUICC 芯片。\n在某些设备上,您可能需要先在此应用的菜单中启用双卡支持。 双卡 双卡支持状态已切换。请等待基带重新启动。 此卡槽支持多个启用配置文件 (MEP)。要启用或禁用此功能,请使用\"卡槽映射\"工具。 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bb2233b..47c88bd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,6 +1,6 @@ OpenEUICC - No eUICC found on this device.\nOn some devices, you may need to enable dual SIM first in the menu of this app. + No eUICC found on this device.\nOn some devices, you may need to enable dual SIM first in the menu of this app. TelephonyManager (Privileged) Dual SIM From bc238c45cd637e5fd64219fa9888383eb7816bc0 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 14 Dec 2024 14:38:09 -0500 Subject: [PATCH 03/11] ui: Add switching timeout message to the new DI text provider --- .../java/im/angry/openeuicc/di/CustomizableTextProvider.kt | 5 +++++ .../im/angry/openeuicc/di/DefaultCustomizableTextProvider.kt | 3 +++ .../java/im/angry/openeuicc/ui/EuiccManagementFragment.kt | 2 +- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/di/CustomizableTextProvider.kt b/app-common/src/main/java/im/angry/openeuicc/di/CustomizableTextProvider.kt index 91dac65..2c86273 100644 --- a/app-common/src/main/java/im/angry/openeuicc/di/CustomizableTextProvider.kt +++ b/app-common/src/main/java/im/angry/openeuicc/di/CustomizableTextProvider.kt @@ -7,6 +7,11 @@ interface CustomizableTextProvider { */ val noEuiccExplanation: String + /** + * Shown when we timed out switching between profiles. + */ + val profileSwitchingTimeoutMessage: String + /** * Format the name of a logical slot; internal only -- not intended for * other channels such as USB. diff --git a/app-common/src/main/java/im/angry/openeuicc/di/DefaultCustomizableTextProvider.kt b/app-common/src/main/java/im/angry/openeuicc/di/DefaultCustomizableTextProvider.kt index f4d526e..b493611 100644 --- a/app-common/src/main/java/im/angry/openeuicc/di/DefaultCustomizableTextProvider.kt +++ b/app-common/src/main/java/im/angry/openeuicc/di/DefaultCustomizableTextProvider.kt @@ -7,6 +7,9 @@ open class DefaultCustomizableTextProvider(private val context: Context) : Custo override val noEuiccExplanation: String get() = context.getString(R.string.no_euicc) + override val profileSwitchingTimeoutMessage: String + get() = context.getString(R.string.enable_disable_timeout) + override fun formatInternalChannelName(logicalSlotId: Int): String = context.getString(R.string.channel_name_format, logicalSlotId) } \ 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 8e7b158..7056c17 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 @@ -261,7 +261,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, invalid = true // Timed out waiting for SIM to come back online, we can no longer assume that the LPA is still valid AlertDialog.Builder(requireContext()).apply { - setMessage(R.string.enable_disable_timeout) + setMessage(appContainer.customizableTextProvider.profileSwitchingTimeoutMessage) setPositiveButton(android.R.string.ok) { dialog, _ -> dialog.dismiss() requireActivity().finish() From 70f1e00eb4f5c7cf96a8a06cbc016bae6395b8d7 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 14 Dec 2024 14:55:27 -0500 Subject: [PATCH 04/11] refactor: Extract shared log-saving behavior ...so that we implement log-sharing once and apply it to both spots. --- .../im/angry/openeuicc/ui/LogsActivity.kt | 24 ++++++-------- .../DownloadWizardDiagnosticsFragment.kt | 26 ++++++--------- .../java/im/angry/openeuicc/util/UiUtils.kt | 32 ++++++++++++++++++- 3 files changed, 51 insertions(+), 31 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/LogsActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/LogsActivity.kt index 49bfa0f..c6ba256 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/LogsActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/LogsActivity.kt @@ -8,7 +8,6 @@ import android.view.View import android.widget.ScrollView import android.widget.TextView import androidx.activity.enableEdgeToEdge -import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import androidx.swiperefreshlayout.widget.SwipeRefreshLayout @@ -17,7 +16,6 @@ import im.angry.openeuicc.util.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.io.FileOutputStream import java.util.Date class LogsActivity : AppCompatActivity() { @@ -27,15 +25,15 @@ class LogsActivity : AppCompatActivity() { private lateinit var logStr: String private val saveLogs = - registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri -> - if (uri == null) return@registerForActivityResult - if (!this::logStr.isInitialized) return@registerForActivityResult - contentResolver.openFileDescriptor(uri, "w")?.use { - FileOutputStream(it.fileDescriptor).use { os -> - os.write(logStr.encodeToByteArray()) - } - } - } + setupLogSaving( + getLogFileName = { + getString( + R.string.logs_filename_template, + SimpleDateFormat.getDateTimeInstance().format(Date()) + ) + }, + getLogText = { logStr } + ) override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() @@ -76,9 +74,7 @@ class LogsActivity : AppCompatActivity() { true } R.id.save -> { - saveLogs.launch(getString(R.string.logs_filename_template, - SimpleDateFormat.getDateTimeInstance().format(Date()) - )) + saveLogs() true } else -> super.onOptionsItemSelected(item) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt index fbbd1a0..e282196 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt @@ -6,10 +6,8 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView -import androidx.activity.result.contract.ActivityResultContracts import im.angry.openeuicc.common.R import im.angry.openeuicc.util.* -import java.io.FileOutputStream import java.util.Date class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardStepFragment() { @@ -21,14 +19,15 @@ class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardS private lateinit var diagnosticTextView: TextView private val saveDiagnostics = - registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri -> - if (uri == null) return@registerForActivityResult - requireActivity().contentResolver.openFileDescriptor(uri, "w")?.use { - FileOutputStream(it.fileDescriptor).use { os -> - os.write(diagnosticTextView.text.toString().encodeToByteArray()) - } - } - } + setupLogSaving( + getLogFileName = { + getString( + R.string.download_wizard_diagnostics_file_template, + SimpleDateFormat.getDateTimeInstance().format(Date()) + ) + }, + getLogText = { diagnosticTextView.text.toString() } + ) override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment? = null @@ -41,12 +40,7 @@ class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardS ): View? { val view = inflater.inflate(R.layout.fragment_download_diagnostics, container, false) view.requireViewById(R.id.download_wizard_diagnostics_save).setOnClickListener { - saveDiagnostics.launch( - getString( - R.string.download_wizard_diagnostics_file_template, - SimpleDateFormat.getDateTimeInstance().format(Date()) - ) - ) + saveDiagnostics() } diagnosticTextView = view.requireViewById(R.id.download_wizard_diagnostics_text) return view diff --git a/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt index fbede87..b02574e 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt @@ -1,17 +1,21 @@ package im.angry.openeuicc.util +import android.content.Context import android.content.res.Resources import android.graphics.Rect import android.view.View import android.view.ViewGroup +import androidx.activity.result.ActivityResultCaller +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.widget.Toolbar import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding import androidx.fragment.app.DialogFragment +import androidx.fragment.app.Fragment import im.angry.openeuicc.common.R +import java.io.FileOutputStream // Source: /** @@ -69,4 +73,30 @@ fun setupRootViewInsets(view: ViewGroup) { WindowInsetsCompat.CONSUMED } +} + +fun T.setupLogSaving( + getLogFileName: () -> String, + getLogText: () -> String +): () -> Unit { + val launchSaveIntent = + registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri -> + if (uri == null) return@registerForActivityResult + + val contentResolver = when (this@setupLogSaving) { + is Context -> contentResolver + is Fragment -> requireContext().contentResolver + else -> throw IllegalArgumentException("Must be either Context or Fragment!") + } + + contentResolver.openFileDescriptor(uri, "w")?.use { + FileOutputStream(it.fileDescriptor).use { os -> + os.write(getLogText().encodeToByteArray()) + } + } + } + + return { + launchSaveIntent.launch(getLogFileName()) + } } \ No newline at end of file From ec334d104aa84610be95d077bc4bfd97ea3d0d98 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 14 Dec 2024 15:18:47 -0500 Subject: [PATCH 05/11] ui: Implement log content sharing --- .../java/im/angry/openeuicc/util/UiUtils.kt | 24 +++++++++++++++---- app-common/src/main/res/values/strings.xml | 2 ++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt index b02574e..c8e481c 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt @@ -1,12 +1,14 @@ package im.angry.openeuicc.util import android.content.Context +import android.content.Intent import android.content.res.Resources import android.graphics.Rect import android.view.View import android.view.ViewGroup import androidx.activity.result.ActivityResultCaller import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat @@ -83,17 +85,31 @@ fun T.setupLogSaving( registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri -> if (uri == null) return@registerForActivityResult - val contentResolver = when (this@setupLogSaving) { - is Context -> contentResolver - is Fragment -> requireContext().contentResolver + val context = when (this@setupLogSaving) { + is Context -> this@setupLogSaving + is Fragment -> requireContext() else -> throw IllegalArgumentException("Must be either Context or Fragment!") } - contentResolver.openFileDescriptor(uri, "w")?.use { + context.contentResolver.openFileDescriptor(uri, "w")?.use { FileOutputStream(it.fileDescriptor).use { os -> os.write(getLogText().encodeToByteArray()) } } + + AlertDialog.Builder(context).apply { + setMessage(R.string.logs_saved_message) + setNegativeButton(R.string.no) { _, _ -> } + setPositiveButton(R.string.yes) { _, _ -> + val intent = Intent().apply { + action = Intent.ACTION_SEND + type = "text/plain" + putExtra(Intent.EXTRA_STREAM, uri) + } + + context.startActivity(Intent.createChooser(intent, null)) + } + }.show() } return { diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index eec59c0..34448ff 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -94,6 +94,8 @@ Save Diagnostics at %s + Logs have been saved to the selected path. Would you like to share the log through another app? + New nickname Are you sure you want to delete the profile %s? This operation is irreversible. From 815d4d4324f265cfc81abe45ab375676922d73a4 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 14 Dec 2024 16:11:49 -0500 Subject: [PATCH 06/11] Expose USB device name as intrinsic name for use with download wizard --- .../im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt | 2 ++ .../src/main/java/im/angry/openeuicc/core/EuiccChannel.kt | 7 +++++++ .../main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt | 1 + .../java/im/angry/openeuicc/core/EuiccChannelWrapper.kt | 2 ++ .../ui/wizard/DownloadWizardSlotSelectFragment.kt | 8 +++++--- .../angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt | 1 + 6 files changed, 18 insertions(+), 3 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt index a8fa1d5..5e87564 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt @@ -37,6 +37,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha return EuiccChannelImpl( context.getString(R.string.omapi), port, + intrinsicChannelName = null, OmapiApduInterface( seService!!, port, @@ -67,6 +68,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha return EuiccChannelImpl( context.getString(R.string.usb), FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)), + intrinsicChannelName = usbDevice.productName, UsbApduInterface( conn, bulkIn, diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt index 4ef6808..541f867 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt @@ -16,5 +16,12 @@ interface EuiccChannel { val valid: Boolean + /** + * Intrinsic name of this channel. For device-internal SIM slots, + * this should be null; for USB readers, this should be the name of + * the reader device. + */ + val intrinsicChannelName: String? + fun close() } \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt index 79dec34..a281948 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt @@ -10,6 +10,7 @@ import net.typeblog.lpac_jni.impl.LocalProfileAssistantImpl class EuiccChannelImpl( override val type: String, override val port: UiccPortInfoCompat, + override val intrinsicChannelName: String?, apduInterface: ApduInterface, verboseLoggingFlow: Flow, ignoreTLSCertificateFlow: Flow diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt index ab01f22..6011f53 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt @@ -31,6 +31,8 @@ class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel { override val lpa: LocalProfileAssistant by lpaDelegate override val valid: Boolean get() = channel.valid + override val intrinsicChannelName: String? + get() = channel.intrinsicChannelName override fun close() = channel.close() 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 8879878..7e1dc23 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 @@ -35,7 +35,8 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt val eID: String, val freeSpace: Int, val imei: String, - val enabledProfileName: String? + val enabledProfileName: String?, + val intrinsicChannelName: String?, ) private var loaded = false @@ -106,7 +107,8 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt } catch (e: Exception) { "" }, - channel.lpa.profiles.find { it.state == LocalProfileInfo.State.Enabled }?.displayName + channel.lpa.profiles.find { it.state == LocalProfileInfo.State.Enabled }?.displayName, + channel.intrinsicChannelName, ) } }.toList().sortedBy { it.logicalSlotId } @@ -177,7 +179,7 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt } title.text = if (item.logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { - root.context.getString(R.string.usb) + item.intrinsicChannelName ?: root.context.getString(R.string.usb) } else { appContainer.customizableTextProvider.formatInternalChannelName(item.logicalSlotId) } diff --git a/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt b/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt index 1537fc9..b690c79 100644 --- a/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt +++ b/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt @@ -30,6 +30,7 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto return EuiccChannelImpl( context.getString(R.string.telephony_manager), port, + intrinsicChannelName = null, TelephonyManagerApduInterface( port, tm, From 343dfb43f8346f967922d31d67bcc9cdb5e58d04 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 14 Dec 2024 16:13:47 -0500 Subject: [PATCH 07/11] Remove unused SlotSelectFragment --- .../angry/openeuicc/ui/SlotSelectFragment.kt | 93 ------------------- 1 file changed, 93 deletions(-) delete mode 100644 app-common/src/main/java/im/angry/openeuicc/ui/SlotSelectFragment.kt diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/SlotSelectFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/SlotSelectFragment.kt deleted file mode 100644 index 2c4fe3c..0000000 --- a/app-common/src/main/java/im/angry/openeuicc/ui/SlotSelectFragment.kt +++ /dev/null @@ -1,93 +0,0 @@ -package im.angry.openeuicc.ui - -import android.content.DialogInterface -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ArrayAdapter -import android.widget.Spinner -import androidx.appcompat.widget.Toolbar -import im.angry.openeuicc.common.R -import im.angry.openeuicc.core.EuiccChannel -import im.angry.openeuicc.util.* - -class SlotSelectFragment : BaseMaterialDialogFragment(), OpenEuiccContextMarker { - companion object { - const val TAG = "SlotSelectFragment" - - fun newInstance(slotIds: List, logicalSlotIds: List, portIds: List): SlotSelectFragment { - return SlotSelectFragment().apply { - arguments = Bundle().apply { - putIntArray("slotIds", slotIds.toIntArray()) - putIntArray("logicalSlotIds", logicalSlotIds.toIntArray()) - putIntArray("portIds", portIds.toIntArray()) - } - } - } - } - - interface SlotSelectedListener { - fun onSlotSelected(slotId: Int, portId: Int) - fun onSlotSelectCancelled() - } - - private lateinit var toolbar: Toolbar - private lateinit var spinner: Spinner - private lateinit var adapter: ArrayAdapter - private lateinit var slotIds: IntArray - private lateinit var logicalSlotIds: IntArray - private lateinit var portIds: IntArray - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - val view = inflater.inflate(R.layout.fragment_slot_select, container, false) - - toolbar = view.requireViewById(R.id.toolbar) - toolbar.setTitle(R.string.slot_select) - toolbar.inflateMenu(R.menu.fragment_slot_select) - - adapter = ArrayAdapter(inflater.context, R.layout.spinner_item) - - spinner = view.requireViewById(R.id.spinner) - spinner.adapter = adapter - - return view - } - - override fun onStart() { - super.onStart() - - slotIds = requireArguments().getIntArray("slotIds")!! - logicalSlotIds = requireArguments().getIntArray("logicalSlotIds")!! - portIds = requireArguments().getIntArray("portIds")!! - - logicalSlotIds.forEach { id -> - adapter.add(getString(R.string.channel_name_format, id)) - } - - toolbar.setNavigationOnClickListener { - (requireActivity() as SlotSelectedListener).onSlotSelectCancelled() - } - toolbar.setOnMenuItemClickListener { - val slotId = slotIds[spinner.selectedItemPosition] - val portId = portIds[spinner.selectedItemPosition] - (requireActivity() as SlotSelectedListener).onSlotSelected(slotId, portId) - dismiss() - true - } - } - - override fun onResume() { - super.onResume() - setWidthPercent(75) - } - - override fun onCancel(dialog: DialogInterface) { - super.onCancel(dialog) - (requireActivity() as SlotSelectedListener).onSlotSelectCancelled() - } -} \ No newline at end of file From 55c99831f396b89926b128a978cb3961e59ef643 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 14 Dec 2024 20:36:59 -0500 Subject: [PATCH 08/11] ui: Copyable eid string peter: redid parts of EuiccInfoViewHolder to remove the need for root context (we can get it from activity directly). Co-Authored-By: septs commit 2a6eb746bff034ed6032b8eac7462963bb39bcf1 Author: septs Date: Sat Dec 14 11:26:42 2024 +0800 feat: hide copied toast in android 13 or higher commit d5bfd090b7b2e2c74c2cef698a43fc3607db260c Merge: 0b2a498 aed2479 Author: septs Date: Sat Dec 14 03:58:59 2024 +0800 Merge branch 'master' into copyable-eid-string commit 0b2a4988f2a2c2c7131a06867fc6bf622b281828 Author: septs Date: Fri Dec 13 12:35:27 2024 +0800 refactor: eid copied toast commit ce854575aae304de68bddc0490998af5729632ab Author: septs Date: Fri Dec 13 12:27:18 2024 +0800 revert: EuiccManagementFragment commit 41229cac5727001ecb21992f5005b5b96b1d9557 Author: septs Date: Fri Dec 13 12:27:10 2024 +0800 revert: EuiccManagementFragment commit 1f2b2d73ddfdc690442c0167e7f2f1748a78c921 Author: septs Date: Fri Dec 13 12:25:01 2024 +0800 feat: add euiccinfo activity specific strings commit ca76ac841eab489ff1f825fef2f2aecd1ae88bb1 Merge: 110d490 55d96c6 Author: septs Date: Fri Dec 13 12:23:34 2024 +0800 Merge branch 'master' into copyable-eid-string commit 110d49005a0cba26ee72eedfda0fc9e38a501d2a Author: septs Date: Fri Dec 13 12:23:21 2024 +0800 revert: common strings commit dc12e8d4107b9c80a1ca2bee0c5897f3e7d8dfa4 Author: septs Date: Fri Dec 13 12:22:47 2024 +0800 revert: unpriv strings commit ccba9be141c533145464da3dbbc88afa16fac1fd Merge: 34c5e41 0687354 Author: septs Date: Fri Dec 13 12:15:42 2024 +0800 Merge branch 'master' of ssh://gitea-ssh.angry.im:2222/PeterCxy/OpenEUICC into copyable-eid-string commit 34c5e41dde663026b72b2c80301bfde2d14c0964 Author: septs Date: Thu Dec 12 11:45:59 2024 +0800 chore: simplify clickable commit 4f1ed329f12b590691d273ac2150cb81ded11521 Author: septs Date: Thu Dec 12 11:37:15 2024 +0800 feat: copyable eid string --- .../angry/openeuicc/ui/EuiccInfoActivity.kt | 80 +++++++++++++------ app-common/src/main/res/values/strings.xml | 1 + 2 files changed, 58 insertions(+), 23 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt index 84b300e..36031dd 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt @@ -1,13 +1,18 @@ package im.angry.openeuicc.ui import android.annotation.SuppressLint +import android.content.ClipData +import android.content.ClipboardManager +import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.widget.TextView +import android.widget.Toast import androidx.activity.enableEdgeToEdge +import androidx.annotation.StringRes import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager @@ -32,6 +37,13 @@ class EuiccInfoActivity : BaseEuiccAccessActivity() { private var logicalSlotId: Int = -1 + data class Item( + @StringRes + val titleResId: Int, + val content: String?, + val copiedToastResId: Int? = null + ) + override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) @@ -41,12 +53,11 @@ class EuiccInfoActivity : BaseEuiccAccessActivity() { supportActionBar!!.setDisplayHomeAsUpEnabled(true) swipeRefresh = requireViewById(R.id.swipe_refresh) - infoList = requireViewById(R.id.recycler_view) - - infoList.layoutManager = - LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) - infoList.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL)) - infoList.adapter = EuiccInfoAdapter() + infoList = requireViewById(R.id.recycler_view).also { + it.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) + it.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL)) + it.adapter = EuiccInfoAdapter() + } logicalSlotId = intent.getIntExtra("logicalSlotId", 0) @@ -81,29 +92,33 @@ class EuiccInfoActivity : BaseEuiccAccessActivity() { lifecycleScope.launch { (infoList.adapter!! as EuiccInfoAdapter).euiccInfoItems = - euiccChannelManager.withEuiccChannel(logicalSlotId, ::buildPairs).map { - Pair(getString(it.first), it.second ?: getString(R.string.unknown)) - } + euiccChannelManager.withEuiccChannel(logicalSlotId, ::buildEuiccInfoItems) swipeRefresh.isRefreshing = false } } - private fun buildPairs(channel: EuiccChannel) = buildList { - add(Pair(R.string.euicc_info_access_mode, channel.type)) + private fun buildEuiccInfoItems(channel: EuiccChannel) = buildList { + add(Item(R.string.euicc_info_access_mode, channel.type)) add( - Pair( + Item( R.string.euicc_info_removable, formatByBoolean(channel.port.card.isRemovable, YES_NO) ) ) - add(Pair(R.string.euicc_info_eid, channel.lpa.eID)) + add( + Item( + R.string.euicc_info_eid, + channel.lpa.eID, + copiedToastResId = R.string.toast_eid_copied + ) + ) channel.lpa.euiccInfo2.let { info -> - add(Pair(R.string.euicc_info_firmware_version, info?.euiccFirmwareVersion)) - add(Pair(R.string.euicc_info_globalplatform_version, info?.globalPlatformVersion)) - add(Pair(R.string.euicc_info_pp_version, info?.ppVersion)) - add(Pair(R.string.euicc_info_sas_accreditation_number, info?.sasAccreditationNumber)) - add(Pair(R.string.euicc_info_free_nvram, info?.freeNvram?.let(::formatFreeSpace))) + add(Item(R.string.euicc_info_firmware_version, info?.euiccFirmwareVersion)) + add(Item(R.string.euicc_info_globalplatform_version, info?.globalPlatformVersion)) + add(Item(R.string.euicc_info_pp_version, info?.ppVersion)) + add(Item(R.string.euicc_info_sas_accreditation_number, info?.sasAccreditationNumber)) + add(Item(R.string.euicc_info_free_nvram, info?.freeNvram?.let(::formatFreeSpace))) } channel.lpa.euiccInfo2?.euiccCiPKIdListForSigning.orEmpty().let { signers -> // SGP.28 v1.0, eSIM CI Registration Criteria (Page 5 of 9, 2019-10-24) @@ -116,7 +131,7 @@ class EuiccInfoActivity : BaseEuiccAccessActivity() { PKID_GSMA_TEST_CI.any(signers::contains) -> R.string.euicc_info_ci_gsma_test else -> R.string.euicc_info_ci_unknown } - add(Pair(R.string.euicc_info_ci_type, getString(resId))) + add(Item(R.string.euicc_info_ci_type, getString(resId))) } } @@ -132,15 +147,34 @@ class EuiccInfoActivity : BaseEuiccAccessActivity() { inner class EuiccInfoViewHolder(root: View) : ViewHolder(root) { private val title: TextView = root.requireViewById(R.id.euicc_info_title) private val content: TextView = root.requireViewById(R.id.euicc_info_content) + private var copiedToastResId: Int? = null - fun bind(item: Pair) { - title.text = item.first - content.text = item.second + init { + root.setOnClickListener { + if (copiedToastResId != null) { + val label = title.text.toString() + getSystemService(ClipboardManager::class.java)!! + .setPrimaryClip(ClipData.newPlainText(label, content.text)) + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { + Toast.makeText( + this@EuiccInfoActivity, + copiedToastResId!!, + Toast.LENGTH_SHORT + ).show() + } + } + } + } + + fun bind(item: Item) { + copiedToastResId = item.copiedToastResId + title.setText(item.titleResId) + content.text = item.content ?: getString(R.string.unknown) } } inner class EuiccInfoAdapter : RecyclerView.Adapter() { - var euiccInfoItems: List> = listOf() + var euiccInfoItems: List = listOf() @SuppressLint("NotifyDataSetChanged") set(newVal) { field = newVal diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index 34448ff..e349c68 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -31,6 +31,7 @@ Nickname cannot be longer than 64 characters Confirmation string mismatch ICCID copied to clipboard + EID copied to clipboard Select Slot Select From 5a8d92c3df844fbbf6ad15db86c885ad21cdfb10 Mon Sep 17 00:00:00 2001 From: septs Date: Sun, 15 Dec 2024 02:44:44 +0100 Subject: [PATCH 09/11] feat: hide copied toast in android 13 or higher (#114) see https://developer.android.com/develop/ui/views/touch-and-input/copy-paste#duplicate-notifications Co-authored-by: Peter Cai Reviewed-on: https://gitea.angry.im/PeterCxy/OpenEUICC/pulls/114 Co-authored-by: septs Co-committed-by: septs --- .../java/im/angry/openeuicc/ui/EuiccManagementFragment.kt | 4 +++- .../im/angry/openeuicc/ui/UnprivilegedSettingsFragment.kt | 4 +++- 2 files changed, 6 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 7056c17..f806ae0 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 @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.content.ClipData import android.content.ClipboardManager import android.content.Intent +import android.os.Build import android.os.Bundle import android.text.method.PasswordTransformationMethod import android.view.LayoutInflater @@ -348,7 +349,8 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, iccid.setOnLongClickListener { requireContext().getSystemService(ClipboardManager::class.java)!! .setPrimaryClip(ClipData.newPlainText("iccid", iccid.text)) - Toast.makeText(requireContext(), R.string.toast_iccid_copied, Toast.LENGTH_SHORT) + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) Toast + .makeText(requireContext(), R.string.toast_iccid_copied, Toast.LENGTH_SHORT) .show() true } diff --git a/app-unpriv/src/main/java/im/angry/openeuicc/ui/UnprivilegedSettingsFragment.kt b/app-unpriv/src/main/java/im/angry/openeuicc/ui/UnprivilegedSettingsFragment.kt index 1ca9d0f..1ef3e89 100644 --- a/app-unpriv/src/main/java/im/angry/openeuicc/ui/UnprivilegedSettingsFragment.kt +++ b/app-unpriv/src/main/java/im/angry/openeuicc/ui/UnprivilegedSettingsFragment.kt @@ -3,6 +3,7 @@ package im.angry.openeuicc.ui import android.content.ClipData import android.content.ClipboardManager import android.content.pm.PackageManager +import android.os.Build import android.os.Bundle import android.widget.Toast import androidx.preference.Preference @@ -35,7 +36,8 @@ class UnprivilegedSettingsFragment : SettingsFragment() { setOnPreferenceClickListener { requireContext().getSystemService(ClipboardManager::class.java)!! .setPrimaryClip(ClipData.newPlainText("ara-m", summary)) - Toast.makeText(requireContext(), R.string.toast_ara_m_copied, Toast.LENGTH_SHORT) + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) Toast + .makeText(requireContext(), R.string.toast_ara_m_copied, Toast.LENGTH_SHORT) .show() true } From 60396796935b32add43645de560b1fa6f5acdb76 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 14 Dec 2024 20:57:53 -0500 Subject: [PATCH 10/11] refactor: Remove the need for specifying preference keys when binding Co-authored-by: septs --- .../im/angry/openeuicc/ui/SettingsFragment.kt | 25 ++++++++----------- .../angry/openeuicc/util/PreferenceUtils.kt | 21 +++++++++++++--- 2 files changed, 27 insertions(+), 19 deletions(-) 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 c2cbee3..cb801af 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 @@ -6,7 +6,6 @@ import android.os.Build import android.os.Bundle import android.provider.Settings import android.widget.Toast -import androidx.datastore.preferences.core.Preferences import androidx.lifecycle.lifecycleScope import androidx.preference.CheckBoxPreference import androidx.preference.Preference @@ -14,7 +13,6 @@ import androidx.preference.PreferenceCategory import androidx.preference.PreferenceFragmentCompat import im.angry.openeuicc.common.R import im.angry.openeuicc.util.* -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -60,25 +58,25 @@ open class SettingsFragment: PreferenceFragmentCompat() { } findPreference("pref_notifications_download") - ?.bindBooleanFlow(preferenceRepository.notificationDownloadFlow, PreferenceKeys.NOTIFICATION_DOWNLOAD) + ?.bindBooleanFlow(preferenceRepository.notificationDownloadFlow) findPreference("pref_notifications_delete") - ?.bindBooleanFlow(preferenceRepository.notificationDeleteFlow, PreferenceKeys.NOTIFICATION_DELETE) + ?.bindBooleanFlow(preferenceRepository.notificationDeleteFlow) findPreference("pref_notifications_switch") - ?.bindBooleanFlow(preferenceRepository.notificationSwitchFlow, PreferenceKeys.NOTIFICATION_SWITCH) + ?.bindBooleanFlow(preferenceRepository.notificationSwitchFlow) findPreference("pref_advanced_disable_safeguard_removable_esim") - ?.bindBooleanFlow(preferenceRepository.disableSafeguardFlow, PreferenceKeys.DISABLE_SAFEGUARD_REMOVABLE_ESIM) + ?.bindBooleanFlow(preferenceRepository.disableSafeguardFlow) findPreference("pref_advanced_verbose_logging") - ?.bindBooleanFlow(preferenceRepository.verboseLoggingFlow, PreferenceKeys.VERBOSE_LOGGING) + ?.bindBooleanFlow(preferenceRepository.verboseLoggingFlow) findPreference("pref_developer_unfiltered_profile_list") - ?.bindBooleanFlow(preferenceRepository.unfilteredProfileListFlow, PreferenceKeys.UNFILTERED_PROFILE_LIST) + ?.bindBooleanFlow(preferenceRepository.unfilteredProfileListFlow) findPreference("pref_ignore_tls_certificate") - ?.bindBooleanFlow(preferenceRepository.ignoreTLSCertificateFlow, PreferenceKeys.IGNORE_TLS_CERTIFICATE) + ?.bindBooleanFlow(preferenceRepository.ignoreTLSCertificateFlow) } override fun onStart() { @@ -99,10 +97,7 @@ open class SettingsFragment: PreferenceFragmentCompat() { if (numClicks == 7) { lifecycleScope.launch { - preferenceRepository.updatePreference( - PreferenceKeys.DEVELOPER_OPTIONS_ENABLED, - true - ) + preferenceRepository.developerOptionsEnabledFlow.updatePreference(true) lastToast?.cancel() Toast.makeText( @@ -124,14 +119,14 @@ open class SettingsFragment: PreferenceFragmentCompat() { return true } - private fun CheckBoxPreference.bindBooleanFlow(flow: Flow, key: Preferences.Key) { + private fun CheckBoxPreference.bindBooleanFlow(flow: PreferenceFlowWrapper) { lifecycleScope.launch { flow.collect { isChecked = it } } setOnPreferenceChangeListener { _, newValue -> runBlocking { - preferenceRepository.updatePreference(key, newValue as Boolean) + flow.updatePreference(newValue as Boolean) } true } 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 e768fa9..f5e3ca2 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 @@ -19,7 +19,7 @@ val Context.preferenceRepository: PreferenceRepository val Fragment.preferenceRepository: PreferenceRepository get() = requireContext().preferenceRepository -object PreferenceKeys { +internal object PreferenceKeys { // ---- Profile Notifications ---- val NOTIFICATION_DOWNLOAD = booleanPreferencesKey("notification_download") val NOTIFICATION_DELETE = booleanPreferencesKey("notification_delete") @@ -51,9 +51,22 @@ class PreferenceRepository(private val context: Context) { val unfilteredProfileListFlow = bindFlow(PreferenceKeys.UNFILTERED_PROFILE_LIST, false) val ignoreTLSCertificateFlow = bindFlow(PreferenceKeys.IGNORE_TLS_CERTIFICATE, false) - private fun bindFlow(key: Preferences.Key, defaultValue: T): Flow = - context.dataStore.data.map { it[key] ?: defaultValue } + private fun bindFlow(key: Preferences.Key, defaultValue: T): PreferenceFlowWrapper = + PreferenceFlowWrapper(context, key, defaultValue) +} - suspend fun updatePreference(key: Preferences.Key, value: T) = +class PreferenceFlowWrapper private constructor( + private val context: Context, + private val key: Preferences.Key, + inner: Flow +) : Flow by inner { + 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 From 9d18253e44574a4cf518523a339e4cada2f3f57c Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 14 Dec 2024 22:47:00 -0500 Subject: [PATCH 11/11] ui: Cancel download from the low nvram warning dialog --- .../openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 7e1dc23..f16a086 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 @@ -62,7 +62,9 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt setMessage(R.string.profile_download_low_nvram_message) setCancelable(true) setPositiveButton(android.R.string.ok, null) - setNegativeButton(android.R.string.cancel, null) + setNegativeButton(android.R.string.cancel) { _, _ -> + requireActivity().finish() + } show() } }