Refactor app structure

This commit is contained in:
Thomas Farstrike
2025-10-30 11:42:42 +01:00
parent 1d471af4b3
commit 4f575273fc
16 changed files with 364 additions and 350 deletions
@@ -7,6 +7,7 @@ import time
import _thread
from mpos.apps import Activity, Intent
from mpos.app import App
import mpos.ui
from mpos.package_manager import PackageManager
@@ -63,7 +64,7 @@ class AppStore(Activity):
applist = json.loads(response.text)
for app in json.loads(response.text):
try:
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"]))
self.apps.append(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
@@ -15,8 +15,9 @@ import lvgl as lv
import mpos.apps
import mpos.ui
from mpos.package_manager import PackageManager
from mpos import Activity
class Launcher(mpos.apps.Activity):
class Launcher(Activity):
def onCreate(self):
print("launcher.py onCreate()")
+11
View File
@@ -0,0 +1,11 @@
# Re-export common classes for convenience
from .app.app import App
from .app.activity import Activity
from .content.intent import Intent
from .navigator import ActivityNavigator
from .package_manager import PackageManager
# Optional: re-export activities
from .app.activities.chooser import ChooserActivity
from .app.activities.view import ViewActivity
from .app.activities.share import ShareActivity
@@ -0,0 +1,10 @@
from .app import App
from .activity import Activity
from .activities.chooser import ChooserActivity
from .activities.view import ViewActivity
from .activities.share import ShareActivity
__all__ = [
"App", "Activity",
"ChooserActivity", "ViewActivity", "ShareActivity"
]
@@ -0,0 +1,5 @@
from .chooser import ChooserActivity
from .view import ViewActivity
from .share import ShareActivity
__all__ = ["ChooserActivity", "ViewActivity", "ShareActivity"]
@@ -0,0 +1,47 @@
from ..activity import Activity
#from ..activity import Activity
#import mpos.app.activity
#import mpos.app.activity
#from mpos.app import Activity
import mpos.package_manager
class ChooserActivity(Activity):
def __init__(self):
super().__init__()
def onCreate(self):
screen = lv.obj()
# Get handlers from intent extras
original_intent = self.getIntent().extras.get("original_intent")
handlers = self.getIntent().extras.gepackage_managert("handlers", [])
label = lv.label(screen)
label.set_text("Choose an app")
label.set_pos(10, 10)
for i, handler_name in enumerate(handlers):
btn = lv.btn(screen)
btn.set_user_data(f"handler_{i}")
btn_label = lv.label(btn)
btn_label.set_text(handler_name)
btn.set_pos(10, 50 * (i + 1) + 10)
btn.add_event_cb(lambda e, h=handler_name, oi=original_intent: self._select_handler(h, oi), lv.EVENT.CLICKED)
self.setContentView(screen)
def _select_handler(self, handler_name, original_intent):
for handler in mpos.package_manager.PackageManager.APP_REGISTRY.get(original_intent.action, []):
if handler.__name__ == handler_name:
original_intent.activity_class = handler
navigator.startActivity(original_intent)
break
navigator.finish() # Close chooser
def onStop(self, screen):
if self.getIntent() and self.getIntent().getStringExtra("destination") == "ChooserActivity":
print("Stopped for Chooser")
else:
print("Stopped for other screen")
@@ -0,0 +1,35 @@
from ..activity import Activity
class ShareActivity(Activity):
def __init__(self):
super().__init__()
def onCreate(self):
screen = lv.obj()
# Get text from intent (prefer extras.text, fallback to data)
text = self.getIntent().extras.get("text", self.getIntent().data or "No text")
label = lv.label(screen)
label.set_user_data("share_label")
label.set_text(f"Share: {text}")
label.set_pos(10, 10)
btn = lv.btn(screen)
btn.set_user_data("share_btn")
btn_label = lv.label(btn)
btn_label.set_text("Share")
btn.set_pos(10, 50)
btn.add_event_cb(lambda e: self._share_content(text), lv.EVENT.CLICKED)
self.setContentView(screen)
def _share_content(self, text):
# Dispatch to another app (e.g., MessagingActivity) or simulate sharing
print(f"Sharing: {text}") # Placeholder for actual sharing
# Example: Launch another share handler
navigator.startActivity(Intent(action="share", data=text))
navigator.finish() # Close ShareActivity
def onStop(self, screen):
if self.getIntent() and self.getIntent().getStringExtra("destination") == "ShareActivity":
print("Stopped for Share")
else:
print("Stopped for other screen")
@@ -0,0 +1,27 @@
from ..activity import Activity
class ViewActivity(Activity):
def __init__(self):
super().__init__()
def onCreate(self):
screen = lv.obj()
# Get content from intent (prefer extras.url, fallback to data)
content = self.getIntent().extras.get("url", self.getIntent().data or "No content")
label = lv.label(screen)
label.set_user_data("content_label")
label.set_text(f"Viewing: {content}")
label.center()
self.setContentView(screen)
def onStart(self, screen):
content = self.getIntent().extras.get("url", self.getIntent().data or "No content")
for i in range(screen.get_child_cnt()):
if screen.get_child(i).get_user_data() == "content_label":
screen.get_child(i).set_text(f"Viewing: {content}")
def onStop(self, screen):
if self.getIntent() and self.getIntent().getStringExtra("destination") == "ViewActivity":
print("Stopped for View")
else:
print("Stopped for other screen")
@@ -0,0 +1,57 @@
from mpos.navigator import ActivityNavigator
import mpos.ui
class Activity:
def __init__(self):
self.intent = None # Store the intent that launched this activity
self.result = None
self._result_callback = None
def onCreate(self):
pass
def onStart(self, screen):
pass
def onResume(self, screen):
pass
def onPause(self, screen):
pass
def onStop(self, screen):
pass
def onDestroy(self, screen):
pass
def setContentView(self, screen):
mpos.ui.setContentView(self, screen)
def startActivity(self, intent):
ActivityNavigator.startActivity(intent)
def startActivityForResult(self, intent, result_callback):
ActivityNavigator.startActivityForResult(intent, result_callback)
def initError(self, e):
print(f"WARNING: You might have inherited from Activity with a custom __init__() without calling super().__init__(). Got AttributeError: {e}")
def getIntent(self):
try:
return self.intent
except AttributeError as e:
self.initError(e)
def setResult(self, result_code, data=None):
"""Set the result to be returned when the activity finishes."""
try:
self.result = {"result_code": result_code, "data": data or {}}
except AttributeError as e:
self.initError(e)
def finish(self):
mpos.ui.back_screen()
try:
if self._result_callback and self.result:
self._result_callback(self.result)
self._result_callback = None # Clean up
except AttributeError as e:
self.initError(e)
+73
View File
@@ -0,0 +1,73 @@
import ujson
from ..content.intent import Intent # optional, if App uses Intent
class App:
def __init__(
self,
name="Unknown",
publisher="Unknown",
short_description="",
long_description="",
icon_url="",
download_url="",
fullname="Unknown",
version="0.0.0",
category="",
activities=None,
installed_path=None,
):
self.name = name
self.publisher = publisher
self.short_description = short_description
self.long_description = long_description
self.icon_url = icon_url
self.download_url = download_url
self.fullname = fullname
self.version = version
self.category = category
self.activities = activities or []
self.installed_path = installed_path
self.image = None
self.image_dsc = None
self.main_launcher_activity = self._find_main_launcher_activity()
def __str__(self):
return f"App({self.name}, v{self.version}, {self.category})"
def _find_main_launcher_activity(self):
for act in self.activities:
if not act.get("entrypoint") or not act.get("classname"):
continue
for f in act.get("intent_filters", []):
if f.get("action") == "main" and f.get("category") == "launcher":
return act
return None
def is_valid_launcher(self):
return self.category == "launcher" and self.main_launcher_activity
@classmethod
def from_manifest(cls, appdir):
manifest_path = f"{appdir}/META-INF/MANIFEST.JSON"
default = cls(installed_path=appdir)
try:
with open(manifest_path, "r") as f:
data = ujson.load(f)
except OSError:
return default
return cls(
name=data.get("name", default.name),
publisher=data.get("publisher", default.publisher),
short_description=data.get("short_description", default.short_description),
long_description=data.get("long_description", default.long_description),
icon_url=data.get("icon_url", default.icon_url),
download_url=data.get("download_url", default.download_url),
fullname=data.get("fullname", default.fullname),
version=data.get("version", default.version),
category=data.get("category", default.category),
activities=data.get("activities", default.activities),
installed_path=appdir,
)
+1 -339
View File
@@ -10,6 +10,7 @@ import traceback
import mpos.info
import mpos.ui
from mpos import Activity, Intent
from mpos.package_manager import PackageManager
def good_stack_size():
@@ -142,343 +143,4 @@ def restart_launcher():
start_app(app.fullname)
break
class App:
# ------------------------------------------------------------------ #
# Regular constructor use when you already have the data
# ------------------------------------------------------------------ #
def __init__(
self,
name="Unknown",
publisher="Unknown",
short_description="",
long_description="",
icon_url="",
download_url="",
fullname="Unknown",
version="0.0.0",
category="",
activities=None,
installed_path=None,
):
self.name = name
self.publisher = publisher
self.short_description = short_description
self.long_description = long_description
self.icon_url = icon_url
self.download_url = download_url
self.fullname = fullname
self.version = version
self.category = category
self.activities = activities if activities is not None else []
self.installed_path = installed_path
# Cached image fields (kept for compatibility)
self.image = None
self.image_dsc = None
# Find the main launcher activity once, at construction time
self.main_launcher_activity = self._find_main_launcher_activity()
# ------------------------------------------------------------------ #
# Human-readable representation
# ------------------------------------------------------------------ #
def __str__(self):
return (
f"App(name='{self.name}', "
f"publisher='{self.publisher}', "
f"short_description='{self.short_description}', "
f"version='{self.version}', "
f"category='{self.category}', "
f"activities={len(self.activities)} items, "
f"installed_path={self.installed_path})"
)
# ------------------------------------------------------------------ #
# Private helper locate the MAIN/LAUNCHER activity
# ------------------------------------------------------------------ #
def _find_main_launcher_activity(self):
for activity in self.activities:
if not activity.get("entrypoint") or not activity.get("classname"):
print("Warning: activity missing entrypoint or classname skipping")
continue
for intent_filter in activity.get("intent_filters", []):
if (
intent_filter.get("action") == "main"
and intent_filter.get("category") == "launcher"
):
print("Found main launcher activity!")
return activity
return None
# ------------------------------------------------------------------ #
# Convenience check for launcher-type apps
# ------------------------------------------------------------------ #
def is_valid_launcher(self):
return self.category == "launcher" and self.main_launcher_activity is not None
# ------------------------------------------------------------------ #
# Class-method constructor that builds an App from a manifest file
# ------------------------------------------------------------------ #
@classmethod
def from_manifest(cls, appdir):
"""
Parse <appdir>/META-INF/MANIFEST.JSON and return a fully-populated
App instance. If the file cannot be read, a default App with
placeholder values is returned.
"""
print(f"parse_manifest({appdir})")
manifest_path = f"{appdir}/META-INF/MANIFEST.JSON"
# Minimal default instance guarantees every field has a fallback
default = cls(installed_path=appdir)
try:
with open(manifest_path, "r") as f:
data = ujson.load(f)
except OSError as exc:
print(f"parse_manifest: error loading {manifest_path} {exc}")
return default
# Merge manifest data with defaults
return cls(
name=data.get("name", default.name),
publisher=data.get("publisher", default.publisher),
short_description=data.get("short_description", default.short_description),
long_description=data.get("long_description", default.long_description),
icon_url=data.get("icon_url", default.icon_url),
download_url=data.get("download_url", default.download_url),
fullname=data.get("fullname", default.fullname),
version=data.get("version", default.version),
category=data.get("category", default.category),
activities=data.get("activities", default.activities),
installed_path=appdir,
)
class Activity:
def __init__(self):
self.intent = None # Store the intent that launched this activity
self.result = None
self._result_callback = None
def onCreate(self):
pass
def onStart(self, screen):
pass
def onResume(self, screen):
pass
def onPause(self, screen):
pass
def onStop(self, screen):
pass
def onDestroy(self, screen):
pass
def setContentView(self, screen):
mpos.ui.setContentView(self, screen)
def startActivity(self, intent):
ActivityNavigator.startActivity(intent)
def startActivityForResult(self, intent, result_callback):
ActivityNavigator.startActivityForResult(intent, result_callback)
def initError(self, e):
print(f"WARNING: You might have inherited from Activity with a custom __init__() without calling super().__init__(). Got AttributeError: {e}")
def getIntent(self):
try:
return self.intent
except AttributeError as e:
self.initError(e)
def setResult(self, result_code, data=None):
"""Set the result to be returned when the activity finishes."""
try:
self.result = {"result_code": result_code, "data": data or {}}
except AttributeError as e:
self.initError(e)
def finish(self):
mpos.ui.back_screen()
try:
if self._result_callback and self.result:
self._result_callback(self.result)
self._result_callback = None # Clean up
except AttributeError as e:
self.initError(e)
class Intent:
def __init__(self, activity_class=None, action=None, data=None, extras=None):
self.activity_class = activity_class # Explicit target (e.g., SettingsActivity)
self.action = action # Action string (e.g., "view", "share")
self.data = data # Single data item (e.g., URL)
self.extras = extras or {} # Dictionary for additional data
self.flags = {} # Simplified flags: {"clear_top": bool, "no_history": bool, "no_animation": bool}
def addFlag(self, flag, value=True):
self.flags[flag] = value
return self
def putExtra(self, key, value):
self.extras[key] = value
return self
class ActivityNavigator:
@staticmethod
def startActivity(intent):
if not isinstance(intent, Intent):
raise ValueError("Must provide an Intent")
if intent.action: # Implicit intent: resolve handlers
handlers = APP_REGISTRY.get(intent.action, [])
if len(handlers) == 1:
intent.activity_class = handlers[0]
ActivityNavigator._launch_activity(intent)
elif handlers:
ActivityNavigator._show_chooser(intent, handlers)
else:
raise ValueError(f"No handlers for action: {intent.action}")
else:
ActivityNavigator._launch_activity(intent)
@staticmethod
def startActivityForResult(intent, result_callback):
"""Launch an activity and pass a callback for the result."""
if not isinstance(intent, Intent):
raise ValueError("Must provide an Intent")
if intent.action: # Implicit intent: resolve handlers
handlers = APP_REGISTRY.get(intent.action, [])
if len(handlers) == 1:
intent.activity_class = handlers[0]
return ActivityNavigator._launch_activity(intent, result_callback)
elif handlers:
ActivityNavigator._show_chooser(intent, handlers)
return None # Chooser handles result forwarding
else:
raise ValueError(f"No handlers for action: {intent.action}")
else:
return ActivityNavigator._launch_activity(intent, result_callback)
@staticmethod
def _launch_activity(intent, result_callback=None):
"""Launch an activity and set up result callback."""
activity = intent.activity_class()
activity.intent = intent
activity._result_callback = result_callback # Pass callback to activity
start_time = utime.ticks_ms()
mpos.ui.save_and_clear_current_focusgroup()
activity.onCreate()
end_time = utime.ticks_diff(utime.ticks_ms(), start_time)
print(f"apps.py _launch_activity: activity.onCreate took {end_time}ms")
return activity
@staticmethod
def _show_chooser(intent, handlers):
chooser_intent = Intent(ChooserActivity, extras={"original_intent": intent, "handlers": [h.__name__ for h in handlers]})
ActivityNavigator._launch_activity(chooser_intent)
class ChooserActivity(Activity):
def __init__(self):
super().__init__()
def onCreate(self):
screen = lv.obj()
# Get handlers from intent extras
original_intent = self.getIntent().extras.get("original_intent")
handlers = self.getIntent().extras.get("handlers", [])
label = lv.label(screen)
label.set_text("Choose an app")
label.set_pos(10, 10)
for i, handler_name in enumerate(handlers):
btn = lv.btn(screen)
btn.set_user_data(f"handler_{i}")
btn_label = lv.label(btn)
btn_label.set_text(handler_name)
btn.set_pos(10, 50 * (i + 1) + 10)
btn.add_event_cb(lambda e, h=handler_name, oi=original_intent: self._select_handler(h, oi), lv.EVENT.CLICKED)
self.setContentView(screen)
def _select_handler(self, handler_name, original_intent):
for handler in APP_REGISTRY.get(original_intent.action, []):
if handler.__name__ == handler_name:
original_intent.activity_class = handler
navigator.startActivity(original_intent)
break
navigator.finish() # Close chooser
def onStop(self, screen):
if self.getIntent() and self.getIntent().getStringExtra("destination") == "ChooserActivity":
print("Stopped for Chooser")
else:
print("Stopped for other screen")
class ViewActivity(Activity):
def __init__(self):
super().__init__()
def onCreate(self):
screen = lv.obj()
# Get content from intent (prefer extras.url, fallback to data)
content = self.getIntent().extras.get("url", self.getIntent().data or "No content")
label = lv.label(screen)
label.set_user_data("content_label")
label.set_text(f"Viewing: {content}")
label.center()
self.setContentView(screen)
def onStart(self, screen):
content = self.getIntent().extras.get("url", self.getIntent().data or "No content")
for i in range(screen.get_child_cnt()):
if screen.get_child(i).get_user_data() == "content_label":
screen.get_child(i).set_text(f"Viewing: {content}")
def onStop(self, screen):
if self.getIntent() and self.getIntent().getStringExtra("destination") == "ViewActivity":
print("Stopped for View")
else:
print("Stopped for other screen")
class ShareActivity(Activity):
def __init__(self):
super().__init__()
def onCreate(self):
screen = lv.obj()
# Get text from intent (prefer extras.text, fallback to data)
text = self.getIntent().extras.get("text", self.getIntent().data or "No text")
label = lv.label(screen)
label.set_user_data("share_label")
label.set_text(f"Share: {text}")
label.set_pos(10, 10)
btn = lv.btn(screen)
btn.set_user_data("share_btn")
btn_label = lv.label(btn)
btn_label.set_text("Share")
btn.set_pos(10, 50)
btn.add_event_cb(lambda e: self._share_content(text), lv.EVENT.CLICKED)
self.setContentView(screen)
def _share_content(self, text):
# Dispatch to another app (e.g., MessagingActivity) or simulate sharing
print(f"Sharing: {text}") # Placeholder for actual sharing
# Example: Launch another share handler
navigator.startActivity(Intent(action="share", data=text))
navigator.finish() # Close ShareActivity
def onStop(self, screen):
if self.getIntent() and self.getIntent().getStringExtra("destination") == "ShareActivity":
print("Stopped for Share")
else:
print("Stopped for other screen")
APP_REGISTRY = { # This should be handled by a new class PackageManager:
"view": [ViewActivity], # Hypothetical activities
"share": [ShareActivity]
}
@@ -0,0 +1,2 @@
from .intent import Intent
__all__ = ["Intent"]
@@ -0,0 +1,16 @@
class Intent:
def __init__(self, activity_class=None, action=None, data=None, extras=None):
self.activity_class = activity_class # Explicit target (e.g., SettingsActivity)
self.action = action # Action string (e.g., "view", "share")
self.data = data # Single data item (e.g., URL)
self.extras = extras or {} # Dictionary for additional data
self.flags = {} # Simplified flags: {"clear_top": bool, "no_history": bool, "no_animation": bool}
def addFlag(self, flag, value=True):
self.flags[flag] = value
return self
def putExtra(self, key, value):
self.extras[key] = value
return self
+61
View File
@@ -0,0 +1,61 @@
import utime
from .content.intent import Intent
#from .app.activity import Activity
#import mpos.package_manager
import mpos.ui
class ActivityNavigator:
@staticmethod
def startActivity(intent):
if not isinstance(intent, Intent):
raise ValueError("Must provide an Intent")
if intent.action: # Implicit intent: resolve handlers
#handlers = mpos.package_manager.PackageManager.APP_REGISTRY.get(intent.action, [])
if len(handlers) == 1:
intent.activity_class = handlers[0]
ActivityNavigator._launch_activity(intent)
elif handlers:
ActivityNavigator._show_chooser(intent, handlers)
else:
raise ValueError(f"No handlers for action: {intent.action}")
else:
ActivityNavigator._launch_activity(intent)
@staticmethod
def startActivityForResult(intent, result_callback):
"""Launch an activity and pass a callback for the result."""
if not isinstance(intent, Intent):
raise ValueError("Must provide an Intent")
if intent.action: # Implicit intent: resolve handlers
#handlers = PackageManager.APP_REGISTRY.get(intent.action, [])
if len(handlers) == 1:
intent.activity_class = handlers[0]
return ActivityNavigator._launch_activity(intent, result_callback)
elif handlers:
ActivityNavigator._show_chooser(intent, handlers)
return None # Chooser handles result forwarding
else:
raise ValueError(f"No handlers for action: {intent.action}")
else:
return ActivityNavigator._launch_activity(intent, result_callback)
@staticmethod
def _launch_activity(intent, result_callback=None):
"""Launch an activity and set up result callback."""
activity = intent.activity_class()
activity.intent = intent
activity._result_callback = result_callback # Pass callback to activity
start_time = utime.ticks_ms()
mpos.ui.save_and_clear_current_focusgroup()
activity.onCreate()
end_time = utime.ticks_diff(utime.ticks_ms(), start_time)
print(f"apps.py _launch_activity: activity.onCreate took {end_time}ms")
return activity
@staticmethod
def _show_chooser(intent, handlers):
chooser_intent = Intent(ChooserActivity, extras={"original_intent": intent, "handlers": [h.__name__ for h in handlers]})
ActivityNavigator._launch_activity(chooser_intent)
@@ -1,5 +1,8 @@
import os
import mpos.apps
from mpos.app.app import App
from mpos.app.activities.view import ViewActivity
from mpos.app.activities.share import ShareActivity
try:
import zipfile
@@ -28,11 +31,13 @@ Question: does it make sense to cache the database?
'''
# PackageManager.py (MicroPython)
import os
class PackageManager:
APP_REGISTRY = {
"view": [ViewActivity],
"share": [ShareActivity]
}
"""Registry of all discovered apps.
* PackageManager.get_app_list() -> list of App objects (sorted by name)
@@ -121,7 +126,7 @@ class PackageManager:
# ---- parse the manifest ---------------------------------
try:
app = mpos.apps.App.from_manifest(full_path)
app = App.from_manifest(full_path)
except Exception as e:
print("PackageManager: parsing {} failed: {}".format(full_path, e))
continue
@@ -219,3 +224,4 @@ 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}")
+3 -3
View File
@@ -1,5 +1,5 @@
import lvgl as lv
import mpos.apps
#import mpos.apps
import mpos.time
import mpos.wifi
from mpos.ui.anim import WidgetAnimator
@@ -44,8 +44,8 @@ def set_foreground_app(appname):
foreground_app_name = appname
print(f"foreground app is: {foreground_app_name}")
def show_launcher():
mpos.apps.restart_launcher()
#def show_launcher():
# mpos.apps.restart_launcher()
def init_rootscreen():
global horizontal_resolution, vertical_resolution