Option MICROSD_TOAST_TIMER_FOREVER refactor

- Prevent escaping the RemoveMicroSDWarningView by launching SettingsEntryUpdateSelectionView
  in a blocking mode. If the tracked setting remains unchanged, Back/normal navigation
  returns to the blocking view (RemoveMicroSDWarningView).
- Only 2 ways of escaping this mode: 1) physically removing MicroSD and
2) modifying the setting
- Add test tests/test_flows.py::TestFlowTest::test_remove_microsd_blocking covering:
This commit is contained in:
alvroble
2025-10-25 00:21:19 +02:00
parent 21bf4e8eaf
commit 663ada8123
3 changed files with 87 additions and 12 deletions
+19 -4
View File
@@ -150,11 +150,15 @@ class SettingsEntryUpdateSelectionView(View):
Handles changes to all selection-type settings (Multiselect, SELECT_1,
Enabled/Disabled, etc).
"""
def __init__(self, attr_name: str, parent_initial_scroll: int = 0, selected_button: int = None):
def __init__(self, attr_name: str, parent_initial_scroll: int = 0, selected_button: int = None, blocking_view: View = None, unblocking_view: View = None):
super().__init__()
self.settings_entry = SettingsDefinition.get_settings_entry(attr_name)
self.selected_button = selected_button
self.parent_initial_scroll = parent_initial_scroll
# If the setting remains unchanged, navigation should return to blocking_view (if set)
self.blocking_view = blocking_view
# unblocking_view is an optional target to navigate to once the setting actually changes.
self.unblocking_view = unblocking_view
def run(self):
@@ -200,6 +204,8 @@ class SettingsEntryUpdateSelectionView(View):
)
if ret_value == RET_CODE__BACK_BUTTON:
if self.blocking_view:
return Destination(self.blocking_view, clear_history=True)
return settings_menu_view_destination
value = self.settings_entry.get_selection_option_value(ret_value)
@@ -219,8 +225,7 @@ class SettingsEntryUpdateSelectionView(View):
else:
# All other types are single selects (e.g. Enabled/Disabled, SELECT_1)
if value == initial_value:
# No change, return to menu
if value == initial_value and not self.blocking_view:
return settings_menu_view_destination
else:
updated_value = value
@@ -239,10 +244,20 @@ class SettingsEntryUpdateSelectionView(View):
if destination:
return destination
# If this selection view was opened from a blocking flow (e.g. RemoveMicroSDWarningView),
# prevent navigation away until the setting actually changes. If it hasn't changed,
# return to the blocking view so it can re-evaluate the state.
if self.blocking_view:
current_value = self.settings.get_value(self.settings_entry.attr_name)
if current_value == initial_value:
return Destination(self.blocking_view, clear_history=True)
elif self.unblocking_view:
return Destination(self.unblocking_view, clear_history=True)
# All selects stay in place; re-initialize where in the list we left off
self.selected_button = ret_value
return Destination(SettingsEntryUpdateSelectionView, view_args=dict(attr_name=self.settings_entry.attr_name, parent_initial_scroll=self.parent_initial_scroll, selected_button=self.selected_button), skip_current_view=True)
return Destination(SettingsEntryUpdateSelectionView, view_args=dict(attr_name=self.settings_entry.attr_name, parent_initial_scroll=self.parent_initial_scroll, selected_button=self.selected_button, blocking_view=self.blocking_view, unblocking_view=self.unblocking_view), skip_current_view=True)
+17 -5
View File
@@ -412,10 +412,10 @@ class OptionDisabledView(View):
class RemoveMicroSDWarningView(View):
CONTINUE = ButtonOption("Continue")
DISMISS = ButtonOption("Dismiss")
SETTINGS = ButtonOption("Settings")
def run(self):
button_data = [self.CONTINUE, self.DISMISS]
button_data = [self.CONTINUE, self.SETTINGS]
selected_menu_num = self.run_screen(
WarningScreen,
title=_("Action Required"),
@@ -426,8 +426,20 @@ class RemoveMicroSDWarningView(View):
button_data=button_data,
)
if button_data[selected_menu_num] == self.CONTINUE:
from seedsigner.hardware.microsd import MicroSD
if button_data[selected_menu_num] == self.CONTINUE and MicroSD.get_instance().is_inserted:
return Destination(RemoveMicroSDWarningView, clear_history=True)
else:
if not MicroSD.get_instance().is_inserted:
return Destination(MainMenuView, clear_history=True)
else:
return Destination(RemoveMicroSDWarningView, clear_history=True)
elif button_data[selected_menu_num] == self.SETTINGS:
from seedsigner.views.settings_views import SettingsEntryUpdateSelectionView
return Destination(
SettingsEntryUpdateSelectionView,
view_args=dict(
attr_name=SettingsConstants.SETTING__MICROSD_TOAST_TIMER,
blocking_view=RemoveMicroSDWarningView,
unblocking_view=MainMenuView
)
)
+49 -1
View File
@@ -9,8 +9,12 @@ from seedsigner.models.seed import Seed
from seedsigner.views import scan_views
from seedsigner.views.psbt_views import PSBTSelectSeedView
from seedsigner.views.seed_views import SeedBackupView, SeedMnemonicEntryView, SeedOptionsView, SeedsMenuView
from seedsigner.views.view import Destination, MainMenuView, PowerOptionsView, UnhandledExceptionView, View
from seedsigner.views.view import Destination, MainMenuView, PowerOptionsView, UnhandledExceptionView, RemoveMicroSDWarningView, MainMenuView, View
from seedsigner.views.tools_views import ToolsMenuView, ToolsCalcFinalWordNumWordsView
from seedsigner.views.settings_views import SettingsEntryUpdateSelectionView
from seedsigner.models.settings_definition import SettingsDefinition
from seedsigner.models.settings import SettingsConstants
from seedsigner.hardware.microsd import MicroSD
@@ -192,3 +196,47 @@ class TestFlowTest(FlowTest):
FlowStep(MainMenuView), # Need a next Destination to force the first step to run
])
def test_remove_microsd_blocking(self):
"""
Verifies three related behaviors:
1) If the RemoveMicroSDWarningView launches the SettingsEntryUpdateSelectionView
and the user presses Back without changing the tracked setting, the flow
returns to RemoveMicroSDWarningView (the blocking condition remains).
2) If the user changes the tracked setting while in the settings entry, the
flow unblocks and navigates to MainMenuView.
3) If the MicroSD is physically removed and the user presses Continue on the
warning, the flow proceeds to MainMenuView.
"""
controller = Controller.get_instance()
settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__MICROSD_TOAST_TIMER)
controller.settings.set_value(settings_entry.attr_name, SettingsConstants.MICROSD_TOAST_TIMER_FOREVER)
# There are only two ways of exiting RemoveMicroSDWarningView when SETTING__MICROSD_TOAST_TIMER -> MICROSD_TOAST_TIMER_FOREVER
self.run_sequence([
FlowStep(RemoveMicroSDWarningView, button_data_selection=RemoveMicroSDWarningView.SETTINGS),
FlowStep(SettingsEntryUpdateSelectionView, screen_return_value=RET_CODE__BACK_BUTTON),
FlowStep(RemoveMicroSDWarningView, button_data_selection=RemoveMicroSDWarningView.SETTINGS),
# 1) Modifying the setting
FlowStep(SettingsEntryUpdateSelectionView, screen_return_value=0),
FlowStep(MainMenuView)
])
self.reset_controller()
controller = Controller.get_instance()
settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__MICROSD_TOAST_TIMER)
controller.settings.set_value(settings_entry.attr_name, SettingsConstants.MICROSD_TOAST_TIMER_FOREVER)
# 2) Removing the MicroSD card and pressing CONTINUE
self.mock_microsd.is_inserted = False
assert MicroSD.get_instance().is_inserted is False
self.run_sequence([
FlowStep(RemoveMicroSDWarningView, button_data_selection=RemoveMicroSDWarningView.CONTINUE),
FlowStep(MainMenuView)
])
# Restore the setting for the controller
controller.settings.set_value(settings_entry.attr_name, SettingsConstants.MICROSD_TOAST_TIMER_FIVE_SECONDS)