Fix AppStore

This commit is contained in:
Thomas Farstrike
2025-06-03 16:37:50 +02:00
parent 3cf5d7f11a
commit 69a11e9d54
15 changed files with 488 additions and 475 deletions
@@ -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)
@@ -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"
}
@@ -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",))
@@ -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"
}
]
}
]
}
@@ -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
+1
View File
@@ -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")
+16 -7
View File
@@ -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