From 5691058a1b5ae333f340994265dd872875d823be Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Wed, 29 Oct 2025 22:58:21 +0100 Subject: [PATCH] Add PackageManager to reduce duplication --- .../assets/appstore.py | 15 ++-- .../assets/launcher.py | 40 ++--------- internal_filesystem/lib/mpos/apps.py | 28 +++++--- .../lib/mpos/package_manager.py | 69 +++++++++++++++++++ 4 files changed, 105 insertions(+), 47 deletions(-) create mode 100644 internal_filesystem/lib/mpos/package_manager.py diff --git a/internal_filesystem/builtin/apps/com.micropythonos.appstore/assets/appstore.py b/internal_filesystem/builtin/apps/com.micropythonos.appstore/assets/appstore.py index 8acf0f95..cecb6bf1 100644 --- a/internal_filesystem/builtin/apps/com.micropythonos.appstore/assets/appstore.py +++ b/internal_filesystem/builtin/apps/com.micropythonos.appstore/assets/appstore.py @@ -66,7 +66,7 @@ class AppStore(Activity): applist = json.loads(response.text) for app in json.loads(response.text): try: - self.apps.append(mpos.apps.App(**app)) + self.apps.append(mpos.apps.App(app["name"], app["publisher"], app["short_description"], app["long_description"], app["icon_url"], app["download_url"], app["fullname"], app["version"], app["category"], app["activities"])) except Exception as e: print(f"Warning: could not add app from {json_url} to apps list: {e}") # Remove duplicates based on app.name @@ -137,14 +137,19 @@ class AppStore(Activity): if app.image_dsc: print(f"Skipping icon download for {app.name} because already downloaded.") continue + if not self.keep_running: + print(f"App is stopping, aborting icon downloads.") + break print(f"Downloading icon for {app.name}") image_dsc = self.download_icon(app.icon_url) app.image_dsc = image_dsc # save it for the app detail page if not self.keep_running: + print(f"App is stopping, aborting all icon downloads.") break - lv.async_call(lambda l: app.image.set_src(image_dsc), None) + else: + lv.async_call(lambda l: app.image.set_src(image_dsc), None) time.sleep_ms(200) # not waiting here will result in some async_calls() not being executed - print("Finished downloading icons...") + print("Finished downloading icons.") def show_app_detail(self, app): intent = Intent(activity_class=AppDetail) @@ -439,10 +444,10 @@ class AppDetail(Activity): installed_app=None if AppDetail.is_installed_by_path(appdir): print(f"{appdir} found, getting version...") - installed_app = mpos.apps.parse_manifest(f"{appdir}/META-INF/MANIFEST.JSON") + installed_app = mpos.apps.parse_manifest(appdir) elif AppDetail.is_installed_by_path(builtinappdir): print(f"{builtinappdir} found, getting version...") - installed_app = mpos.apps.parse_manifest(f"{builtinappdir}/META-INF/MANIFEST.JSON") + installed_app = mpos.apps.parse_manifest(builtinappdir) if not installed_app or installed_app.version == "0.0.0": # special case, if the installed app doesn't have a version number then there's no update return False return AppDetail.compare_versions(new_version, installed_app.version) diff --git a/internal_filesystem/builtin/apps/com.micropythonos.launcher/assets/launcher.py b/internal_filesystem/builtin/apps/com.micropythonos.launcher/assets/launcher.py index b8a449e3..20cf2a26 100644 --- a/internal_filesystem/builtin/apps/com.micropythonos.launcher/assets/launcher.py +++ b/internal_filesystem/builtin/apps/com.micropythonos.launcher/assets/launcher.py @@ -14,6 +14,7 @@ import lvgl as lv import mpos.apps import mpos.ui +from mpos.package_manager import PackageManager class Launcher(mpos.apps.Activity): @@ -30,57 +31,28 @@ class Launcher(mpos.apps.Activity): self.setContentView(main_screen) def onResume(self, screen): - app_list = [] - seen_base_names = set() - # Check and collect subdirectories from existing directories - apps_dir = "apps" - apps_dir_builtin = "builtin/apps" # Grid parameters icon_size = 64 # Adjust based on your display label_height = 24 iconcont_width = icon_size + label_height iconcont_height = icon_size + label_height - - # Check and collect unique subdirectories - for dir_path in [apps_dir, apps_dir_builtin]: - try: - if uos.stat(dir_path)[0] & 0x4000: # Verify directory exists - try: - for d in uos.listdir(dir_path): - full_path = f"{dir_path}/{d}" - #print(f"full_path: {full_path}") - try: - if uos.stat(full_path)[0] & 0x4000: # Check if it's a directory - base_name = d - if base_name not in seen_base_names: # Avoid duplicates - seen_base_names.add(base_name) - app = mpos.apps.parse_manifest(f"{full_path}/META-INF/MANIFEST.JSON") - if app.category != "launcher": # Skip launchers - main_launcher = mpos.apps.find_main_launcher_activity(app) - if main_launcher: - app_list.append((app.name, full_path)) - except Exception as e: - print(f"launcher.py stat of {full_path} got exception: {e}") - except Exception as e: - print(f"launcher.py listdir of {dir_path} got exception: {e}") - except Exception as e: - print(f"launcher.py stat of {dir_path} got exception: {e}") + app_list = PackageManager.app_list import time start = time.ticks_ms() screen.clean() + # Get the group for focusable objects focusgroup = lv.group_get_default() if not focusgroup: print("WARNING: could not get default focusgroup") - # Sort apps alphabetically by app.name - app_list.sort(key=lambda x: x[0].lower()) # Case-insensitive sorting - # Create UI for each app - for app_name, app_dir_fullpath in app_list: + for app in app_list: + app_name = app.name + app_dir_fullpath = app.installed_path print(f"Adding app {app_name} from {app_dir_fullpath}") # Create container for each app (icon + label) app_cont = lv.obj(screen) diff --git a/internal_filesystem/lib/mpos/apps.py b/internal_filesystem/lib/mpos/apps.py index 554ab77c..37f6b97c 100644 --- a/internal_filesystem/lib/mpos/apps.py +++ b/internal_filesystem/lib/mpos/apps.py @@ -10,6 +10,7 @@ import traceback import mpos.info import mpos.ui +from mpos.package_manager import PackageManager def good_stack_size(): stacksize = 24*1024 @@ -119,8 +120,7 @@ def start_app(app_dir, is_launcher=False): import utime start_time = utime.ticks_ms() mpos.ui.set_foreground_app(app_dir) # would be better to store only the app name... - manifest_path = f"{app_dir}/META-INF/MANIFEST.JSON" - app = mpos.apps.parse_manifest(manifest_path) + app = mpos.apps.parse_manifest(app_dir) print(f"start_app parsed manifest and got: {str(app)}") main_launcher_activity = find_main_launcher_activity(app) if not main_launcher_activity: @@ -136,10 +136,16 @@ def start_app(app_dir, is_launcher=False): end_time = utime.ticks_diff(utime.ticks_ms(), start_time) print(f"start_app() took {end_time}ms") +# Starts the first launcher that's found def restart_launcher(): + print("restart_launcher") mpos.ui.empty_screen_stack() # No need to stop the other launcher first, because it exits after building the screen - start_app_by_name("com.micropythonos.launcher", True) # Would be better to query the PackageManager for Activities that are launchers + for app in mpos.package_manager.PackageManager.get_app_list(): + #print(f"checking {app}") + if app.category == "launcher" and find_main_launcher_activity(app): + print(f"Found launcher, starting {app.fullname}") + start_app_by_name(app.fullname, True) def find_main_launcher_activity(app): result = None @@ -163,7 +169,7 @@ def is_launcher(app_name): class App: - def __init__(self, name, publisher, short_description, long_description, icon_url, download_url, fullname, version, category, activities): + def __init__(self, name, publisher, short_description, long_description, icon_url, download_url, fullname, version, category, activities, installed_path=None): self.name = name self.publisher = publisher self.short_description = short_description @@ -176,6 +182,7 @@ class App: self.image = None self.image_dsc = None self.activities = activities + self.installed_path = installed_path def __str__(self): return (f"App(name='{self.name}', " @@ -183,9 +190,12 @@ class App: f"short_description='{self.short_description}', " f"version='{self.version}', " f"category='{self.category}', " - f"activities={self.activities})") + f"activities='{self.activities}', " + f"installed_path={self.installed_path})") -def parse_manifest(manifest_path): +def parse_manifest(appdir): + print(f"parse_manifest({appdir})") + manifest_path = f"{appdir}/META-INF/MANIFEST.JSON" # Default values for App object default_app = App( name="Unknown", @@ -197,7 +207,8 @@ def parse_manifest(manifest_path): fullname="Unknown", version="0.0.0", category="", - activities=[] + activities=[], + installed_path=appdir ) try: with open(manifest_path, 'r') as f: @@ -214,7 +225,8 @@ def parse_manifest(manifest_path): fullname=app_info.get("fullname", default_app.fullname), version=app_info.get("version", default_app.version), category=app_info.get("category", default_app.category), - activities=app_info.get("activities", default_app.activities) + activities=app_info.get("activities", default_app.activities), + installed_path=appdir ) except OSError: print(f"parse_manifest: error loading manifest_path: {manifest_path}") diff --git a/internal_filesystem/lib/mpos/package_manager.py b/internal_filesystem/lib/mpos/package_manager.py new file mode 100644 index 00000000..2c87f4f6 --- /dev/null +++ b/internal_filesystem/lib/mpos/package_manager.py @@ -0,0 +1,69 @@ +import uos +import mpos.apps + +''' +Initialized at boot. +Typical users: appstore, launcher + +Allows users to: +- list installed apps (including all app data like icon, version, etc) +- install app from .zip file +- uninstall app +- check if an app is installed + which version + +Why this exists: +- the launcher was listing installed apps, reading them, loading the icons, starting apps +- the appstore was also listing installed apps, reading them, (down)loading the icons, starting apps +- other apps might also want to do so +Previously, some functionality was deduplicated into apps.py +But the main issue was that the list of apps was built by both etc. + +Question: does it make sense to cache the database? +=> No, just read/load them at startup and keep the list in memory, and load the icons at runtime. + +''' + + +class PackageManager(): + + app_list = [] # list of App objects, sorted alphabetically by app.name, unique by full_name (com.example.appname) + + @classmethod + def get_app_list(cls): + if len(cls.app_list) == 0: + cls.find_apps() + return cls.app_list + + @classmethod + def find_apps(cls): + print("\n\n\nPackageManager finding apps...") + seen_fullnames = set() + # Check and collect subdirectories from existing directories + apps_dir = "apps" + apps_dir_builtin = "builtin/apps" + + # Check and collect unique subdirectories + for dir_path in [apps_dir, apps_dir_builtin]: + try: + if uos.stat(dir_path)[0] & 0x4000: # Verify directory exists + try: + for d in uos.listdir(dir_path): + full_path = f"{dir_path}/{d}" + print(f"full_path: {full_path}") + try: + if uos.stat(full_path)[0] & 0x4000: # Check if it's a directory + fullname = d + if fullname not in seen_fullnames: # Avoid duplicates + seen_fullnames.add(fullname) + app = mpos.apps.parse_manifest(full_path) + cls.app_list.append(app) + print(f"added app {app}") + except Exception as e: + print(f"PackageManager: stat of {full_path} got exception: {e}") + except Exception as e: + print(f"PackageManager: listdir of {dir_path} got exception: {e}") + except Exception as e: + print(f"PackageManager: stat of {dir_path} got exception: {e}") + + # Sort apps alphabetically by app.name + cls.app_list.sort(key=lambda x: x.name.lower()) # Case-insensitive sorting by name