Add PackageManager to reduce duplication

This commit is contained in:
Thomas Farstrike
2025-10-29 22:58:21 +01:00
parent 3e10479c82
commit 5691058a1b
4 changed files with 105 additions and 47 deletions
@@ -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)
@@ -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)
+20 -8
View File
@@ -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}")
@@ -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