Move code from AppStore to PackageManager

This commit is contained in:
Thomas Farstrike
2025-10-29 23:27:04 +01:00
parent f044e6c35f
commit e7b71cda0a
3 changed files with 115 additions and 104 deletions
@@ -66,5 +66,7 @@ class About(Activity):
print("main.py: WARNING: could not import/run freezefs_mount_builtin: ", e)
label11 = lv.label(screen)
label11.set_text(f"freezefs_mount_builtin exception (normal on dev builds): {e}")
# TODO:
# - add total size, used and free space on internal storage
# - add total size, used and free space on SD card
self.setContentView(screen)
@@ -13,6 +13,8 @@ except ImportError:
from mpos.apps import Activity, Intent
import mpos.ui
from mpos.package_manager import PackageManager
class AppStore(Activity):
apps = []
@@ -250,7 +252,7 @@ class AppDetail(Activity):
self.install_label = lv.label(self.install_button)
self.install_label.center()
self.set_install_label(app.fullname)
if self.is_update_available(app.fullname, app.version):
if PackageManager.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.")
self.update_button = lv.button(buttoncont)
@@ -285,10 +287,10 @@ class AppDetail(Activity):
# - update is separate button, only shown if already installed and new version
is_installed = True
update_available = False
builtin_app = self.is_builtin_app(app_fullname)
overridden_builtin_app = self.is_overridden_builtin_app(app_fullname)
builtin_app = PackageManager.is_builtin_app(app_fullname)
overridden_builtin_app = PackageManager.is_overridden_builtin_app(app_fullname)
if not overridden_builtin_app:
is_installed = AppDetail.is_installed_by_name(app_fullname)
is_installed = PackageManager.is_installed_by_name(app_fullname)
if is_installed:
if builtin_app:
if overridden_builtin_app:
@@ -308,16 +310,16 @@ class AppDetail(Activity):
if label_text == self.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))
_thread.start_new_thread(self.download_and_install, (download_url, f"apps/{fullname}", fullname))
except Exception as e:
print("Could not start download_and_unzip thread: ", e)
print("Could not start download_and_install thread: ", e)
elif label_text == self.action_label_uninstall or label_text == self.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))
_thread.start_new_thread(self.uninstall_app, (fullname))
except Exception as e:
print("Could not start download_and_unzip thread: ", e)
print("Could not start uninstall_app thread: ", e)
def update_button_click(self, download_url, fullname):
print(f"Update button clicked for {download_url} and fullname {fullname}")
@@ -325,32 +327,26 @@ class AppDetail(Activity):
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))
_thread.start_new_thread(self.download_and_install, (download_url, f"apps/{fullname}", fullname))
except Exception as e:
print("Could not start download_and_unzip thread: ", e)
def uninstall_app(self, app_folder, app_fullname):
print("Could not start download_and_install thread: ", e)
def uninstall_app(self, app_fullname):
self.install_button.add_state(lv.STATE.DISABLED)
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, True)
try:
import shutil
shutil.rmtree(app_folder)
self.progress_bar.set_value(66, True)
except Exception as e:
print(f"Removing app_folder {app_folder} got error: {e}")
self.progress_bar.set_value(42, True)
PackageManager.uninstall_app(app_fullname)
self.progress_bar.set_value(100, False)
self.progress_bar.add_flag(lv.obj.FLAG.HIDDEN)
self.progress_bar.set_value(0, False)
self.set_install_label(app_fullname)
self.install_button.remove_state(lv.STATE.DISABLED)
if self.is_builtin_app(app_fullname):
if PackageManager.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(self, zip_url, dest_folder, app_fullname):
def download_and_install(self, zip_url, dest_folder, app_fullname):
self.install_button.add_state(lv.STATE.DISABLED)
self.install_label.set_text("Please wait...") # TODO: Put "Cancel" if cancellation is possible
self.progress_bar.remove_flag(lv.obj.FLAG.HIDDEN)
@@ -387,86 +383,11 @@ class AppDetail(Activity):
finally:
if 'response' in locals():
response.close()
try:
# 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)
self.progress_bar.set_value(80, True)
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...
# Step 2: install it:
PackageManager.install_mpk(temp_zip_path, dest_folder)
# Success:
self.progress_bar.set_value(100, False)
self.progress_bar.add_flag(lv.obj.FLAG.HIDDEN)
self.progress_bar.set_value(0, False)
self.set_install_label(app_fullname)
self.install_button.remove_state(lv.STATE.DISABLED)
@staticmethod
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
@staticmethod
def is_builtin_app(app_fullname):
return AppDetail.is_installed_by_path(f"builtin/apps/{app_fullname}")
@staticmethod
def is_overridden_builtin_app(app_fullname):
return AppDetail.is_installed_by_path(f"apps/{app_fullname}") and AppDetail.is_installed_by_path(f"builtin/apps/{app_fullname}")
@staticmethod
def is_update_available(app_fullname, new_version):
appdir = f"apps/{app_fullname}"
builtinappdir = f"builtin/apps/{app_fullname}"
installed_app=None
if AppDetail.is_installed_by_path(appdir):
print(f"{appdir} found, getting version...")
installed_app = mpos.apps.parse_manifest(appdir)
elif AppDetail.is_installed_by_path(builtinappdir):
print(f"{builtinappdir} found, getting version...")
installed_app = mpos.apps.parse_manifest(builtinappdir)
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 AppDetail.compare_versions(new_version, installed_app.version)
@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...")
return AppDetail.is_installed_by_path(f"apps/{app_fullname}") or AppDetail.is_installed_by_path(f"builtin/apps/{app_fullname}")
@@ -1,4 +1,4 @@
import uos
import os
import mpos.apps
'''
@@ -45,13 +45,13 @@ class PackageManager():
# Check and collect unique subdirectories
for dir_path in [apps_dir, apps_dir_builtin]:
try:
if uos.stat(dir_path)[0] & 0x4000: # Verify directory exists
if os.stat(dir_path)[0] & 0x4000: # Verify directory exists
try:
for d in uos.listdir(dir_path):
for d in os.listdir(dir_path):
full_path = f"{dir_path}/{d}"
print(f"full_path: {full_path}")
try:
if uos.stat(full_path)[0] & 0x4000: # Check if it's a directory
if os.stat(full_path)[0] & 0x4000: # Check if it's a directory
fullname = d
if fullname not in seen_fullnames: # Avoid duplicates
seen_fullnames.add(fullname)
@@ -67,3 +67,91 @@ class PackageManager():
# Sort apps alphabetically by app.name
cls.app_list.sort(key=lambda x: x.name.lower()) # Case-insensitive sorting by name
@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
# TODO: also remove it from the app_list
except Exception as e:
print(f"Removing app_folder {app_folder} got error: {e}")
@staticmethod
def install_mpk(temp_zip_path, dest_folder):
try:
# 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...
@staticmethod
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
@staticmethod
def is_builtin_app(app_fullname):
return PackageManager.is_installed_by_path(f"builtin/apps/{app_fullname}")
@staticmethod
def is_overridden_builtin_app(app_fullname):
return PackageManager.is_installed_by_path(f"apps/{app_fullname}") and PackageManager.is_installed_by_path(f"builtin/apps/{app_fullname}")
@staticmethod
def is_update_available(app_fullname, new_version):
appdir = f"apps/{app_fullname}"
builtinappdir = f"builtin/apps/{app_fullname}"
installed_app=None
if PackageManager.is_installed_by_path(appdir):
print(f"{appdir} found, getting version...")
installed_app = mpos.apps.parse_manifest(appdir) # probably no need to re-parse the manifest
elif PackageManager.is_installed_by_path(builtinappdir):
print(f"{builtinappdir} found, getting version...")
installed_app = mpos.apps.parse_manifest(builtinappdir) # probably no need to re-parse the manifest
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 PackageManager.compare_versions(new_version, installed_app.version)
@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...")
return PackageManager.is_installed_by_path(f"apps/{app_fullname}") or PackageManager.is_installed_by_path(f"builtin/apps/{app_fullname}")