You've already forked MicroPythonOS
mirror of
https://github.com/m5stack/MicroPythonOS.git
synced 2026-05-20 11:51:27 -07:00
Add support for activities and intent filters to MANIFEST.JSON
This commit is contained in:
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+6
-3
@@ -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)
|
||||
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user