2025-10-29 23:27:04 +01:00
|
|
|
|
import os
|
2025-10-30 11:42:42 +01:00
|
|
|
|
|
2025-10-29 23:30:43 +01:00
|
|
|
|
try:
|
|
|
|
|
|
import zipfile
|
|
|
|
|
|
except ImportError:
|
|
|
|
|
|
print("import zipfile failed, installation won't work!")
|
|
|
|
|
|
|
2025-10-29 22:58:21 +01:00
|
|
|
|
'''
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
|
|
2026-01-25 00:19:38 +01:00
|
|
|
|
class AppManager:
|
2025-10-30 11:42:42 +01:00
|
|
|
|
|
2025-10-30 13:03:19 +01:00
|
|
|
|
_registry = {} # action → [ActivityClass, ...]
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def register_activity(cls, action, activity_cls):
|
|
|
|
|
|
"""Called by each activity module to register itself."""
|
|
|
|
|
|
if action not in cls._registry:
|
|
|
|
|
|
cls._registry[action] = []
|
|
|
|
|
|
if activity_cls not in cls._registry[action]:
|
|
|
|
|
|
cls._registry[action].append(activity_cls)
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def resolve_activity(cls, intent):
|
|
|
|
|
|
"""Return list of Activity classes that handle the intent.action."""
|
|
|
|
|
|
return cls._registry.get(intent.action, [])
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def query_intent_activities(cls, intent):
|
|
|
|
|
|
"""Same as resolve_activity – more Android-like name."""
|
|
|
|
|
|
return cls.resolve_activity(intent)
|
2025-10-30 11:42:42 +01:00
|
|
|
|
|
2025-10-29 23:40:05 +01:00
|
|
|
|
"""Registry of all discovered apps.
|
2025-10-29 22:58:21 +01:00
|
|
|
|
|
2026-01-25 00:19:38 +01:00
|
|
|
|
* AppManager.get_app_list() -> list of App objects (sorted by name)
|
|
|
|
|
|
* AppManager[fullname] -> App (raises KeyError if missing)
|
|
|
|
|
|
* AppManager.get(fullname) -> App or None
|
2025-10-29 23:40:05 +01:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
_app_list = [] # sorted by app.name
|
|
|
|
|
|
_by_fullname = {} # fullname -> App
|
|
|
|
|
|
|
2025-10-29 22:58:21 +01:00
|
|
|
|
@classmethod
|
|
|
|
|
|
def get_app_list(cls):
|
2025-10-29 23:40:05 +01:00
|
|
|
|
if not cls._app_list:
|
2025-10-30 00:13:25 +01:00
|
|
|
|
cls.refresh_apps()
|
2025-10-29 23:40:05 +01:00
|
|
|
|
return cls._app_list
|
2025-10-29 22:58:21 +01:00
|
|
|
|
|
2025-10-29 23:40:05 +01:00
|
|
|
|
def __class_getitem__(cls, fullname):
|
|
|
|
|
|
try:
|
|
|
|
|
|
return cls._by_fullname[fullname]
|
|
|
|
|
|
except KeyError:
|
|
|
|
|
|
raise KeyError("No app with fullname='{}'".format(fullname))
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def get(cls, fullname):
|
2025-11-12 11:56:16 +01:00
|
|
|
|
if not cls._app_list:
|
|
|
|
|
|
cls.refresh_apps()
|
2025-10-29 23:40:05 +01:00
|
|
|
|
return cls._by_fullname.get(fullname)
|
|
|
|
|
|
|
2025-11-12 11:56:16 +01:00
|
|
|
|
@classmethod
|
|
|
|
|
|
def get_launcher(cls):
|
|
|
|
|
|
for app in cls.get_app_list():
|
|
|
|
|
|
if app.is_valid_launcher():
|
|
|
|
|
|
print(f"Found launcher {app.fullname}")
|
|
|
|
|
|
return app
|
|
|
|
|
|
|
2025-10-30 00:13:25 +01:00
|
|
|
|
@classmethod
|
|
|
|
|
|
def clear(cls):
|
|
|
|
|
|
"""Empty the internal caches. Call ``get_app_list()`` afterwards to repopulate."""
|
|
|
|
|
|
cls._app_list = []
|
|
|
|
|
|
cls._by_fullname = {}
|
|
|
|
|
|
|
2025-10-29 22:58:21 +01:00
|
|
|
|
@classmethod
|
2025-10-30 00:13:25 +01:00
|
|
|
|
def refresh_apps(cls):
|
2026-01-25 00:19:38 +01:00
|
|
|
|
print("AppManager finding apps...")
|
2025-10-29 23:40:05 +01:00
|
|
|
|
|
2025-10-30 00:13:25 +01:00
|
|
|
|
cls.clear() # <-- this guarantees both containers are empty
|
2025-10-29 23:40:05 +01:00
|
|
|
|
seen = set() # avoid processing the same fullname twice
|
|
|
|
|
|
apps_dir = "apps"
|
2025-10-29 22:58:21 +01:00
|
|
|
|
apps_dir_builtin = "builtin/apps"
|
|
|
|
|
|
|
2025-11-14 11:51:15 +01:00
|
|
|
|
for base in (apps_dir, apps_dir_builtin): # added apps override builtin apps
|
2025-10-29 22:58:21 +01:00
|
|
|
|
try:
|
2025-10-29 23:40:05 +01:00
|
|
|
|
# ---- does the directory exist? --------------------------------
|
|
|
|
|
|
st = os.stat(base)
|
|
|
|
|
|
if not (st[0] & 0x4000): # 0x4000 = directory bit
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# ---- iterate over immediate children -------------------------
|
|
|
|
|
|
for name in os.listdir(base):
|
|
|
|
|
|
full_path = "{}/{}".format(base, name)
|
|
|
|
|
|
|
|
|
|
|
|
# ---- is it a directory? ---------------------------------
|
2025-10-29 22:58:21 +01:00
|
|
|
|
try:
|
2025-10-29 23:40:05 +01:00
|
|
|
|
st = os.stat(full_path)
|
|
|
|
|
|
if not (st[0] & 0x4000):
|
|
|
|
|
|
continue
|
2025-10-29 22:58:21 +01:00
|
|
|
|
except Exception as e:
|
2026-01-25 00:19:38 +01:00
|
|
|
|
print("AppManager: stat of {} got exception: {}".format(full_path, e))
|
2025-10-29 23:40:05 +01:00
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
fullname = name
|
|
|
|
|
|
|
|
|
|
|
|
# ---- skip duplicates ------------------------------------
|
|
|
|
|
|
if fullname in seen:
|
|
|
|
|
|
continue
|
|
|
|
|
|
seen.add(fullname)
|
|
|
|
|
|
|
|
|
|
|
|
# ---- parse the manifest ---------------------------------
|
|
|
|
|
|
try:
|
2025-10-30 13:03:19 +01:00
|
|
|
|
from ..app.app import App
|
2025-10-30 11:42:42 +01:00
|
|
|
|
app = App.from_manifest(full_path)
|
2025-10-29 23:40:05 +01:00
|
|
|
|
except Exception as e:
|
2026-01-25 00:19:38 +01:00
|
|
|
|
print("AppManager: parsing {} failed: {}".format(full_path, e))
|
2025-10-29 23:40:05 +01:00
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# ---- store in both containers ---------------------------
|
|
|
|
|
|
cls._app_list.append(app)
|
|
|
|
|
|
cls._by_fullname[fullname] = app
|
2026-02-25 15:40:36 +01:00
|
|
|
|
#print("added app {}".format(app))
|
2025-10-29 23:40:05 +01:00
|
|
|
|
|
2025-10-29 22:58:21 +01:00
|
|
|
|
except Exception as e:
|
2026-01-25 00:19:38 +01:00
|
|
|
|
print("AppManager: handling {} got exception: {}".format(base, e))
|
2025-10-29 23:40:05 +01:00
|
|
|
|
|
|
|
|
|
|
# ---- sort the list by display name (case-insensitive) ------------
|
|
|
|
|
|
cls._app_list.sort(key=lambda a: a.name.lower())
|
2025-10-29 22:58:21 +01:00
|
|
|
|
|
2025-10-29 23:27:04 +01:00
|
|
|
|
@staticmethod
|
|
|
|
|
|
def uninstall_app(app_fullname):
|
|
|
|
|
|
try:
|
|
|
|
|
|
import shutil
|
|
|
|
|
|
shutil.rmtree(f"apps/{app_fullname}") # never in builtin/apps because those can't be uninstalled
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"Removing app_folder {app_folder} got error: {e}")
|
2026-01-25 00:19:38 +01:00
|
|
|
|
AppManager.refresh_apps()
|
2025-10-29 23:27:04 +01:00
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def install_mpk(temp_zip_path, dest_folder):
|
|
|
|
|
|
try:
|
2026-02-21 06:42:04 +00:00
|
|
|
|
# Step 1: Remove any existing (possibly partial) install or symlink
|
|
|
|
|
|
try:
|
|
|
|
|
|
st = os.stat(dest_folder)
|
|
|
|
|
|
if st[0] & 0x4000: # It's a real directory
|
|
|
|
|
|
import shutil
|
|
|
|
|
|
shutil.rmtree(dest_folder)
|
|
|
|
|
|
print("Removed existing folder:", dest_folder)
|
|
|
|
|
|
else:
|
|
|
|
|
|
os.remove(dest_folder)
|
|
|
|
|
|
print("Removed existing file:", dest_folder)
|
|
|
|
|
|
except OSError:
|
|
|
|
|
|
pass # Doesn't exist, that's fine
|
|
|
|
|
|
# Also remove if it's a symlink (broken or otherwise)
|
|
|
|
|
|
try:
|
|
|
|
|
|
os.remove(dest_folder)
|
|
|
|
|
|
print("Removed symlink:", dest_folder)
|
|
|
|
|
|
except OSError:
|
|
|
|
|
|
pass # Not a symlink or already removed
|
|
|
|
|
|
|
2025-10-29 23:27:04 +01:00
|
|
|
|
# Step 2: Unzip the file
|
|
|
|
|
|
print("Unzipping it to:", dest_folder)
|
|
|
|
|
|
with zipfile.ZipFile(temp_zip_path, "r") as zip_ref:
|
|
|
|
|
|
zip_ref.extractall(dest_folder)
|
|
|
|
|
|
print("Unzipped successfully")
|
|
|
|
|
|
# Step 3: Clean up
|
|
|
|
|
|
os.remove(temp_zip_path)
|
|
|
|
|
|
print("Removed temporary .mpk file")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"Unzip and cleanup failed: {e}")
|
|
|
|
|
|
# Would be good to show error message here if it fails...
|
2026-01-25 00:19:38 +01:00
|
|
|
|
AppManager.refresh_apps()
|
2025-10-29 23:27:04 +01:00
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def compare_versions(ver1: str, ver2: str) -> bool:
|
|
|
|
|
|
"""Compare two version numbers (e.g., '1.2.3' vs '4.5.6').
|
2025-11-02 14:04:16 +01:00
|
|
|
|
Returns True if ver1 is greater than ver2, False otherwise.
|
|
|
|
|
|
Invalid or empty version numbers also result in False."""
|
2025-10-29 23:27:04 +01:00
|
|
|
|
print(f"Comparing versions: {ver1} vs {ver2}")
|
2025-11-02 13:28:29 +01:00
|
|
|
|
try:
|
|
|
|
|
|
v1_parts = [int(x) for x in ver1.split('.')]
|
|
|
|
|
|
v2_parts = [int(x) for x in ver2.split('.')]
|
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
|
print(f"Invalid input, got error: {e}")
|
|
|
|
|
|
return False
|
2025-10-29 23:27:04 +01:00
|
|
|
|
print(f"Version 1 parts: {v1_parts}")
|
|
|
|
|
|
print(f"Version 2 parts: {v2_parts}")
|
|
|
|
|
|
for i in range(max(len(v1_parts), len(v2_parts))):
|
|
|
|
|
|
v1 = v1_parts[i] if i < len(v1_parts) else 0
|
|
|
|
|
|
v2 = v2_parts[i] if i < len(v2_parts) else 0
|
|
|
|
|
|
print(f"Comparing part {i}: {v1} vs {v2}")
|
|
|
|
|
|
if v1 > v2:
|
|
|
|
|
|
print(f"{ver1} is greater than {ver2}")
|
|
|
|
|
|
return True
|
|
|
|
|
|
if v1 < v2:
|
|
|
|
|
|
print(f"{ver1} is less than {ver2}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
print(f"Versions are equal or {ver1} is not greater than {ver2}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def is_builtin_app(app_fullname):
|
2026-01-25 00:19:38 +01:00
|
|
|
|
return AppManager.is_installed_by_path(f"builtin/apps/{app_fullname}")
|
2025-10-29 23:27:04 +01:00
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def is_overridden_builtin_app(app_fullname):
|
2026-01-25 00:19:38 +01:00
|
|
|
|
return AppManager.is_installed_by_path(f"apps/{app_fullname}") and AppManager.is_installed_by_path(f"builtin/apps/{app_fullname}")
|
2025-10-29 23:27:04 +01:00
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def is_update_available(app_fullname, new_version):
|
|
|
|
|
|
appdir = f"apps/{app_fullname}"
|
|
|
|
|
|
builtinappdir = f"builtin/apps/{app_fullname}"
|
2026-01-25 00:19:38 +01:00
|
|
|
|
installed_app=AppManager.get(app_fullname)
|
2025-10-30 00:13:25 +01:00
|
|
|
|
if not installed_app:
|
2025-10-29 23:27:04 +01:00
|
|
|
|
return False
|
2026-01-25 00:19:38 +01:00
|
|
|
|
return AppManager.compare_versions(new_version, installed_app.version)
|
2025-10-29 23:27:04 +01:00
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def is_installed_by_path(dir_path):
|
|
|
|
|
|
try:
|
|
|
|
|
|
if os.stat(dir_path)[0] & 0x4000:
|
|
|
|
|
|
print(f"is_installed_by_path: {dir_path} found, checking manifest...")
|
|
|
|
|
|
manifest = f"{dir_path}/META-INF/MANIFEST.JSON"
|
|
|
|
|
|
if os.stat(manifest)[0] & 0x8000:
|
|
|
|
|
|
return True
|
|
|
|
|
|
except OSError:
|
|
|
|
|
|
print(f"is_installed_by_path got OSError for {dir_path}")
|
|
|
|
|
|
pass # Skip if directory or manifest doesn't exist
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def is_installed_by_name(app_fullname):
|
|
|
|
|
|
print(f"Checking if app {app_fullname} is installed...")
|
2026-01-25 00:19:38 +01:00
|
|
|
|
return AppManager.is_installed_by_path(f"apps/{app_fullname}") or AppManager.is_installed_by_path(f"builtin/apps/{app_fullname}")
|
2025-10-29 23:27:04 +01:00
|
|
|
|
|
2026-01-25 00:08:01 +01:00
|
|
|
|
@staticmethod
|
|
|
|
|
|
def execute_script(script_source, is_file, classname, cwd=None):
|
|
|
|
|
|
"""Run the script in the current thread. Returns True if successful."""
|
|
|
|
|
|
import utime # for timing read and compile
|
|
|
|
|
|
import lvgl as lv
|
|
|
|
|
|
import mpos.ui
|
|
|
|
|
|
import _thread
|
|
|
|
|
|
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}")
|
|
|
|
|
|
try:
|
|
|
|
|
|
if is_file:
|
|
|
|
|
|
print(f"Thread {thread_id}: reading script from file {script_source}")
|
|
|
|
|
|
with open(script_source, 'r') as f: # No need to check if it exists as exceptions are caught
|
|
|
|
|
|
start_time = utime.ticks_ms()
|
|
|
|
|
|
script_source = f.read()
|
|
|
|
|
|
read_time = utime.ticks_diff(utime.ticks_ms(), start_time)
|
|
|
|
|
|
print(f"execute_script: reading script_source took {read_time}ms")
|
|
|
|
|
|
script_globals = {
|
|
|
|
|
|
'lv': lv,
|
2026-01-26 14:50:18 +01:00
|
|
|
|
'__name__': "__main__", # in case the script wants this
|
|
|
|
|
|
'__file__': compile_name
|
2026-01-25 00:08:01 +01:00
|
|
|
|
}
|
|
|
|
|
|
print(f"Thread {thread_id}: starting script")
|
|
|
|
|
|
import sys
|
|
|
|
|
|
path_before = sys.path[:] # Make a copy, not a reference
|
|
|
|
|
|
if cwd:
|
|
|
|
|
|
sys.path.append(cwd)
|
|
|
|
|
|
try:
|
|
|
|
|
|
start_time = utime.ticks_ms()
|
|
|
|
|
|
compiled_script = compile(script_source, compile_name, 'exec')
|
|
|
|
|
|
compile_time = utime.ticks_diff(utime.ticks_ms(), start_time)
|
|
|
|
|
|
print(f"execute_script: compiling script_source took {compile_time}ms")
|
|
|
|
|
|
start_time = utime.ticks_ms()
|
|
|
|
|
|
exec(compiled_script, script_globals)
|
|
|
|
|
|
end_time = utime.ticks_diff(utime.ticks_ms(), start_time)
|
|
|
|
|
|
print(f"apps.py execute_script: exec took {end_time}ms")
|
|
|
|
|
|
# Introspect globals
|
|
|
|
|
|
classes = {k: v for k, v in script_globals.items() if isinstance(v, type)}
|
|
|
|
|
|
functions = {k: v for k, v in script_globals.items() if callable(v) and not isinstance(v, type)}
|
|
|
|
|
|
variables = {k: v for k, v in script_globals.items() if not callable(v)}
|
|
|
|
|
|
print("Classes:", classes.keys()) # This lists a whole bunch of classes, including lib/mpos/ stuff
|
|
|
|
|
|
print("Functions:", functions.keys())
|
|
|
|
|
|
print("Variables:", variables.keys())
|
|
|
|
|
|
main_activity = script_globals.get(classname)
|
|
|
|
|
|
if main_activity:
|
|
|
|
|
|
from ..app.activity import Activity
|
|
|
|
|
|
from .intent import Intent
|
|
|
|
|
|
start_time = utime.ticks_ms()
|
|
|
|
|
|
Activity.startActivity(None, Intent(activity_class=main_activity))
|
|
|
|
|
|
end_time = utime.ticks_diff(utime.ticks_ms(), start_time)
|
|
|
|
|
|
print(f"execute_script: Activity.startActivity took {end_time}ms")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"Warning: could not find app's main_activity {classname}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"Thread {thread_id}: exception during execution:")
|
2026-01-25 00:30:01 +01:00
|
|
|
|
sys.print_exception(e)
|
2026-01-25 00:08:01 +01:00
|
|
|
|
return False
|
|
|
|
|
|
finally:
|
|
|
|
|
|
# Always restore sys.path, even if we return early or raise an exception
|
|
|
|
|
|
print(f"Thread {thread_id}: script {compile_name} finished, restoring sys.path from {sys.path} to {path_before}")
|
|
|
|
|
|
sys.path = path_before
|
|
|
|
|
|
return True
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"Thread {thread_id}: error:")
|
|
|
|
|
|
tb = getattr(e, '__traceback__', None)
|
|
|
|
|
|
traceback.print_exception(type(e), e, tb)
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def start_app(fullname):
|
|
|
|
|
|
"""Start an app by fullname. Returns True if successful."""
|
|
|
|
|
|
import mpos.ui
|
|
|
|
|
|
mpos.ui.set_foreground_app(fullname)
|
|
|
|
|
|
import utime
|
|
|
|
|
|
start_time = utime.ticks_ms()
|
2026-01-25 00:19:38 +01:00
|
|
|
|
app = AppManager.get(fullname)
|
2026-01-25 00:08:01 +01:00
|
|
|
|
if not app:
|
|
|
|
|
|
print(f"Warning: start_app can't find app {fullname}")
|
|
|
|
|
|
return
|
|
|
|
|
|
if not app.installed_path:
|
|
|
|
|
|
print(f"Warning: start_app can't start {fullname} because no it doesn't have an installed_path")
|
|
|
|
|
|
return
|
|
|
|
|
|
entrypoint = "assets/main.py"
|
|
|
|
|
|
classname = "Main"
|
|
|
|
|
|
if not app.main_launcher_activity:
|
|
|
|
|
|
print(f"WARNING: app {fullname} doesn't have a main_launcher_activity, defaulting to class {classname} in {entrypoint}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
entrypoint = app.main_launcher_activity.get('entrypoint')
|
|
|
|
|
|
classname = app.main_launcher_activity.get("classname")
|
2026-01-25 00:19:38 +01:00
|
|
|
|
result = AppManager.execute_script(app.installed_path + "/" + entrypoint, True, classname, app.installed_path + "/assets/")
|
2026-01-25 00:08:01 +01:00
|
|
|
|
# Launchers have the bar, other apps don't have it
|
|
|
|
|
|
if app.is_valid_launcher():
|
|
|
|
|
|
mpos.ui.topmenu.open_bar()
|
|
|
|
|
|
else:
|
|
|
|
|
|
mpos.ui.topmenu.close_bar()
|
|
|
|
|
|
end_time = utime.ticks_diff(utime.ticks_ms(), start_time)
|
|
|
|
|
|
print(f"start_app() took {end_time}ms")
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def restart_launcher():
|
|
|
|
|
|
"""Restart the launcher by stopping all activities and starting the launcher app."""
|
|
|
|
|
|
import mpos.ui
|
|
|
|
|
|
print("restart_launcher")
|
|
|
|
|
|
# Stop all apps
|
|
|
|
|
|
mpos.ui.remove_and_stop_all_activities()
|
|
|
|
|
|
# No need to stop the other launcher first, because it exits after building the screen
|
2026-01-25 00:19:38 +01:00
|
|
|
|
return AppManager.start_app(AppManager.get_launcher().fullname)
|