From bae7fb057a0717314248f28f9f39b3b8d6533f32 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Wed, 12 Nov 2025 11:56:16 +0100 Subject: [PATCH] Add auto_start_app setting --- .../assets/settings.py | 5 ++- internal_filesystem/lib/mpos/apps.py | 38 +++++++++++-------- .../lib/mpos/content/package_manager.py | 10 ++++- internal_filesystem/lib/mpos/ui/topmenu.py | 4 +- internal_filesystem/main.py | 9 ++++- scripts/bundle_apps.sh | 5 ++- tests/test_start_app.py | 37 ++++++++++++++++++ 7 files changed, 86 insertions(+), 22 deletions(-) create mode 100644 tests/test_start_app.py diff --git a/internal_filesystem/builtin/apps/com.micropythonos.settings/assets/settings.py b/internal_filesystem/builtin/apps/com.micropythonos.settings/assets/settings.py index ed53bea2..3205140e 100644 --- a/internal_filesystem/builtin/apps/com.micropythonos.settings/assets/settings.py +++ b/internal_filesystem/builtin/apps/com.micropythonos.settings/assets/settings.py @@ -36,10 +36,13 @@ class SettingsActivity(Activity): ("Turquoise", "40e0d0") ] self.settings = [ + # Novice settings, alphabetically: {"title": "Light/Dark Theme", "key": "theme_light_dark", "value_label": None, "cont": None, "ui": "radiobuttons", "ui_options": [("Light", "light"), ("Dark", "dark")]}, {"title": "Theme Color", "key": "theme_primary_color", "value_label": None, "cont": None, "placeholder": "HTML hex color, like: EC048C", "ui": "dropdown", "ui_options": theme_colors}, - {"title": "Restart to Bootloader", "key": "boot_mode", "value_label": None, "cont": None, "ui": "radiobuttons", "ui_options": [("Normal", "normal"), ("Bootloader", "bootloader")]}, # special that doesn't get saved {"title": "Timezone", "key": "timezone", "value_label": None, "cont": None, "ui": "dropdown", "ui_options": self.get_timezone_tuples(), "changed_callback": lambda : mpos.time.refresh_timezone_preference()}, + # Advanced settings, alphabetically: + {"title": "Auto Start App", "key": "auto_start_app", "value_label": None, "cont": None, "ui": "radiobuttons", "ui_options": [("Launcher", "com.micropythonos.launcher"), ("LightningPiggy", "com.lightningpiggy.displaywallet")]}, + {"title": "Restart to Bootloader", "key": "boot_mode", "value_label": None, "cont": None, "ui": "radiobuttons", "ui_options": [("Normal", "normal"), ("Bootloader", "bootloader")]}, # special that doesn't get saved # This is currently only in the drawer but would make sense to have it here for completeness: #{"title": "Display Brightness", "key": "display_brightness", "value_label": None, "cont": None, "placeholder": "A value from 0 to 100."}, # Maybe also add font size (but ideally then all fonts should scale up/down) diff --git a/internal_filesystem/lib/mpos/apps.py b/internal_filesystem/lib/mpos/apps.py index 4881f356..366d914e 100644 --- a/internal_filesystem/lib/mpos/apps.py +++ b/internal_filesystem/lib/mpos/apps.py @@ -17,6 +17,7 @@ def good_stack_size(): return stacksize # Run the script in the current thread: +# Returns True if successful def execute_script(script_source, is_file, cwd=None, classname=None): import utime # for timing read and compile thread_id = _thread.get_ident() @@ -55,26 +56,32 @@ def execute_script(script_source, is_file, cwd=None, classname=None): #print("Classes:", classes.keys()) #print("Functions:", functions.keys()) #print("Variables:", variables.keys()) - if classname: - main_activity = script_globals.get(classname) - if main_activity: - start_time = utime.ticks_ms() - Activity.startActivity(None, Intent(activity_class=main_activity)) - end_time = utime.ticks_diff(utime.ticks_ms(), start_time) - print(f"execute_script: Activity.startActivity took {end_time}ms") - else: - print("Warning: could not find main_activity") + if not classname: + print("Running without a classname isn't supported right now.") + return False + main_activity = script_globals.get(classname) + if main_activity: + start_time = utime.ticks_ms() + Activity.startActivity(None, Intent(activity_class=main_activity)) + end_time = utime.ticks_diff(utime.ticks_ms(), start_time) + print(f"execute_script: Activity.startActivity took {end_time}ms") + else: + print(f"Warning: could not find app's main_activity {main_activity}") + return False except Exception as e: print(f"Thread {thread_id}: exception during execution:") # Print stack trace with exception type, value, and traceback tb = getattr(e, '__traceback__', None) traceback.print_exception(type(e), e, tb) - print(f"Thread {thread_id}: script {compile_name} finished") + return False + print(f"Thread {thread_id}: script {compile_name} finished, restoring sys.path to {sys.path}") sys.path = path_before + return True except Exception as e: print(f"Thread {thread_id}: error:") tb = getattr(e, '__traceback__', None) traceback.print_exception(type(e), e, tb) + return False """ Unused: # Run the script in a new thread: @@ -104,6 +111,7 @@ def execute_script_new_thread(scriptname, is_file): print("main.py: execute_script_new_thread(): error starting new thread thread: ", e) """ +# Returns True if successful def start_app(fullname): mpos.ui.set_foreground_app(fullname) import utime @@ -119,7 +127,7 @@ def start_app(fullname): print(f"WARNING: start_app can't start {fullname} because it doesn't have a main_launcher_activity") return start_script_fullpath = f"{app.installed_path}/{app.main_launcher_activity.get('entrypoint')}" - execute_script(start_script_fullpath, True, app.installed_path + "/assets/", app.main_launcher_activity.get("classname")) + result = execute_script(start_script_fullpath, True, app.installed_path + "/assets/", app.main_launcher_activity.get("classname")) # Launchers have the bar, other apps don't have it if app.is_valid_launcher(): mpos.ui.topmenu.open_bar() @@ -127,6 +135,8 @@ def start_app(fullname): mpos.ui.topmenu.close_bar() end_time = utime.ticks_diff(utime.ticks_ms(), start_time) print(f"start_app() took {end_time}ms") + return result + # Starts the first launcher that's found def restart_launcher(): @@ -134,9 +144,5 @@ def restart_launcher(): # Stop all apps mpos.ui.remove_and_stop_all_activities() # No need to stop the other launcher first, because it exits after building the screen - for app in PackageManager.get_app_list(): - if app.is_valid_launcher(): - print(f"Found launcher, starting {app.fullname}") - start_app(app.fullname) - break + return start_app(PackageManager.get_launcher().fullname) diff --git a/internal_filesystem/lib/mpos/content/package_manager.py b/internal_filesystem/lib/mpos/content/package_manager.py index 5d52e992..51e93935 100644 --- a/internal_filesystem/lib/mpos/content/package_manager.py +++ b/internal_filesystem/lib/mpos/content/package_manager.py @@ -73,8 +73,17 @@ class PackageManager: @classmethod def get(cls, fullname): + if not cls._app_list: + cls.refresh_apps() return cls._by_fullname.get(fullname) + @classmethod + def get_launcher(cls): + for app in cls.get_app_list(): + if app.is_valid_launcher(): + print(f"Found launcher {app.fullname}") + return app + @classmethod def clear(cls): """Empty the internal caches. Call ``get_app_list()`` afterwards to repopulate.""" @@ -223,4 +232,3 @@ class PackageManager: print(f"Checking if app {app_fullname} is installed...") return PackageManager.is_installed_by_path(f"apps/{app_fullname}") or PackageManager.is_installed_by_path(f"builtin/apps/{app_fullname}") - diff --git a/internal_filesystem/lib/mpos/ui/topmenu.py b/internal_filesystem/lib/mpos/ui/topmenu.py index 0fcb1d53..7b2ec009 100644 --- a/internal_filesystem/lib/mpos/ui/topmenu.py +++ b/internal_filesystem/lib/mpos/ui/topmenu.py @@ -271,10 +271,10 @@ def create_drawer(display=None): launcher_btn.set_size(lv.pct(drawer_button_pct),lv.pct(20)) launcher_btn.align(lv.ALIGN.CENTER,0,0) launcher_label=lv.label(launcher_btn) - launcher_label.set_text(lv.SYMBOL.HOME+" Home") + launcher_label.set_text(lv.SYMBOL.HOME+" Launch") launcher_label.center() def launcher_event(e): - print("Home button pressed!") + print("Launch button pressed!") close_drawer(True) mpos.apps.restart_launcher() launcher_btn.add_event_cb(launcher_event,lv.EVENT.CLICKED,None) diff --git a/internal_filesystem/main.py b/internal_filesystem/main.py index 34bb2efc..e3d4b376 100644 --- a/internal_filesystem/main.py +++ b/internal_filesystem/main.py @@ -12,6 +12,7 @@ import mpos.config import mpos.ui import mpos.ui.topmenu from mpos.ui.display import init_rootscreen +from mpos.content.package_manager import PackageManager prefs = mpos.config.SharedPreferences("com.micropythonos.settings") @@ -71,7 +72,13 @@ try: except Exception as e: print(f"Couldn't start mpos.wifi.WifiService.auto_connect thread because: {e}") -mpos.apps.restart_launcher() +# Start launcher so it's always at bottom of stack +launcher_app = PackageManager.get_launcher() +mpos.apps.start_app(launcher_app.fullname) +# Then start another app if configured +auto_start_app = prefs.get_string("auto_start_app", None) +if auto_start_app and launcher_app.fullname != auto_start_app: + mpos.apps.start_app(auto_start_app) # If we got this far without crashing, then no need to rollback the update: try: diff --git a/scripts/bundle_apps.sh b/scripts/bundle_apps.sh index a5936d66..8c268e39 100755 --- a/scripts/bundle_apps.sh +++ b/scripts/bundle_apps.sh @@ -14,7 +14,10 @@ mkdir -p "$output" #rm "$output"/*.png rm "$outputjson" -blacklist="com.micropythonos.filemanager com.example.bla" +# These apps are for testing, or aren't ready yet: +# com.quasikili.quasidoodle doesn't work on touch screen devices +# com.micropythonos.filemanager doesn't do anything other than let you browse the filesystem, so it's confusing +blacklist="com.micropythonos.filemanager com.quasikili.quasidoodle" echo "[" | tee -a "$outputjson" diff --git a/tests/test_start_app.py b/tests/test_start_app.py new file mode 100644 index 00000000..975639ef --- /dev/null +++ b/tests/test_start_app.py @@ -0,0 +1,37 @@ +import unittest + +import sdl_display +import lcd_bus +import lvgl as lv +import mpos.ui +import task_handler +import mpos.apps +import mpos.ui.topmenu +import mpos.config +from mpos.ui.display import init_rootscreen + +class TestStartApp(unittest.TestCase): + + def __init__(self): + + TFT_HOR_RES=320 + TFT_VER_RES=240 + + bus = lcd_bus.SDLBus(flags=0) + buf1 = bus.allocate_framebuffer(320 * 240 * 2, 0) + display = sdl_display.SDLDisplay(data_bus=bus,display_width=TFT_HOR_RES,display_height=TFT_VER_RES,frame_buffer1=buf1,color_space=lv.COLOR_FORMAT.RGB565) + display.init() + init_rootscreen() + mpos.ui.topmenu.create_notification_bar() + mpos.ui.topmenu.create_drawer(display) + mpos.ui.th = task_handler.TaskHandler(duration=5) # 5ms is recommended for MicroPython+LVGL on desktop (less results in lower framerate) + + + def test_normal(self): + self.assertTrue(mpos.apps.start_app("com.micropythonos.launcher"), "com.micropythonos.launcher should start") + + def test_nonexistent(self): + self.assertFalse(mpos.apps.start_app("com.micropythonos.nonexistent"), "com.micropythonos.nonexistent should not start") + + def test_restart_launcher(self): + self.assertTrue(mpos.apps.restart_launcher(), "restart_launcher() should succeed")