From 69a11e9d54c79bb2c81252f9da2607d17d5df025 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Tue, 3 Jun 2025 16:37:50 +0200 Subject: [PATCH] Fix AppStore --- .../assets/captureqr.py | 1 - .../META-INF/MANIFEST.JSON | 13 - .../com.example.appstore/assets/appstore.py | 454 ------------------ .../META-INF/MANIFEST.JSON | 24 + .../assets/appstore.py | 447 +++++++++++++++++ .../res/mipmap-mdpi/icon_64x64.png | Bin .../META-INF/MANIFEST.JSON | 0 .../assets/osupdate.py | 0 .../res/mipmap-mdpi/icon_64x64.png | Bin .../META-INF/MANIFEST.JSON | 0 .../assets/auto_connect.py | 0 .../assets/wificonf.py | 0 .../res/mipmap-mdpi/icon_64x64.png | Bin internal_filesystem/lib/mpos/ui.py | 1 + scripts/bundleapps.sh | 23 +- 15 files changed, 488 insertions(+), 475 deletions(-) delete mode 100644 internal_filesystem/builtin/apps/com.example.appstore/META-INF/MANIFEST.JSON delete mode 100644 internal_filesystem/builtin/apps/com.example.appstore/assets/appstore.py create mode 100644 internal_filesystem/builtin/apps/com.micropythonos.appstore/META-INF/MANIFEST.JSON create mode 100644 internal_filesystem/builtin/apps/com.micropythonos.appstore/assets/appstore.py rename internal_filesystem/builtin/apps/{com.example.appstore => com.micropythonos.appstore}/res/mipmap-mdpi/icon_64x64.png (100%) rename internal_filesystem/builtin/apps/{com.example.osupdate => com.micropythonos.osupdate}/META-INF/MANIFEST.JSON (100%) rename internal_filesystem/builtin/apps/{com.example.osupdate => com.micropythonos.osupdate}/assets/osupdate.py (100%) rename internal_filesystem/builtin/apps/{com.example.osupdate => com.micropythonos.osupdate}/res/mipmap-mdpi/icon_64x64.png (100%) rename internal_filesystem/builtin/apps/{com.example.wificonf => com.micropythonos.wificonf}/META-INF/MANIFEST.JSON (100%) rename internal_filesystem/builtin/apps/{com.example.wificonf => com.micropythonos.wificonf}/assets/auto_connect.py (100%) rename internal_filesystem/builtin/apps/{com.example.wificonf => com.micropythonos.wificonf}/assets/wificonf.py (100%) rename internal_filesystem/builtin/apps/{com.example.wificonf => com.micropythonos.wificonf}/res/mipmap-mdpi/icon_64x64.png (100%) diff --git a/internal_filesystem/apps/com.lightningpiggy.displaywallet/assets/captureqr.py b/internal_filesystem/apps/com.lightningpiggy.displaywallet/assets/captureqr.py index 061b445e..b06bd138 100644 --- a/internal_filesystem/apps/com.lightningpiggy.displaywallet/assets/captureqr.py +++ b/internal_filesystem/apps/com.lightningpiggy.displaywallet/assets/captureqr.py @@ -143,7 +143,6 @@ class Camera(Activity): if not result: self.status_label.set_text(status_label_text_searching) else: - self.stop_qr_decoding() result = remove_bom(result) result = print_qr_buffer(result) diff --git a/internal_filesystem/builtin/apps/com.example.appstore/META-INF/MANIFEST.JSON b/internal_filesystem/builtin/apps/com.example.appstore/META-INF/MANIFEST.JSON deleted file mode 100644 index 4aa59024..00000000 --- a/internal_filesystem/builtin/apps/com.example.appstore/META-INF/MANIFEST.JSON +++ /dev/null @@ -1,13 +0,0 @@ -{ -"name": "AppStore", -"publisher": "ACME Inc", -"short_description": "Store for App(lication)s", -"long_description": "", -"icon_url": "http://demo.lnpiggy.com:2121/apps/com.example.appstore_0.0.2.mpk_icon_64x64.png", -"download_url": "http://demo.lnpiggy.com:2121/apps/com.example.appstore_0.0.2.mpk", -"fullname": "com.example.appstore", -"version": "0.0.2", -"entrypoint": "assets/appstore.py", -"category": "appstore" -} - diff --git a/internal_filesystem/builtin/apps/com.example.appstore/assets/appstore.py b/internal_filesystem/builtin/apps/com.example.appstore/assets/appstore.py deleted file mode 100644 index 38ff975b..00000000 --- a/internal_filesystem/builtin/apps/com.example.appstore/assets/appstore.py +++ /dev/null @@ -1,454 +0,0 @@ -import lvgl as lv -import json -import requests -import gc -import os -import time -import _thread - -import mpos.apps -import mpos.ui - -# Screens: -app_detail_screen = None -main_screen = None - -apps = [] -update_button = None -install_button = None -install_label = None -please_wait_label = None - -progress_bar = None - -action_label_install = "Install" -action_label_uninstall = "Uninstall" -action_label_restore = "Restore Built-in" -action_label_nothing = "Disable" # This doesn't do anything at the moment, but it could mark builtin apps as "Disabled" somehow and also allow for "Enable" then - -def compare_versions(ver1: str, ver2: str) -> bool: - """Compare two version numbers (e.g., '1.2.3' vs '4.5.6'). - Returns True if ver1 is greater than ver2, False otherwise.""" - print(f"Comparing versions: {ver1} vs {ver2}") - v1_parts = [int(x) for x in ver1.split('.')] - v2_parts = [int(x) for x in ver2.split('.')] - 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 - -def is_builtin_app(app_fullname): - return is_installed_by_path(f"builtin/apps/{app_fullname}") - -def is_overridden_builtin_app(app_fullname): - return is_installed_by_path(f"apps/{app_fullname}") and is_installed_by_path(f"builtin/apps/{app_fullname}") - -def is_update_available(app_fullname, new_version): - appdir = f"apps/{app_fullname}" - builtinappdir = f"builtin/apps/{app_fullname}" - installed_app=None - if is_installed_by_path(appdir): - print(f"{appdir} found, getting version...") - installed_app = mpos.apps.parse_manifest(f"{appdir}/META-INF/MANIFEST.JSON") - elif is_installed_by_path(builtinappdir): - print(f"{builtinappdir} found, getting version...") - installed_app = mpos.apps.parse_manifest(f"{builtinappdir}/META-INF/MANIFEST.JSON") - 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 compare_versions(new_version, installed_app.version) - - -def is_installed_by_path(dir_path): - try: - if os.stat(dir_path)[0] & 0x4000: - manifest = f"{dir_path}/META-INF/MANIFEST.JSON" - if os.stat(manifest)[0] & 0x8000: - return True - except OSError: - pass # Skip if directory or manifest doesn't exist - return False - -def is_installed_by_name(app_fullname): - print(f"Checking if app {app_fullname} is installed...") - return is_installed_by_path(f"apps/{app_fullname}") or is_installed_by_path(f"builtin/apps/{app_fullname}") - -def set_install_label(app_fullname): - global install_label - # Figure out whether to show: - # - "install" option if not installed - # - "update" option if already installed and new version - # - "uninstall" option if already installed and not builtin - # - "restore builtin" option if it's an overridden builtin app - # So: - # - install, uninstall and restore builtin can be same button, always shown - # - update is separate button, only shown if already installed and new version - is_installed = True - update_available = False - builtin_app = is_builtin_app(app_fullname) - overridden_builtin_app = is_overridden_builtin_app(app_fullname) - if not overridden_builtin_app: - is_installed = is_installed_by_name(app_fullname) - if is_installed: - if builtin_app: - if overridden_builtin_app: - action_label = action_label_restore - else: - action_label = action_label_nothing - else: - action_label = action_label_uninstall - else: - action_label = action_label_install - install_label.set_text(action_label) - - -def download_icon(url): - print(f"Downloading icon from {url}") - try: - response = requests.get(url, timeout=5) - if response.status_code == 200: - image_data = response.content - print("Downloaded image, size:", len(image_data), "bytes") - image_dsc = lv.image_dsc_t({ - 'data_size': len(image_data), - 'data': image_data - }) - return image_dsc - else: - print("Failed to download image: Status code", response.status_code) - except Exception as e: - print(f"Exception during download of icon: {e}") - return None - -try: - import zipfile -except ImportError: - zipfile = None - - -def uninstall_app(app_folder, app_fullname): - global install_button, progress_bar, update_button - install_button.remove_flag(lv.obj.FLAG.CLICKABLE) # TODO: change color so it's clear the button is not clickable - install_label.set_text("Please wait...") # TODO: Put "Cancel" if cancellation is possible - progress_bar.remove_flag(lv.obj.FLAG.HIDDEN) - progress_bar.set_value(33, lv.ANIM.ON) - time.sleep_ms(500) - try: - import shutil - shutil.rmtree(app_folder) - progress_bar.set_value(66, lv.ANIM.ON) - time.sleep_ms(500) - except Exception as e: - print(f"Removing app_folder {app_folder} got error: {e}") - progress_bar.set_value(100, lv.ANIM.OFF) - time.sleep(1) - progress_bar.add_flag(lv.obj.FLAG.HIDDEN) - progress_bar.set_value(0, lv.ANIM.OFF) - set_install_label(app_fullname) - install_button.add_flag(lv.obj.FLAG.CLICKABLE) - if is_builtin_app(app_fullname): - update_button.remove_flag(lv.obj.FLAG.HIDDEN) - install_button.set_size(lv.pct(47), 40) # if a builtin app was removed, then it was overridden, and a new version is available, so make space for update button - - -def download_and_unzip(zip_url, dest_folder, app_fullname): - global install_button, progress_bar - install_button.remove_flag(lv.obj.FLAG.CLICKABLE) # TODO: change color so it's clear the button is not clickable - install_label.set_text("Please wait...") # TODO: Put "Cancel" if cancellation is possible - progress_bar.remove_flag(lv.obj.FLAG.HIDDEN) - progress_bar.set_value(20, lv.ANIM.ON) - time.sleep_ms(500) - try: - # Step 1: Download the .mpk file - print(f"Downloading .mpk file from: {zip_url}") - response = requests.get(zip_url, timeout=10) - if response.status_code != 200: - print("Download failed: Status code", response.status_code) - response.close() - set_install_label(app_fullname) - progress_bar.set_value(40, lv.ANIM.ON) - time.sleep_ms(500) - # Save the .mpk file to a temporary location - try: - os.remove(temp_zip_path) - except Exception: - pass - try: - os.mkdir("tmp") - except Exception: - pass - temp_zip_path = "tmp/temp.mpk" - print(f"Writing to temporary mpk path: {temp_zip_path}") - # TODO: check free available space first! - with open(temp_zip_path, "wb") as f: - f.write(response.content) - progress_bar.set_value(60, lv.ANIM.ON) - time.sleep_ms(500) - response.close() - print("Downloaded .mpk file, size:", os.stat(temp_zip_path)[6], "bytes") - except Exception as e: - print("Download failed:", str(e)) - # Would be good to show error message here if it fails... - finally: - if 'response' in locals(): - response.close() - try: - # Step 2: Unzip the file - if zipfile is None: - print("WARNING: zipfile module not available in this MicroPython build, unzip will fail!") - print("Unzipping it to:", dest_folder) - with zipfile.ZipFile(temp_zip_path, "r") as zip_ref: - zip_ref.extractall(dest_folder) - progress_bar.set_value(80, lv.ANIM.ON) - time.sleep_ms(500) - 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... - # Success: - progress_bar.set_value(100, lv.ANIM.OFF) - time.sleep(1) - progress_bar.add_flag(lv.obj.FLAG.HIDDEN) - progress_bar.set_value(0, lv.ANIM.OFF) - set_install_label(app_fullname) - install_button.add_flag(lv.obj.FLAG.CLICKABLE) - - -def download_apps(json_url): - global apps, please_wait_label - try: - response = requests.get(json_url, timeout=10) - except Exception as e: - print("Download failed:", e) - lv.async_call(lambda l: please_wait_label.set_text(f"Error downloading app index: {e}"), None) - if response and response.status_code == 200: - print(f"Got response text: {response.text}") - apps = [mpos.apps.App(**app) for app in json.loads(response.text)] - response.close() - # Remove duplicates based on app.name - seen = set() - apps = [app for app in apps if not (app.name in seen or seen.add(app.name))] - # Sort apps by app.name - apps.sort(key=lambda x: x.name.lower()) # Use .lower() for case-insensitive sorting - please_wait_label.add_flag(lv.obj.FLAG.HIDDEN) - lv.async_call(lambda l: create_apps_list(), None) - -def download_icons(): - for app in apps: - print(f"Downloading icon for {app.name}") - image_dsc = download_icon(app.icon_url) - app.image_dsc = image_dsc # save it for the app detail page - lv.async_call(lambda l: app.image.set_src(image_dsc), None) - time.sleep_ms(100) # not waiting here will result in some async_calls() not being executed - print("Finished downloading icons...") - - -def load_icon(icon_path): - with open(icon_path, 'rb') as f: - image_data = f.read() - image_dsc = lv.image_dsc_t({ - 'data_size': len(image_data), - 'data': image_data - }) - return image_dsc - -def create_ui(): - global main_screen, please_wait_label - main_screen = lv.obj() - please_wait_label = lv.label(main_screen) - please_wait_label.set_text("Downloading app index...") - please_wait_label.center() - mpos.ui.load_screen(main_screen) - -def create_apps_list(): - print("create_apps_list") - global apps, main_screen - default_icon_dsc = load_icon("builtin/res/mipmap-mdpi/default_icon_64x64.png") - apps_list = lv.list(main_screen) - apps_list.set_style_pad_all(0, 0) - apps_list.set_size(lv.pct(100), lv.pct(100)) - print("create_apps_list iterating") - for app in apps: - item = apps_list.add_button(None, "Test") - item.set_style_pad_all(0, 0) - item.add_flag(lv.obj.FLAG.CLICKABLE) - item.set_size(lv.pct(100), lv.SIZE_CONTENT) - item.add_event_cb(lambda e, a=app: show_app_detail(a), lv.EVENT.CLICKED, None) - cont = lv.obj(item) - cont.set_style_pad_all(0, 0) - cont.set_flex_flow(lv.FLEX_FLOW.ROW) - cont.set_size(lv.pct(100), lv.SIZE_CONTENT) - cont.set_scrollbar_mode(lv.SCROLLBAR_MODE.OFF) - cont.add_event_cb(lambda e, a=app: show_app_detail(a), lv.EVENT.CLICKED, None) - icon_spacer = lv.image(cont) - icon_spacer.set_size(64, 64) - app.image = icon_spacer - icon_spacer.add_event_cb(lambda e, a=app: show_app_detail(a), lv.EVENT.CLICKED, None) - label_cont = lv.obj(cont) - label_cont.set_flex_flow(lv.FLEX_FLOW.COLUMN) - label_cont.set_size(lv.pct(75), lv.SIZE_CONTENT) - label_cont.add_event_cb(lambda e, a=app: show_app_detail(a), lv.EVENT.CLICKED, None) - name_label = lv.label(label_cont) - name_label.set_text(app.name) - name_label.set_style_text_font(lv.font_montserrat_16, 0) - name_label.add_event_cb(lambda e, a=app: show_app_detail(a), lv.EVENT.CLICKED, None) - desc_label = lv.label(label_cont) - desc_label.set_text(app.short_description) - desc_label.set_style_text_font(lv.font_montserrat_12, 0) - desc_label.add_event_cb(lambda e, a=app: show_app_detail(a), lv.EVENT.CLICKED, None) - print("create_apps_list app done") - try: - _thread.stack_size(mpos.apps.good_stack_size()) - _thread.start_new_thread(download_icons,()) - except Exception as e: - print("Could not start thread to download icons: ", e) - - -def show_app_detail(app): - print("Creating app detail screen...") - global app_detail_screen, install_button, progress_bar, install_label - #app_detail_screen = lv.obj() - #app_detail_screen.set_size(lv.pct(100), lv.pct(100)) - #back_button = lv.button(app_detail_screen) - #back_button.set_width(lv.pct(15)) - #back_button.add_flag(lv.obj.FLAG.CLICKABLE) - #back_button.add_event_cb(back_to_main, lv.EVENT.CLICKED, None) - #back_label = lv.label(back_button) - #back_label.set_text(lv.SYMBOL.LEFT) - #back_label.center() - app_detail_screen = lv.obj() - app_detail_screen.set_size(lv.pct(100), lv.pct(100)) - app_detail_screen.set_pos(0, 40) - app_detail_screen.set_flex_flow(lv.FLEX_FLOW.COLUMN) - # - headercont = lv.obj(app_detail_screen) - headercont.set_style_pad_all(0, 0) - headercont.set_flex_flow(lv.FLEX_FLOW.ROW) - headercont.set_size(lv.pct(100), lv.SIZE_CONTENT) - headercont.set_scrollbar_mode(lv.SCROLLBAR_MODE.OFF) - icon_spacer = lv.image(headercont) - if app.image_dsc: - icon_spacer.set_src(app.image_dsc) - icon_spacer.set_size(64, 64) - # - detail_cont = lv.obj(headercont) - detail_cont.set_style_pad_all(0, 0) - detail_cont.set_flex_flow(lv.FLEX_FLOW.COLUMN) - detail_cont.set_size(lv.pct(75), lv.SIZE_CONTENT) - name_label = lv.label(detail_cont) - name_label.set_text(app.name) - name_label.set_style_text_font(lv.font_montserrat_24, 0) - publisher_label = lv.label(detail_cont) - publisher_label.set_text(app.publisher) - publisher_label.set_style_text_font(lv.font_montserrat_16, 0) - # - progress_bar = lv.bar(app_detail_screen) - progress_bar.set_width(lv.pct(100)) - progress_bar.set_range(0, 100) - progress_bar.add_flag(lv.obj.FLAG.HIDDEN) - # Always have this button: - buttoncont = lv.obj(app_detail_screen) - buttoncont.set_style_pad_all(0, 0) - buttoncont.set_flex_flow(lv.FLEX_FLOW.ROW) - buttoncont.set_size(lv.pct(100), lv.SIZE_CONTENT) - buttoncont.set_scrollbar_mode(lv.SCROLLBAR_MODE.OFF) - print(f"Adding (un)install button for url: {app.download_url}") - install_button = lv.button(buttoncont) - install_button.add_flag(lv.obj.FLAG.CLICKABLE) - install_button.add_event_cb(lambda e, d=app.download_url, f=app.fullname: toggle_install(d,f), lv.EVENT.CLICKED, None) - install_button.set_size(lv.pct(100), 40) - install_label = lv.label(install_button) - install_label.center() - set_install_label(app.fullname) - if is_update_available(app.fullname, app.version): - install_button.set_size(lv.pct(47), 40) # make space for update button - print("Update available, adding update button.") - global update_button - update_button = lv.button(buttoncont) - update_button.set_size(lv.pct(47), 40) - update_button.add_event_cb(lambda e, d=app.download_url, f=app.fullname: update_button_click(d,f), lv.EVENT.CLICKED, None) - update_label = lv.label(update_button) - update_label.set_text("Update") - update_label.center() - # version label: - version_label = lv.label(app_detail_screen) - version_label.set_width(lv.pct(100)) - version_label.set_text(f"Latest version: {app.version}") # make this bold if this is newer than the currently installed one - version_label.set_style_text_font(lv.font_montserrat_12, 0) - version_label.align_to(install_button, lv.ALIGN.OUT_BOTTOM_MID, 0, lv.pct(5)) - long_desc_label = lv.label(app_detail_screen) - long_desc_label.align_to(version_label, lv.ALIGN.OUT_BOTTOM_MID, 0, lv.pct(5)) - long_desc_label.set_text(app.long_description) - long_desc_label.set_style_text_font(lv.font_montserrat_12, 0) - long_desc_label.set_width(lv.pct(100)) - print("Loading app detail screen...") - mpos.ui.load_screen(app_detail_screen) - - -def toggle_install(download_url, fullname): - global install_label - print(f"Install button clicked for {download_url} and fullname {fullname}") - label_text = install_label.get_text() - if label_text == action_label_install: - try: - _thread.stack_size(mpos.apps.good_stack_size()) - _thread.start_new_thread(download_and_unzip, (download_url, f"apps/{fullname}", fullname)) - except Exception as e: - print("Could not start download_and_unzip thread: ", e) - elif label_text == action_label_uninstall or label_text == action_label_restore: - print("Uninstalling app....") - try: - _thread.stack_size(mpos.apps.good_stack_size()) - _thread.start_new_thread(uninstall_app, (f"apps/{fullname}", fullname)) - except Exception as e: - print("Could not start download_and_unzip thread: ", e) - -def update_button_click(download_url, fullname): - print(f"Update button clicked for {download_url} and fullname {fullname}") - global update_button - update_button.add_flag(lv.obj.FLAG.HIDDEN) - install_button.set_size(lv.pct(100), 40) - try: - _thread.stack_size(mpos.apps.good_stack_size()) - _thread.start_new_thread(download_and_unzip, (download_url, f"apps/{fullname}", fullname)) - except Exception as e: - print("Could not start download_and_unzip thread: ", e) - - - -def janitor_cb(timer): - global main_screen, app_detail_screen - if lv.screen_active() != main_screen and lv.screen_active() != app_detail_screen: - print("appstore.py backgrounded, cleaning up...") - janitor.delete() - mpos.apps.restart_launcher() # refresh the launcher - print("appstore.py ending") - -create_ui() - -janitor = lv.timer_create(janitor_cb, 400, None) - -can_check_network = True -try: - import network -except Exception as e: - can_check_network = False - -if can_check_network and not network.WLAN(network.STA_IF).isconnected(): - please_wait_label.set_text("Error: WiFi is not connected.") -else: - _thread.stack_size(mpos.apps.good_stack_size()) - _thread.start_new_thread(download_apps, ("http://demo.lnpiggy.com:2121/apps.json",)) - diff --git a/internal_filesystem/builtin/apps/com.micropythonos.appstore/META-INF/MANIFEST.JSON b/internal_filesystem/builtin/apps/com.micropythonos.appstore/META-INF/MANIFEST.JSON new file mode 100644 index 00000000..355d3585 --- /dev/null +++ b/internal_filesystem/builtin/apps/com.micropythonos.appstore/META-INF/MANIFEST.JSON @@ -0,0 +1,24 @@ +{ +"name": "AppStore", +"publisher": "MicroPythonOS", +"short_description": "Store for App(lication)s", +"long_description": "", +"icon_url": "https://apps.micropythonos.com/icons/com.micropythonos.appstore_0.0.3.mpk_icon_64x64.png", +"download_url": "https://apps.micropythonos.com/mpks/com.micropythonos.appstore_0.0.3.mpk", +"fullname": "com.micropythonos.appstore", +"version": "0.0.3", +"category": "appstore", +"activities": [ + { + "entrypoint": "assets/appstore.py", + "classname": "MainActivity", + "intent_filters": [ + { + "action": "main", + "category": "launcher" + } + ] + } + ] +} + diff --git a/internal_filesystem/builtin/apps/com.micropythonos.appstore/assets/appstore.py b/internal_filesystem/builtin/apps/com.micropythonos.appstore/assets/appstore.py new file mode 100644 index 00000000..f298b2da --- /dev/null +++ b/internal_filesystem/builtin/apps/com.micropythonos.appstore/assets/appstore.py @@ -0,0 +1,447 @@ +import lvgl as lv +import json +import requests +import gc +import os +import time +import _thread + +from mpos.apps import Activity, Intent +import mpos.ui + +# Screens: +app_detail_screen = None +main_screen = None + +action_label_install = "Install" +action_label_uninstall = "Uninstall" +action_label_restore = "Restore Built-in" +action_label_nothing = "Disable" # This doesn't do anything at the moment, but it could mark builtin apps as "Disabled" somehow and also allow for "Enable" then + +class MainActivity(Activity): + main_screen = None + apps = [] + update_button = None + install_button = None + install_label = None + please_wait_label = None + progress_bar = None + can_check_network = True + + def onCreate(self): + self.main_screen = lv.obj() + self.please_wait_label = lv.label(self.main_screen) + self.please_wait_label.set_text("Downloading app index...") + self.please_wait_label.center() + self.setContentView(self.main_screen) + + def onStart(self, screen): + try: + import network + except Exception as e: + self.can_check_network = False + if self.can_check_network and not network.WLAN(network.STA_IF).isconnected(): + self.please_wait_label.set_text("Error: WiFi is not connected.") + else: + _thread.stack_size(mpos.apps.good_stack_size()) + _thread.start_new_thread(self.download_app_index, ("https://apps.micropythonos.com/app_index.json",)) + + def onDestroy(self, screen): + print("appstore.py destroyed, restarting launcher to refresh...") + mpos.apps.restart_launcher() # refresh the launcher + print("appstore.py ending") + + def download_app_index(self, json_url): + try: + response = requests.get(json_url, timeout=10) + except Exception as e: + print("Download failed:", e) + lv.async_call(lambda l, error=e: self.please_wait_label.set_text(f"Downloading app index from:\n{json_url}\ngot error: {error}"), None) + return + if response and response.status_code == 200: + print(f"Got response text: {response.text}") + self.apps = [mpos.apps.App(**app) for app in json.loads(response.text)] + response.close() + # Remove duplicates based on app.name + seen = set() + self.apps = [app for app in self.apps if not (app.name in seen or seen.add(app.name))] + # Sort apps by app.name + self.apps.sort(key=lambda x: x.name.lower()) # Use .lower() for case-insensitive sorting + self.please_wait_label.add_flag(lv.obj.FLAG.HIDDEN) + lv.async_call(lambda l: self.create_apps_list(), None) + + def create_apps_list(self): + print("create_apps_list") + default_icon_dsc = load_icon("builtin/res/mipmap-mdpi/default_icon_64x64.png") + apps_list = lv.list(self.main_screen) + apps_list.set_style_pad_all(0, 0) + apps_list.set_size(lv.pct(100), lv.pct(100)) + print("create_apps_list iterating") + for app in self.apps: + item = apps_list.add_button(None, "Test") + item.set_style_pad_all(0, 0) + item.add_flag(lv.obj.FLAG.CLICKABLE) + item.set_size(lv.pct(100), lv.SIZE_CONTENT) + item.add_event_cb(lambda e, a=app: self.show_app_detail(a), lv.EVENT.CLICKED, None) + cont = lv.obj(item) + cont.set_style_pad_all(0, 0) + cont.set_flex_flow(lv.FLEX_FLOW.ROW) + cont.set_size(lv.pct(100), lv.SIZE_CONTENT) + cont.set_scrollbar_mode(lv.SCROLLBAR_MODE.OFF) + cont.add_event_cb(lambda e, a=app: self.show_app_detail(a), lv.EVENT.CLICKED, None) + icon_spacer = lv.image(cont) + icon_spacer.set_size(64, 64) + app.image = icon_spacer + icon_spacer.add_event_cb(lambda e, a=app: self.show_app_detail(a), lv.EVENT.CLICKED, None) + label_cont = lv.obj(cont) + label_cont.set_flex_flow(lv.FLEX_FLOW.COLUMN) + label_cont.set_size(lv.pct(75), lv.SIZE_CONTENT) + label_cont.add_event_cb(lambda e, a=app: self.show_app_detail(a), lv.EVENT.CLICKED, None) + name_label = lv.label(label_cont) + name_label.set_text(app.name) + name_label.set_style_text_font(lv.font_montserrat_16, 0) + name_label.add_event_cb(lambda e, a=app: self.show_app_detail(a), lv.EVENT.CLICKED, None) + desc_label = lv.label(label_cont) + desc_label.set_text(app.short_description) + desc_label.set_style_text_font(lv.font_montserrat_12, 0) + desc_label.add_event_cb(lambda e, a=app: self.show_app_detail(a), lv.EVENT.CLICKED, None) + print("create_apps_list app done") + try: + _thread.stack_size(mpos.apps.good_stack_size()) + _thread.start_new_thread(self.download_icons,()) + except Exception as e: + print("Could not start thread to download icons: ", e) + + def download_icons(): + for app in self.apps: + print(f"Downloading icon for {app.name}") + image_dsc = download_icon(app.icon_url) + app.image_dsc = image_dsc # save it for the app detail page + lv.async_call(lambda l: app.image.set_src(image_dsc), None) + time.sleep_ms(100) # not waiting here will result in some async_calls() not being executed + print("Finished downloading icons...") + + def show_app_detail(self, app): + intent = Intent(activity_class=AppDetail) + intent.putExtra("app", app) + self.startActivity(intent) + + + +class AppDetail(Activity): + + try: + import zipfile + except ImportError: + zipfile = None + + install_button = None + update_button = None + progress_bar = None + install_label = None + + def onCreate(self): + print("Creating app detail screen...") + app_detail_screen = lv.obj() + app_detail_screen.set_size(lv.pct(100), lv.pct(100)) + app_detail_screen.set_pos(0, 40) + app_detail_screen.set_flex_flow(lv.FLEX_FLOW.COLUMN) + # + headercont = lv.obj(app_detail_screen) + headercont.set_style_pad_all(0, 0) + headercont.set_flex_flow(lv.FLEX_FLOW.ROW) + headercont.set_size(lv.pct(100), lv.SIZE_CONTENT) + headercont.set_scrollbar_mode(lv.SCROLLBAR_MODE.OFF) + icon_spacer = lv.image(headercont) + if app.image_dsc: + icon_spacer.set_src(app.image_dsc) + icon_spacer.set_size(64, 64) + # + detail_cont = lv.obj(headercont) + detail_cont.set_style_pad_all(0, 0) + detail_cont.set_flex_flow(lv.FLEX_FLOW.COLUMN) + detail_cont.set_size(lv.pct(75), lv.SIZE_CONTENT) + name_label = lv.label(detail_cont) + name_label.set_text(app.name) + name_label.set_style_text_font(lv.font_montserrat_24, 0) + publisher_label = lv.label(detail_cont) + publisher_label.set_text(app.publisher) + publisher_label.set_style_text_font(lv.font_montserrat_16, 0) + # + self.progress_bar = lv.bar(app_detail_screen) + self.progress_bar.set_width(lv.pct(100)) + self.progress_bar.set_range(0, 100) + self.progress_bar.add_flag(lv.obj.FLAG.HIDDEN) + # Always have this button: + buttoncont = lv.obj(app_detail_screen) + buttoncont.set_style_pad_all(0, 0) + buttoncont.set_flex_flow(lv.FLEX_FLOW.ROW) + buttoncont.set_size(lv.pct(100), lv.SIZE_CONTENT) + buttoncont.set_scrollbar_mode(lv.SCROLLBAR_MODE.OFF) + print(f"Adding (un)install button for url: {app.download_url}") + self.install_button = lv.button(buttoncont) + self.install_button.add_flag(lv.obj.FLAG.CLICKABLE) + self.install_button.add_event_cb(lambda e, d=app.download_url, f=app.fullname: self.toggle_install(d,f), lv.EVENT.CLICKED, None) + self.install_button.set_size(lv.pct(100), 40) + self.install_label = lv.label(self.install_button) + self.install_label.center() + set_install_label(app.fullname) + if is_update_available(app.fullname, app.version): + self.install_button.set_size(lv.pct(47), 40) # make space for update button + print("Update available, adding update button.") + global update_button + self.update_button = lv.button(buttoncont) + self.update_button.set_size(lv.pct(47), 40) + self.update_button.add_event_cb(lambda e, d=app.download_url, f=app.fullname: self.update_button_click(d,f), lv.EVENT.CLICKED, None) + update_label = lv.label(self.update_button) + update_label.set_text("Update") + update_label.center() + # version label: + version_label = lv.label(app_detail_screen) + version_label.set_width(lv.pct(100)) + version_label.set_text(f"Latest version: {app.version}") # make this bold if this is newer than the currently installed one + version_label.set_style_text_font(lv.font_montserrat_12, 0) + version_label.align_to(install_button, lv.ALIGN.OUT_BOTTOM_MID, 0, lv.pct(5)) + long_desc_label = lv.label(app_detail_screen) + long_desc_label.align_to(version_label, lv.ALIGN.OUT_BOTTOM_MID, 0, lv.pct(5)) + long_desc_label.set_text(app.long_description) + long_desc_label.set_style_text_font(lv.font_montserrat_12, 0) + long_desc_label.set_width(lv.pct(100)) + print("Loading app detail screen...") + self.setContentView(app_detail_screen) + + + def set_install_label(self, app_fullname): + # Figure out whether to show: + # - "install" option if not installed + # - "update" option if already installed and new version + # - "uninstall" option if already installed and not builtin + # - "restore builtin" option if it's an overridden builtin app + # So: + # - install, uninstall and restore builtin can be same button, always shown + # - update is separate button, only shown if already installed and new version + is_installed = True + update_available = False + builtin_app = is_builtin_app(app_fullname) + overridden_builtin_app = is_overridden_builtin_app(app_fullname) + if not overridden_builtin_app: + is_installed = is_installed_by_name(app_fullname) + if is_installed: + if builtin_app: + if overridden_builtin_app: + action_label = action_label_restore + else: + action_label = action_label_nothing + else: + action_label = action_label_uninstall + else: + action_label = action_label_install + self.install_label.set_text(action_label) + + + def toggle_install(self, download_url, fullname): + global install_label + print(f"Install button clicked for {download_url} and fullname {fullname}") + label_text = self.install_label.get_text() + if label_text == action_label_install: + try: + _thread.stack_size(mpos.apps.good_stack_size()) + _thread.start_new_thread(self.download_and_unzip, (download_url, f"apps/{fullname}", fullname)) + except Exception as e: + print("Could not start download_and_unzip thread: ", e) + elif label_text == action_label_uninstall or label_text == action_label_restore: + print("Uninstalling app....") + try: + _thread.stack_size(mpos.apps.good_stack_size()) + _thread.start_new_thread(self.uninstall_app, (f"apps/{fullname}", fullname)) + except Exception as e: + print("Could not start download_and_unzip thread: ", e) + + def update_button_click(self, download_url, fullname): + print(f"Update button clicked for {download_url} and fullname {fullname}") + self.update_button.add_flag(lv.obj.FLAG.HIDDEN) + self.install_button.set_size(lv.pct(100), 40) + try: + _thread.stack_size(mpos.apps.good_stack_size()) + _thread.start_new_thread(self.download_and_unzip, (download_url, f"apps/{fullname}", fullname)) + except Exception as e: + print("Could not start download_and_unzip thread: ", e) + + def uninstall_app(app_folder, app_fullname): + self.install_button.remove_flag(lv.obj.FLAG.CLICKABLE) # TODO: change color so it's clear the button is not clickable + self.install_label.set_text("Please wait...") # TODO: Put "Cancel" if cancellation is possible + self.progress_bar.remove_flag(lv.obj.FLAG.HIDDEN) + self.progress_bar.set_value(33, lv.ANIM.ON) + time.sleep_ms(500) + try: + import shutil + shutil.rmtree(app_folder) + self.progress_bar.set_value(66, lv.ANIM.ON) + time.sleep_ms(500) + except Exception as e: + print(f"Removing app_folder {app_folder} got error: {e}") + self.progress_bar.set_value(100, lv.ANIM.OFF) + time.sleep(1) + self.progress_bar.add_flag(lv.obj.FLAG.HIDDEN) + self.progress_bar.set_value(0, lv.ANIM.OFF) + set_install_label(app_fullname) + self.install_button.add_flag(lv.obj.FLAG.CLICKABLE) + if is_builtin_app(app_fullname): + self.update_button.remove_flag(lv.obj.FLAG.HIDDEN) + self.install_button.set_size(lv.pct(47), 40) # if a builtin app was removed, then it was overridden, and a new version is available, so make space for update button + + + def download_and_unzip(zip_url, dest_folder, app_fullname): + self.install_button.remove_flag(lv.obj.FLAG.CLICKABLE) # TODO: change color so it's clear the button is not clickable + self.install_label.set_text("Please wait...") # TODO: Put "Cancel" if cancellation is possible + self.progress_bar.remove_flag(lv.obj.FLAG.HIDDEN) + self.progress_bar.set_value(20, lv.ANIM.ON) + time.sleep_ms(500) + try: + # Step 1: Download the .mpk file + print(f"Downloading .mpk file from: {zip_url}") + response = requests.get(zip_url, timeout=10) + if response.status_code != 200: + print("Download failed: Status code", response.status_code) + response.close() + set_install_label(app_fullname) + self.progress_bar.set_value(40, lv.ANIM.ON) + time.sleep_ms(500) + # Save the .mpk file to a temporary location + try: + os.remove(temp_zip_path) + except Exception: + pass + try: + os.mkdir("tmp") + except Exception: + pass + temp_zip_path = "tmp/temp.mpk" + print(f"Writing to temporary mpk path: {temp_zip_path}") + # TODO: check free available space first! + with open(temp_zip_path, "wb") as f: + f.write(response.content) + self.progress_bar.set_value(60, lv.ANIM.ON) + time.sleep_ms(500) + response.close() + print("Downloaded .mpk file, size:", os.stat(temp_zip_path)[6], "bytes") + except Exception as e: + print("Download failed:", str(e)) + # Would be good to show error message here if it fails... + finally: + if 'response' in locals(): + response.close() + try: + # Step 2: Unzip the file + if zipfile is None: + print("WARNING: zipfile module not available in this MicroPython build, unzip will fail!") + print("Unzipping it to:", dest_folder) + with zipfile.ZipFile(temp_zip_path, "r") as zip_ref: + zip_ref.extractall(dest_folder) + self.progress_bar.set_value(80, lv.ANIM.ON) + time.sleep_ms(500) + 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... + # Success: + self.progress_bar.set_value(100, lv.ANIM.OFF) + time.sleep(1) + self.progress_bar.add_flag(lv.obj.FLAG.HIDDEN) + self.progress_bar.set_value(0, lv.ANIM.OFF) + set_install_label(app_fullname) + self.install_button.add_flag(lv.obj.FLAG.CLICKABLE) + + + +# Non-class functions: + +def compare_versions(ver1: str, ver2: str) -> bool: + """Compare two version numbers (e.g., '1.2.3' vs '4.5.6'). + Returns True if ver1 is greater than ver2, False otherwise.""" + print(f"Comparing versions: {ver1} vs {ver2}") + v1_parts = [int(x) for x in ver1.split('.')] + v2_parts = [int(x) for x in ver2.split('.')] + 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 + +def is_builtin_app(app_fullname): + return is_installed_by_path(f"builtin/apps/{app_fullname}") + +def is_overridden_builtin_app(app_fullname): + return is_installed_by_path(f"apps/{app_fullname}") and is_installed_by_path(f"builtin/apps/{app_fullname}") + +def is_update_available(app_fullname, new_version): + appdir = f"apps/{app_fullname}" + builtinappdir = f"builtin/apps/{app_fullname}" + installed_app=None + if is_installed_by_path(appdir): + print(f"{appdir} found, getting version...") + installed_app = mpos.apps.parse_manifest(f"{appdir}/META-INF/MANIFEST.JSON") + elif is_installed_by_path(builtinappdir): + print(f"{builtinappdir} found, getting version...") + installed_app = mpos.apps.parse_manifest(f"{builtinappdir}/META-INF/MANIFEST.JSON") + 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 compare_versions(new_version, installed_app.version) + + +def is_installed_by_path(dir_path): + try: + if os.stat(dir_path)[0] & 0x4000: + manifest = f"{dir_path}/META-INF/MANIFEST.JSON" + if os.stat(manifest)[0] & 0x8000: + return True + except OSError: + pass # Skip if directory or manifest doesn't exist + return False + +def is_installed_by_name(app_fullname): + print(f"Checking if app {app_fullname} is installed...") + return is_installed_by_path(f"apps/{app_fullname}") or is_installed_by_path(f"builtin/apps/{app_fullname}") + + +def download_icon(url): + print(f"Downloading icon from {url}") + try: + response = requests.get(url, timeout=5) + if response.status_code == 200: + image_data = response.content + print("Downloaded image, size:", len(image_data), "bytes") + image_dsc = lv.image_dsc_t({ + 'data_size': len(image_data), + 'data': image_data + }) + return image_dsc + else: + print("Failed to download image: Status code", response.status_code) + except Exception as e: + print(f"Exception during download of icon: {e}") + return None + + +def load_icon(icon_path): + with open(icon_path, 'rb') as f: + image_data = f.read() + image_dsc = lv.image_dsc_t({ + 'data_size': len(image_data), + 'data': image_data + }) + return image_dsc + + diff --git a/internal_filesystem/builtin/apps/com.example.appstore/res/mipmap-mdpi/icon_64x64.png b/internal_filesystem/builtin/apps/com.micropythonos.appstore/res/mipmap-mdpi/icon_64x64.png similarity index 100% rename from internal_filesystem/builtin/apps/com.example.appstore/res/mipmap-mdpi/icon_64x64.png rename to internal_filesystem/builtin/apps/com.micropythonos.appstore/res/mipmap-mdpi/icon_64x64.png diff --git a/internal_filesystem/builtin/apps/com.example.osupdate/META-INF/MANIFEST.JSON b/internal_filesystem/builtin/apps/com.micropythonos.osupdate/META-INF/MANIFEST.JSON similarity index 100% rename from internal_filesystem/builtin/apps/com.example.osupdate/META-INF/MANIFEST.JSON rename to internal_filesystem/builtin/apps/com.micropythonos.osupdate/META-INF/MANIFEST.JSON diff --git a/internal_filesystem/builtin/apps/com.example.osupdate/assets/osupdate.py b/internal_filesystem/builtin/apps/com.micropythonos.osupdate/assets/osupdate.py similarity index 100% rename from internal_filesystem/builtin/apps/com.example.osupdate/assets/osupdate.py rename to internal_filesystem/builtin/apps/com.micropythonos.osupdate/assets/osupdate.py diff --git a/internal_filesystem/builtin/apps/com.example.osupdate/res/mipmap-mdpi/icon_64x64.png b/internal_filesystem/builtin/apps/com.micropythonos.osupdate/res/mipmap-mdpi/icon_64x64.png similarity index 100% rename from internal_filesystem/builtin/apps/com.example.osupdate/res/mipmap-mdpi/icon_64x64.png rename to internal_filesystem/builtin/apps/com.micropythonos.osupdate/res/mipmap-mdpi/icon_64x64.png diff --git a/internal_filesystem/builtin/apps/com.example.wificonf/META-INF/MANIFEST.JSON b/internal_filesystem/builtin/apps/com.micropythonos.wificonf/META-INF/MANIFEST.JSON similarity index 100% rename from internal_filesystem/builtin/apps/com.example.wificonf/META-INF/MANIFEST.JSON rename to internal_filesystem/builtin/apps/com.micropythonos.wificonf/META-INF/MANIFEST.JSON diff --git a/internal_filesystem/builtin/apps/com.example.wificonf/assets/auto_connect.py b/internal_filesystem/builtin/apps/com.micropythonos.wificonf/assets/auto_connect.py similarity index 100% rename from internal_filesystem/builtin/apps/com.example.wificonf/assets/auto_connect.py rename to internal_filesystem/builtin/apps/com.micropythonos.wificonf/assets/auto_connect.py diff --git a/internal_filesystem/builtin/apps/com.example.wificonf/assets/wificonf.py b/internal_filesystem/builtin/apps/com.micropythonos.wificonf/assets/wificonf.py similarity index 100% rename from internal_filesystem/builtin/apps/com.example.wificonf/assets/wificonf.py rename to internal_filesystem/builtin/apps/com.micropythonos.wificonf/assets/wificonf.py diff --git a/internal_filesystem/builtin/apps/com.example.wificonf/res/mipmap-mdpi/icon_64x64.png b/internal_filesystem/builtin/apps/com.micropythonos.wificonf/res/mipmap-mdpi/icon_64x64.png similarity index 100% rename from internal_filesystem/builtin/apps/com.example.wificonf/res/mipmap-mdpi/icon_64x64.png rename to internal_filesystem/builtin/apps/com.micropythonos.wificonf/res/mipmap-mdpi/icon_64x64.png diff --git a/internal_filesystem/lib/mpos/ui.py b/internal_filesystem/lib/mpos/ui.py index f9ed8351..605b00a2 100644 --- a/internal_filesystem/lib/mpos/ui.py +++ b/internal_filesystem/lib/mpos/ui.py @@ -480,6 +480,7 @@ def setContentView(new_activity, new_screen): # Notify current activity that it's being backgrounded: current_activity.onPause(current_screen) current_activity.onStop(current_screen) + # don't destroy because the user might go back to it # Start the new one: print("Appending screen to screen_stack") diff --git a/scripts/bundleapps.sh b/scripts/bundleapps.sh index 733bc04e..22b56282 100755 --- a/scripts/bundleapps.sh +++ b/scripts/bundleapps.sh @@ -1,12 +1,17 @@ -output=appstore_backend/bundled_apps/ -outputjson=appstore_backend/apps.json +output=/home/user/projects/MicroPythonOS/apps/ +outputjson="$output"/app_index.json output=$(readlink -f "$output") outputjson=$(readlink -f "$outputjson") -mkdir -p "$output" +#mpks="$output"/mpks/ +#icons="$output"/icons/ -rm "$output"/*.mpk -rm "$output"/*.png +mkdir -p "$output" +#mkdir -p "$mpks" +#mkdir -p "$icons" + +#rm "$output"/*.mpk +#rm "$output"/*.png rm "$outputjson" echo "[" | tee -a "$outputjson" @@ -20,10 +25,14 @@ for apprepo in internal_filesystem/apps internal_filesystem/builtin/apps; do version=$( jq -r '.version' "$manifest" ) cat "$manifest" | tee -a "$outputjson" echo -n "," | tee -a "$outputjson" - mpkname="$output"/"$appdir"_"$version".mpk + thisappdir="$output"/"$appdir" + mkdir -p "$thisappdir" + mkdir -p "$thisappdir"/mpks + mkdir -p "$thisappdir"/icons + mpkname="$thisappdir"/mpks/"$appdir"_"$version".mpk echo "Creating $mpkname" zip -r0 "$mpkname" . - cp res/mipmap-mdpi/icon_64x64.png "$mpkname"_icon_64x64.png + cp res/mipmap-mdpi/icon_64x64.png "$thisappdir"/icons/"$appdir"_"$version"_64x64.png popd done done