Add support for activities and intent filters to MANIFEST.JSON

This commit is contained in:
Thomas Farstrike
2025-06-03 13:24:05 +02:00
parent 8a0eac09b8
commit d0e51305a1
13 changed files with 135 additions and 64 deletions
@@ -1,13 +0,0 @@
{
"name": "Camera",
"publisher": "ACME Inc",
"short_description": "Simple test of the camera",
"long_description": "A simple test of the camera makes it possible to validate the hardware.",
"icon_url": "http://demo.lnpiggy.com:2121/apps/com.example.camtest_0.0.2.mpk_icon_64x64.png",
"download_url": "http://demo.lnpiggy.com:2121/apps/com.example.camtest_0.0.2.mpk",
"fullname": "com.example.camtest",
"version": "0.0.2",
"entrypoint": "assets/camtest.py",
"category": "camera"
}
@@ -3,10 +3,21 @@
"publisher": "LightningPiggy Foundation",
"short_description": "Display wallet that shows balance, transactions, receive QR code etc.",
"long_description": "See https://www.LightningPiggy.com",
"icon_url": "http://demo.lnpiggy.com:2121/apps/com.lightningpiggy.displaywallet_0.0.1.mpk_icon_64x64.png",
"download_url": "http://demo.lnpiggy.com:2121/apps/com.lightningpiggy.displaywallet_0.0.1.mpk",
"icon_url": "https://apps.micropythonos.com/icons/com.lightningpiggy.displaywallet_0.0.1.mpk_icon_64x64.png",
"download_url": "https://apps.micropythonos.com/mpks/com.lightningpiggy.displaywallet_0.0.1.mpk",
"fullname": "com.lightningpiggy.displaywallet",
"version": "0.0.1",
"entrypoint": "assets/displaywallet.py",
"category": "finance"
"category": "finance",
"activities": [
{
"entrypoint": "assets/displaywallet.py",
"classname": "MainActivity",
"intent_filters": [
{
"action": "main",
"category": "launcher"
}
]
}
]
}
@@ -21,9 +21,10 @@ status_label_text = "No camera found."
status_label_text_searching = "Searching QR codes...\n\nHold still and make them big!\n10cm for simple QR codes,\n20cm for complex."
status_label_text_found = "Decoding QR..."
class CameraActivity(Activity):
class Camera(Activity):
def __init__(self):
super().__init__()
self.cam = None
self.current_cam_buffer = None # Variable to hold the current memoryview to prevent garbage collection
self.image_dsc = None
@@ -1,9 +1,8 @@
from mpos.apps import Activity, Intent
import mpos.config
import mpos.ui
from wallet import LNBitsWallet, NWCWallet
from captureqr import CameraActivity
from captureqr import Camera
class MainActivity(Activity):
@@ -328,7 +327,7 @@ class SettingActivity(Activity):
def cambutton_cb(self, event):
print("cambutton clicked!")
self.startActivity(Intent(activity_class=CameraActivity).putExtra("scanqr_callback", self.gotqr_callback))
self.startActivity(Intent(activity_class=Camera).putExtra("scanqr_callback", self.gotqr_callback))
def save_setting(self, setting):
if setting["key"] == "wallet_type" and self.radio_container:
@@ -344,9 +343,7 @@ class SettingActivity(Activity):
setting["value_label"].set_text(new_value if new_value else "Not set")
self.finish()
class FullscreenQR(Activity):
def onCreate(self):
receive_qr_data = self.getIntent().extras.get("receive_qr_data")
qr_screen = lv.obj()
@@ -0,0 +1,28 @@
{
"name": "Camera",
"publisher": "MicroPythonOS",
"short_description": "Camera with QR decoding",
"long_description": "Camera for both internal camera's and webcams, that includes QR decoding.",
"icon_url": "https://apps.micropythonos.com/com.micropython.camera_0.0.3.mpk_icon_64x64.png",
"download_url": "https://apps.micropythonos.com/com.micropython.camera_0.0.3.mpk",
"fullname": "com.micropython.camera",
"version": "0.0.3",
"category": "camera",
"activities": [
{
"entrypoint": "assets/camera.py",
"classname": "Camera",
"intent_filters": [
{
"action": "main",
"category": "launcher"
},
{
"action": "scan_qr_code",
"category": "default"
}
]
}
]
}
@@ -1,13 +0,0 @@
{
"name": "Launcher",
"publisher": "ACME Inc",
"short_description": "Simple launcher to start apps.",
"long_description": "",
"icon_url": "http://demo.lnpiggy.com:2121/apps/com.example.launcher_0.0.2.mpk_icon_64x64.png",
"download_url": "http://demo.lnpiggy.com:2121/apps/com.example.launcher_0.0.2.mpk",
"fullname": "com.example.launcher",
"version": "0.0.2",
"entrypoint": "assets/launcher.py",
"category": "launcher"
}
@@ -0,0 +1,24 @@
{
"name": "Launcher",
"publisher": "MicroPythonOS",
"short_description": "Simple launcher to start apps.",
"long_description": "",
"icon_url": "https://apps.micropythonos.com/icons/com.micropythonos.launcher_0.0.3.mpk_icon_64x64.png",
"download_url": "https://apps.micropythonos.com/mpks/com.micropythonos.launcher_0.0.3.mpk",
"fullname": "com.micropythonos.launcher",
"version": "0.0.3",
"category": "launcher",
"activities": [
{
"entrypoint": "assets/launcher.py",
"classname": "MainActivity",
"intent_filters": [
{
"action": "main",
"category": "launcher"
}
]
}
]
}
@@ -18,6 +18,7 @@ import mpos.ui
class MainActivity(mpos.apps.Activity):
def onCreate(self):
print("launcher.py onCreate()")
main_screen = lv.obj()
main_screen.set_style_border_width(0, 0)
main_screen.set_style_radius(0, 0)
@@ -27,7 +28,6 @@ class MainActivity(mpos.apps.Activity):
main_screen.set_flex_flow(lv.FLEX_FLOW.ROW_WRAP)
self.setContentView(main_screen)
def onResume(self, main_screen):
# Grid parameters
icon_size = 64 # Adjust based on your display
label_height = 24
@@ -63,9 +63,12 @@ class MainActivity(mpos.apps.Activity):
base_name = d
if base_name not in seen_base_names: # Avoid duplicates
seen_base_names.add(base_name)
#print(f"seen_base_names: {seen_base_names}")
app = mpos.apps.parse_manifest(f"{full_path}/META-INF/MANIFEST.JSON")
if app.category != "launcher": # Skip launchers
app_list.append((app.name, full_path))
main_launcher = mpos.apps.find_main_launcher_activity(app)
if main_launcher:
app_list.append((app.name, full_path))
except OSError:
pass
@@ -74,7 +77,7 @@ class MainActivity(mpos.apps.Activity):
# Create UI for each app
for app_name, app_dir_fullpath in app_list:
#print(f"Adding app {app_name} from {app_dir_fullpath}")
print(f"Adding app {app_name} from {app_dir_fullpath}")
# Create container for each app (icon + label)
app_cont = lv.obj(main_screen)
app_cont.set_size(iconcont_width, iconcont_height)
+55 -22
View File
@@ -18,7 +18,7 @@ def good_stack_size():
return stacksize
# Run the script in the current thread:
def execute_script(script_source, is_file, cwd=None):
def execute_script(script_source, is_file, cwd=None, classname=None):
thread_id = _thread.get_ident()
compile_name = 'script' if not is_file else script_source
print(f"Thread {thread_id}: executing script with cwd: {cwd}")
@@ -46,17 +46,12 @@ def execute_script(script_source, is_file, cwd=None):
#print("Classes:", classes.keys())
#print("Functions:", functions.keys())
#print("Variables:", variables.keys())
main_activity = script_globals.get("MainActivity")
if not main_activity: # Fallback to taking the first non-generic Activity class, but that's slower
for k, v in script_globals.items():
if k != "Activity" and isinstance(v, type):
main_activity = v # first one is the 'Activity' from which it inherits so take the second one
break
print(f"Got main_activity: {main_activity}")
if main_activity:
Activity.startActivity(None, Intent(activity_class=main_activity))
else:
print("Warning: could not find main_activity")
if classname:
main_activity = script_globals.get(classname)
if main_activity:
Activity.startActivity(None, Intent(activity_class=main_activity))
else:
print("Warning: could not find main_activity")
except Exception as e:
print(f"Thread {thread_id}: exception during execution:")
# Print stack trace with exception type, value, and traceback
@@ -110,9 +105,13 @@ def start_app(app_dir, is_launcher=False):
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)
start_script_fullpath = f"{app_dir}/{app.entrypoint}"
#execute_script_new_thread(start_script_fullpath, True, is_launcher, True) # Starting (GUI?) apps in a new thread can cause hangs (GIL lock?)
execute_script(start_script_fullpath, True, app_dir + "/assets/")
print(f"start_app parsed manifest and got: {str(app)}")
main_launcher_activity = find_main_launcher_activity(app)
if not main_launcher_activity:
print(f"WARNING: can't start {app_dir} because no main_launcher_activity was found.")
return
start_script_fullpath = f"{app_dir}/{main_launcher_activity.get('entrypoint')}"
execute_script(start_script_fullpath, True, app_dir + "/assets/", main_launcher_activity.get("classname"))
# Launchers have the bar, other apps don't have it
if is_launcher:
mpos.ui.open_bar()
@@ -122,8 +121,22 @@ def start_app(app_dir, is_launcher=False):
def 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.example.launcher", True)
start_app_by_name("com.micropythonos.launcher", True) # Would be better to query the PackageManager for Activities that are launchers
def find_main_launcher_activity(app):
result = None
for activity in app.activities:
if not activity.get("entrypoint") or not activity.get("classname"):
print(f"Warning: activity {activity} has no entrypoint and classname, skipping...")
continue
print("checking activity's intent_filters...")
for intent_filter in activity.get("intent_filters"):
print("checking intent_filter...")
if intent_filter.get("action") == "main" and intent_filter.get("category") == "launcher":
print("found main_launcher!")
result = activity
break
return result
def is_launcher(app_name):
print(f"checking is_launcher for {app_name}")
@@ -132,7 +145,7 @@ def is_launcher(app_name):
class App:
def __init__(self, name, publisher, short_description, long_description, icon_url, download_url, fullname, version, entrypoint, category):
def __init__(self, name, publisher, short_description, long_description, icon_url, download_url, fullname, version, category, activities):
self.name = name
self.publisher = publisher
self.short_description = short_description
@@ -141,10 +154,18 @@ class App:
self.download_url = download_url
self.fullname = fullname
self.version = version
self.entrypoint = entrypoint
self.category = category
self.image = None
self.image_dsc = None
self.activities = activities
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={self.activities})")
def parse_manifest(manifest_path):
# Default values for App object
@@ -157,12 +178,13 @@ def parse_manifest(manifest_path):
download_url="",
fullname="Unknown",
version="0.0.0",
entrypoint="assets/start.py",
category=""
category="",
activities=[]
)
try:
with open(manifest_path, 'r') as f:
app_info = ujson.load(f)
#print(f"parsed app: {app_info}")
# Create App object with values from manifest, falling back to defaults
return App(
name=app_info.get("name", default_app.name),
@@ -173,12 +195,14 @@ def parse_manifest(manifest_path):
download_url=app_info.get("download_url", default_app.download_url),
fullname=app_info.get("fullname", default_app.fullname),
version=app_info.get("version", default_app.version),
entrypoint=app_info.get("entrypoint", default_app.entrypoint),
category=app_info.get("category", default_app.category)
category=app_info.get("category", default_app.category),
activities=app_info.get("activities", default_app.activities)
)
except OSError:
print(f"parse_manifest: error loading manifest_path: {manifest_path}")
return default_app
def auto_connect():
# A generic "start at boot" mechanism hasn't been implemented yet, so do it like this:
@@ -201,6 +225,8 @@ class Activity:
def __init__(self):
self.intent = None # Store the intent that launched this activity
self.result = None
self._result_callback = None
def getIntent(self):
return self.intent
@@ -221,8 +247,15 @@ class Activity:
def setContentView(self, screen):
mpos.ui.setContentView(self, screen)
def setResult(self, result_code, data=None):
"""Set the result to be returned when the activity finishes."""
self.result = {"result_code": result_code, "data": data or {}}
def finish(self):
mpos.ui.back_screen()
if self._result_callback and self.result:
self._result_callback(self.result)
self._result_callback = None # Clean up
def startActivity(self, intent):
ActivityNavigator.startActivity(intent)
+3 -3
View File
@@ -464,8 +464,8 @@ def empty_screen_stack():
screen_stack.clear()
def load_screen(screen):
setContentView(None, screen) # for compatibility with old apps
#def load_screen(screen):
# setContentView(None, screen) # for compatibility with old apps
# new_activity might be None for compatibility, can be removed if compatibility is no longer needed
def setContentView(new_activity, new_screen):
@@ -477,7 +477,7 @@ def setContentView(new_activity, new_screen):
current_activity, current_screen = screen_stack[-1]
if current_activity and current_screen:
# Notify current activity it's being backgrounded:
# Notify current activity that it's being backgrounded:
current_activity.onPause(current_screen)
current_activity.onStop(current_screen)