diff --git a/src/main/assets/apps/store/store.lua b/src/main/assets/apps/store/store.lua
new file mode 100644
index 0000000..ff579cb
--- /dev/null
+++ b/src/main/assets/apps/store/store.lua
@@ -0,0 +1,394 @@
+-- store.lua - App Store system app logic
+-- Milestone 10: Device-Side App Management
+
+-- State
+local state = {
+ screen = "home", -- home, games, updates, search, detail
+ installed = {}, -- Installed apps from mosis.apps
+ updates = {}, -- Available updates
+ featured = {}, -- Featured apps from store API
+ categories = {}, -- Category list
+ search_query = "", -- Current search
+ selected_app = nil, -- Selected app for detail view
+ is_loading = false,
+ error_message = nil
+}
+
+-- Store API configuration
+local STORE_API = "https://portal.mosis.dev/store"
+
+-- ============================================================================
+-- Initialization
+-- ============================================================================
+
+function init()
+ print("[Store] Initializing...")
+
+ -- Load installed apps
+ refreshInstalledApps()
+
+ -- Check for updates
+ checkForUpdates()
+
+ -- Fetch featured apps (async)
+ fetchFeaturedApps()
+end
+
+function refreshInstalledApps()
+ if mosis and mosis.apps then
+ state.installed = mosis.apps.getInstalled() or {}
+ print("[Store] Loaded " .. #state.installed .. " installed apps")
+ else
+ print("[Store] Warning: mosis.apps API not available")
+ state.installed = {}
+ end
+end
+
+function checkForUpdates()
+ if mosis and mosis.apps then
+ state.updates = mosis.apps.checkUpdates() or {}
+ print("[Store] Found " .. #state.updates .. " updates")
+ updateBadge()
+ end
+end
+
+function updateBadge()
+ -- Update the updates tab badge
+ local badge = document:GetElementById("updates-badge")
+ if badge then
+ if #state.updates > 0 then
+ badge.inner_rml = tostring(#state.updates)
+ badge.style.display = "block"
+ else
+ badge.style.display = "none"
+ end
+ end
+end
+
+-- ============================================================================
+-- API Calls
+-- ============================================================================
+
+function fetchFeaturedApps()
+ state.is_loading = true
+
+ -- TODO: Make HTTP request to STORE_API
+ -- For now, use placeholder data
+ state.featured = {
+ {
+ id = "com.mosis.weather",
+ name = "Weather Pro",
+ category = "Weather",
+ rating = 4.8,
+ downloads = 125000,
+ size = 15728640, -- 15 MB
+ description = "Beautiful forecasts for your virtual world",
+ icon = "W",
+ color = "#2196F3"
+ },
+ {
+ id = "com.mosis.notes",
+ name = "Notes",
+ category = "Productivity",
+ rating = 4.7,
+ downloads = 89000,
+ size = 8388608, -- 8 MB
+ description = "Simple note-taking app",
+ icon = "N",
+ color = "#03DAC6"
+ }
+ }
+
+ state.is_loading = false
+ render()
+end
+
+function searchApps(query)
+ state.search_query = query
+ state.screen = "search"
+
+ if query == "" then
+ state.screen = "home"
+ render()
+ return
+ end
+
+ state.is_loading = true
+ render()
+
+ -- TODO: Make HTTP request to STORE_API/search
+ -- For now, filter featured apps
+ local results = {}
+ local lower_query = query:lower()
+ for _, app in ipairs(state.featured) do
+ if app.name:lower():find(lower_query) or
+ app.category:lower():find(lower_query) then
+ table.insert(results, app)
+ end
+ end
+
+ state.search_results = results
+ state.is_loading = false
+ render()
+end
+
+-- ============================================================================
+-- Installation
+-- ============================================================================
+
+function installApp(app_id, download_url, signature)
+ print("[Store] Installing: " .. app_id)
+
+ showProgress(app_id)
+
+ if mosis and mosis.apps then
+ mosis.apps.install(download_url or "", signature or "", function(progress)
+ updateProgress(progress)
+
+ if progress.stage == "complete" then
+ hideProgress()
+ showToast("App installed successfully!")
+ refreshInstalledApps()
+ render()
+ elseif progress.stage == "failed" then
+ hideProgress()
+ showError("Installation failed: " .. (progress.error or "Unknown error"))
+ end
+ end)
+ else
+ hideProgress()
+ showError("App installation not available")
+ end
+end
+
+function uninstallApp(package_id)
+ print("[Store] Uninstalling: " .. package_id)
+
+ if mosis and mosis.apps then
+ local success = mosis.apps.uninstall(package_id)
+ if success then
+ showToast("App uninstalled")
+ refreshInstalledApps()
+ render()
+ else
+ showError("Failed to uninstall app")
+ end
+ end
+end
+
+function openApp(package_id)
+ print("[Store] Launching: " .. package_id)
+
+ if mosis and mosis.apps then
+ mosis.apps.launch(package_id)
+ end
+end
+
+function updateApp(package_id)
+ print("[Store] Updating: " .. package_id)
+
+ -- Find update info
+ for _, update in ipairs(state.updates) do
+ if update.package_id == package_id then
+ installApp(package_id, update.download_url, update.signature)
+ return
+ end
+ end
+
+ showError("No update available for this app")
+end
+
+function updateAllApps()
+ print("[Store] Updating all apps...")
+
+ for _, update in ipairs(state.updates) do
+ -- Queue updates (in a real implementation, this would be sequential)
+ installApp(update.package_id, update.download_url, update.signature)
+ end
+end
+
+-- ============================================================================
+-- UI Helpers
+-- ============================================================================
+
+function isInstalled(package_id)
+ for _, app in ipairs(state.installed) do
+ if app.package_id == package_id then
+ return true
+ end
+ end
+ return false
+end
+
+function hasUpdate(package_id)
+ for _, update in ipairs(state.updates) do
+ if update.package_id == package_id then
+ return true
+ end
+ end
+ return false
+end
+
+function formatSize(bytes)
+ if bytes >= 1048576 then
+ return string.format("%.1f MB", bytes / 1048576)
+ elseif bytes >= 1024 then
+ return string.format("%.0f KB", bytes / 1024)
+ else
+ return bytes .. " B"
+ end
+end
+
+function formatDownloads(count)
+ if count >= 1000000 then
+ return string.format("%.1fM", count / 1000000)
+ elseif count >= 1000 then
+ return string.format("%.0fK", count / 1000)
+ else
+ return tostring(count)
+ end
+end
+
+-- ============================================================================
+-- Progress Dialog
+-- ============================================================================
+
+function showProgress(app_name)
+ local dialog = document:GetElementById("progress-dialog")
+ if dialog then
+ dialog.style.display = "flex"
+ local title = document:GetElementById("progress-title")
+ if title then
+ title.inner_rml = "Installing " .. (app_name or "App")
+ end
+ end
+end
+
+function updateProgress(progress)
+ local bar = document:GetElementById("progress-bar")
+ if bar then
+ bar.style.width = (progress.progress * 100) .. "%"
+ end
+
+ local status = document:GetElementById("progress-status")
+ if status then
+ local stage_names = {
+ downloading = "Downloading...",
+ verifying = "Verifying...",
+ extracting = "Extracting...",
+ registering = "Registering...",
+ complete = "Complete!",
+ failed = "Failed"
+ }
+ status.inner_rml = stage_names[progress.stage] or progress.stage
+ end
+end
+
+function hideProgress()
+ local dialog = document:GetElementById("progress-dialog")
+ if dialog then
+ dialog.style.display = "none"
+ end
+end
+
+-- ============================================================================
+-- Toast/Error Messages
+-- ============================================================================
+
+function showToast(message)
+ local toast = document:GetElementById("toast")
+ if toast then
+ toast.inner_rml = message
+ toast.style.display = "block"
+ -- Auto-hide after 3 seconds (would need timer API)
+ end
+ print("[Store] Toast: " .. message)
+end
+
+function showError(message)
+ state.error_message = message
+ local error_el = document:GetElementById("error-dialog")
+ if error_el then
+ local msg = document:GetElementById("error-message")
+ if msg then
+ msg.inner_rml = message
+ end
+ error_el.style.display = "flex"
+ end
+ print("[Store] Error: " .. message)
+end
+
+function hideError()
+ state.error_message = nil
+ local error_el = document:GetElementById("error-dialog")
+ if error_el then
+ error_el.style.display = "none"
+ end
+end
+
+-- ============================================================================
+-- Navigation
+-- ============================================================================
+
+function showHome()
+ state.screen = "home"
+ setActiveTab("apps")
+ render()
+end
+
+function showGames()
+ state.screen = "games"
+ setActiveTab("games")
+ render()
+end
+
+function showUpdates()
+ state.screen = "updates"
+ setActiveTab("updates")
+ checkForUpdates()
+ render()
+end
+
+function showSearch()
+ state.screen = "search"
+ render()
+end
+
+function showAppDetail(app_id)
+ state.screen = "detail"
+ -- Find app in featured or installed
+ for _, app in ipairs(state.featured) do
+ if app.id == app_id then
+ state.selected_app = app
+ break
+ end
+ end
+ render()
+end
+
+function setActiveTab(tab)
+ local tabs = {"apps", "games", "updates"}
+ for _, t in ipairs(tabs) do
+ local el = document:GetElementById("nav-" .. t)
+ if el then
+ if t == tab then
+ el:SetClass("active", true)
+ else
+ el:SetClass("active", false)
+ end
+ end
+ end
+end
+
+-- ============================================================================
+-- Rendering
+-- ============================================================================
+
+function render()
+ -- The RML is mostly static with dynamic data binding
+ -- In a full implementation, we'd update innerHTML of content areas
+ print("[Store] Rendering screen: " .. state.screen)
+end
+
+-- Initialize on load
+init()
diff --git a/src/main/assets/apps/store/store.rml b/src/main/assets/apps/store/store.rml
index 8ac8135..7b99c20 100644
--- a/src/main/assets/apps/store/store.rml
+++ b/src/main/assets/apps/store/store.rml
@@ -4,6 +4,7 @@
+
Store
@@ -471,18 +587,44 @@
-
+
Apps
-
+
Games
-
+
Updates
+
+
+
+
+
+
Installing...
+
+
Preparing...
+
+
+
+
+
+
+
+
diff --git a/src/main/cpp/apps/app_api.cpp b/src/main/cpp/apps/app_api.cpp
new file mode 100644
index 0000000..0527656
--- /dev/null
+++ b/src/main/cpp/apps/app_api.cpp
@@ -0,0 +1,539 @@
+// app_api.cpp - Lua API bindings for app management implementation
+// Milestone 10: Device-Side App Management
+
+#include "app_api.h"
+#include "app_manager.h"
+#include "update_service.h"
+#include "../logger.h"
+
+#include
+#include
+#include
+
+namespace mosis {
+
+// Registry keys for storing pointers
+static const char* APP_MANAGER_KEY = "mosis.app_manager";
+static const char* UPDATE_SERVICE_KEY = "mosis.update_service";
+static const char* CURRENT_APP_ID_KEY = "mosis.current_app_id";
+static const char* IS_SYSTEM_APP_KEY = "mosis.is_system_app";
+
+// Helper to get AppManager from Lua registry
+static AppManager* GetAppManager(lua_State* L) {
+ lua_getfield(L, LUA_REGISTRYINDEX, APP_MANAGER_KEY);
+ auto* manager = static_cast(lua_touserdata(L, -1));
+ lua_pop(L, 1);
+ return manager;
+}
+
+// Helper to get UpdateService from Lua registry
+static UpdateService* GetUpdateService(lua_State* L) {
+ lua_getfield(L, LUA_REGISTRYINDEX, UPDATE_SERVICE_KEY);
+ auto* service = static_cast(lua_touserdata(L, -1));
+ lua_pop(L, 1);
+ return service;
+}
+
+// Helper to get current app ID
+static std::string GetCurrentAppId(lua_State* L) {
+ lua_getfield(L, LUA_REGISTRYINDEX, CURRENT_APP_ID_KEY);
+ std::string id = lua_tostring(L, -1) ? lua_tostring(L, -1) : "";
+ lua_pop(L, 1);
+ return id;
+}
+
+// Helper to check if system app
+static bool IsSystemApp(lua_State* L) {
+ lua_getfield(L, LUA_REGISTRYINDEX, IS_SYSTEM_APP_KEY);
+ bool is_system = lua_toboolean(L, -1);
+ lua_pop(L, 1);
+ return is_system;
+}
+
+// ============================================================================
+// mosis.apps.* - System apps only
+// ============================================================================
+
+// mosis.apps.getInstalled() -> [{package_id, name, version_name, version_code, installed_at}]
+static int apps_getInstalled(lua_State* L) {
+ if (!IsSystemApp(L)) {
+ return luaL_error(L, "mosis.apps.getInstalled requires system permission");
+ }
+
+ auto* manager = GetAppManager(L);
+ if (!manager) {
+ lua_newtable(L);
+ return 1;
+ }
+
+ auto apps = manager->GetInstalledApps();
+
+ lua_createtable(L, static_cast(apps.size()), 0);
+ int idx = 1;
+
+ for (const auto& app : apps) {
+ lua_createtable(L, 0, 8);
+
+ lua_pushstring(L, app.package_id.c_str());
+ lua_setfield(L, -2, "package_id");
+
+ lua_pushstring(L, app.name.c_str());
+ lua_setfield(L, -2, "name");
+
+ lua_pushstring(L, app.version_name.c_str());
+ lua_setfield(L, -2, "version_name");
+
+ lua_pushinteger(L, app.version_code);
+ lua_setfield(L, -2, "version_code");
+
+ lua_pushboolean(L, app.is_system_app);
+ lua_setfield(L, -2, "is_system_app");
+
+ lua_pushstring(L, app.icon_path.c_str());
+ lua_setfield(L, -2, "icon");
+
+ lua_pushstring(L, app.developer_name.c_str());
+ lua_setfield(L, -2, "developer");
+
+ // installed_at as Unix timestamp
+ auto ts = std::chrono::duration_cast(
+ app.installed_at.time_since_epoch()).count();
+ lua_pushinteger(L, ts);
+ lua_setfield(L, -2, "installed_at");
+
+ lua_rawseti(L, -2, idx++);
+ }
+
+ return 1;
+}
+
+// mosis.apps.getInfo(package_id) -> {info} or nil
+static int apps_getInfo(lua_State* L) {
+ if (!IsSystemApp(L)) {
+ return luaL_error(L, "mosis.apps.getInfo requires system permission");
+ }
+
+ const char* package_id = luaL_checkstring(L, 1);
+
+ auto* manager = GetAppManager(L);
+ if (!manager) {
+ lua_pushnil(L);
+ return 1;
+ }
+
+ auto app = manager->GetApp(package_id);
+ if (!app) {
+ lua_pushnil(L);
+ return 1;
+ }
+
+ lua_createtable(L, 0, 12);
+
+ lua_pushstring(L, app->package_id.c_str());
+ lua_setfield(L, -2, "package_id");
+
+ lua_pushstring(L, app->name.c_str());
+ lua_setfield(L, -2, "name");
+
+ lua_pushstring(L, app->version_name.c_str());
+ lua_setfield(L, -2, "version_name");
+
+ lua_pushinteger(L, app->version_code);
+ lua_setfield(L, -2, "version_code");
+
+ lua_pushboolean(L, app->is_system_app);
+ lua_setfield(L, -2, "is_system_app");
+
+ lua_pushinteger(L, app->package_size);
+ lua_setfield(L, -2, "package_size");
+
+ lua_pushinteger(L, app->data_size);
+ lua_setfield(L, -2, "data_size");
+
+ lua_pushstring(L, app->icon_path.c_str());
+ lua_setfield(L, -2, "icon");
+
+ lua_pushstring(L, app->developer_name.c_str());
+ lua_setfield(L, -2, "developer");
+
+ // Permissions array
+ lua_createtable(L, static_cast(app->permissions.size()), 0);
+ int idx = 1;
+ for (const auto& perm : app->permissions) {
+ lua_pushstring(L, perm.c_str());
+ lua_rawseti(L, -2, idx++);
+ }
+ lua_setfield(L, -2, "permissions");
+
+ return 1;
+}
+
+// mosis.apps.install(url, signature, callback)
+static int apps_install(lua_State* L) {
+ if (!IsSystemApp(L)) {
+ return luaL_error(L, "mosis.apps.install requires system permission");
+ }
+
+ const char* url = luaL_checkstring(L, 1);
+ const char* signature = lua_isstring(L, 2) ? lua_tostring(L, 2) : "";
+
+ // Callback is optional (argument 3)
+ bool has_callback = lua_isfunction(L, 3);
+
+ auto* manager = GetAppManager(L);
+ if (!manager) {
+ lua_pushboolean(L, false);
+ return 1;
+ }
+
+ if (has_callback) {
+ // Store callback reference
+ lua_pushvalue(L, 3);
+ int callback_ref = luaL_ref(L, LUA_REGISTRYINDEX);
+
+ // Create progress callback that calls Lua
+ // Note: This is simplified - real implementation needs thread safety
+ ProgressCallback progress_cb = [L, callback_ref](const InstallProgress& progress) {
+ lua_rawgeti(L, LUA_REGISTRYINDEX, callback_ref);
+
+ lua_createtable(L, 0, 3);
+
+ lua_pushstring(L, InstallProgress::StageName(progress.stage));
+ lua_setfield(L, -2, "stage");
+
+ lua_pushnumber(L, progress.progress);
+ lua_setfield(L, -2, "progress");
+
+ lua_pushstring(L, progress.error.c_str());
+ lua_setfield(L, -2, "error");
+
+ if (lua_pcall(L, 1, 0, 0) != LUA_OK) {
+ LOG_ERROR("Install callback error: %s", lua_tostring(L, -1));
+ lua_pop(L, 1);
+ }
+
+ // Clean up ref when complete
+ if (progress.stage == InstallProgress::Stage::Complete ||
+ progress.stage == InstallProgress::Stage::Failed) {
+ luaL_unref(L, LUA_REGISTRYINDEX, callback_ref);
+ }
+ };
+
+ bool success = manager->Install(url, signature, progress_cb);
+ lua_pushboolean(L, success);
+ } else {
+ bool success = manager->Install(url, signature, nullptr);
+ lua_pushboolean(L, success);
+ }
+
+ return 1;
+}
+
+// mosis.apps.uninstall(package_id) -> boolean
+static int apps_uninstall(lua_State* L) {
+ if (!IsSystemApp(L)) {
+ return luaL_error(L, "mosis.apps.uninstall requires system permission");
+ }
+
+ const char* package_id = luaL_checkstring(L, 1);
+
+ auto* manager = GetAppManager(L);
+ if (!manager) {
+ lua_pushboolean(L, false);
+ return 1;
+ }
+
+ bool success = manager->Uninstall(package_id);
+ lua_pushboolean(L, success);
+ return 1;
+}
+
+// mosis.apps.launch(package_id) -> boolean
+static int apps_launch(lua_State* L) {
+ if (!IsSystemApp(L)) {
+ return luaL_error(L, "mosis.apps.launch requires system permission");
+ }
+
+ const char* package_id = luaL_checkstring(L, 1);
+
+ auto* manager = GetAppManager(L);
+ if (!manager) {
+ lua_pushboolean(L, false);
+ return 1;
+ }
+
+ bool success = manager->LaunchApp(package_id);
+ lua_pushboolean(L, success);
+ return 1;
+}
+
+// mosis.apps.getDataSize(package_id) -> number
+static int apps_getDataSize(lua_State* L) {
+ if (!IsSystemApp(L)) {
+ return luaL_error(L, "mosis.apps.getDataSize requires system permission");
+ }
+
+ const char* package_id = luaL_checkstring(L, 1);
+
+ auto* manager = GetAppManager(L);
+ if (!manager) {
+ lua_pushinteger(L, 0);
+ return 1;
+ }
+
+ int64_t size = manager->GetAppDataSize(package_id);
+ lua_pushinteger(L, size);
+ return 1;
+}
+
+// mosis.apps.clearCache(package_id) -> boolean
+static int apps_clearCache(lua_State* L) {
+ if (!IsSystemApp(L)) {
+ return luaL_error(L, "mosis.apps.clearCache requires system permission");
+ }
+
+ const char* package_id = luaL_checkstring(L, 1);
+
+ auto* manager = GetAppManager(L);
+ if (!manager) {
+ lua_pushboolean(L, false);
+ return 1;
+ }
+
+ bool success = manager->ClearAppCache(package_id);
+ lua_pushboolean(L, success);
+ return 1;
+}
+
+// mosis.apps.clearData(package_id) -> boolean
+static int apps_clearData(lua_State* L) {
+ if (!IsSystemApp(L)) {
+ return luaL_error(L, "mosis.apps.clearData requires system permission");
+ }
+
+ const char* package_id = luaL_checkstring(L, 1);
+
+ auto* manager = GetAppManager(L);
+ if (!manager) {
+ lua_pushboolean(L, false);
+ return 1;
+ }
+
+ bool success = manager->ClearAppData(package_id);
+ lua_pushboolean(L, success);
+ return 1;
+}
+
+// mosis.apps.checkUpdates(callback) -> void
+static int apps_checkUpdates(lua_State* L) {
+ if (!IsSystemApp(L)) {
+ return luaL_error(L, "mosis.apps.checkUpdates requires system permission");
+ }
+
+ auto* service = GetUpdateService(L);
+ if (!service) {
+ lua_pushnil(L);
+ return 1;
+ }
+
+ auto updates = service->CheckForUpdates();
+
+ lua_createtable(L, static_cast(updates.size()), 0);
+ int idx = 1;
+
+ for (const auto& update : updates) {
+ lua_createtable(L, 0, 8);
+
+ lua_pushstring(L, update.package_id.c_str());
+ lua_setfield(L, -2, "package_id");
+
+ lua_pushstring(L, update.name.c_str());
+ lua_setfield(L, -2, "name");
+
+ lua_pushstring(L, update.current_version.c_str());
+ lua_setfield(L, -2, "current_version");
+
+ lua_pushstring(L, update.new_version.c_str());
+ lua_setfield(L, -2, "new_version");
+
+ lua_pushinteger(L, update.download_size);
+ lua_setfield(L, -2, "size");
+
+ lua_pushstring(L, update.release_notes.c_str());
+ lua_setfield(L, -2, "release_notes");
+
+ lua_pushboolean(L, update.is_critical);
+ lua_setfield(L, -2, "critical");
+
+ lua_rawseti(L, -2, idx++);
+ }
+
+ return 1;
+}
+
+// ============================================================================
+// mosis.app.* - All apps (info about current app)
+// ============================================================================
+
+// mosis.app.info() -> {package_id, name, version_name, version_code}
+static int app_info(lua_State* L) {
+ std::string app_id = GetCurrentAppId(L);
+ if (app_id.empty()) {
+ lua_pushnil(L);
+ return 1;
+ }
+
+ auto* manager = GetAppManager(L);
+ if (!manager) {
+ lua_pushnil(L);
+ return 1;
+ }
+
+ auto app = manager->GetApp(app_id);
+ if (!app) {
+ lua_pushnil(L);
+ return 1;
+ }
+
+ lua_createtable(L, 0, 4);
+
+ lua_pushstring(L, app->package_id.c_str());
+ lua_setfield(L, -2, "package_id");
+
+ lua_pushstring(L, app->name.c_str());
+ lua_setfield(L, -2, "name");
+
+ lua_pushstring(L, app->version_name.c_str());
+ lua_setfield(L, -2, "version_name");
+
+ lua_pushinteger(L, app->version_code);
+ lua_setfield(L, -2, "version_code");
+
+ return 1;
+}
+
+// mosis.app.checkUpdate(callback) -> void
+static int app_checkUpdate(lua_State* L) {
+ // Get callback
+ if (!lua_isfunction(L, 1)) {
+ return luaL_error(L, "callback function required");
+ }
+
+ std::string app_id = GetCurrentAppId(L);
+ auto* service = GetUpdateService(L);
+
+ if (app_id.empty() || !service) {
+ // Call callback with no update
+ lua_pushvalue(L, 1);
+ lua_pushboolean(L, false);
+ lua_pushnil(L);
+ lua_pcall(L, 2, 0, 0);
+ return 0;
+ }
+
+ auto updates = service->GetPendingUpdates();
+ for (const auto& update : updates) {
+ if (update.package_id == app_id) {
+ // Call callback with update info
+ lua_pushvalue(L, 1);
+ lua_pushboolean(L, true);
+ lua_pushstring(L, update.new_version.c_str());
+ lua_pcall(L, 2, 0, 0);
+ return 0;
+ }
+ }
+
+ // No update available
+ lua_pushvalue(L, 1);
+ lua_pushboolean(L, false);
+ lua_pushnil(L);
+ lua_pcall(L, 2, 0, 0);
+
+ return 0;
+}
+
+// mosis.app.openStorePage() -> void
+static int app_openStorePage(lua_State* L) {
+ std::string app_id = GetCurrentAppId(L);
+ if (app_id.empty()) {
+ return 0;
+ }
+
+ // TODO: Navigate to store page for this app
+ // This would typically trigger navigation to:
+ // mosis://store/app/{app_id}
+ LOG_INFO("Open store page for: %s", app_id.c_str());
+
+ return 0;
+}
+
+// ============================================================================
+// Registration
+// ============================================================================
+
+static const luaL_Reg apps_functions[] = {
+ {"getInstalled", apps_getInstalled},
+ {"getInfo", apps_getInfo},
+ {"install", apps_install},
+ {"uninstall", apps_uninstall},
+ {"launch", apps_launch},
+ {"getDataSize", apps_getDataSize},
+ {"clearCache", apps_clearCache},
+ {"clearData", apps_clearData},
+ {"checkUpdates", apps_checkUpdates},
+ {nullptr, nullptr}
+};
+
+static const luaL_Reg app_functions[] = {
+ {"info", app_info},
+ {"checkUpdate", app_checkUpdate},
+ {"openStorePage", app_openStorePage},
+ {nullptr, nullptr}
+};
+
+void RegisterAppAPIs(lua_State* L,
+ AppManager* app_manager,
+ UpdateService* update_service,
+ const std::string& current_app_id,
+ bool is_system_app) {
+ // Store pointers in registry
+ lua_pushlightuserdata(L, app_manager);
+ lua_setfield(L, LUA_REGISTRYINDEX, APP_MANAGER_KEY);
+
+ lua_pushlightuserdata(L, update_service);
+ lua_setfield(L, LUA_REGISTRYINDEX, UPDATE_SERVICE_KEY);
+
+ lua_pushstring(L, current_app_id.c_str());
+ lua_setfield(L, LUA_REGISTRYINDEX, CURRENT_APP_ID_KEY);
+
+ lua_pushboolean(L, is_system_app);
+ lua_setfield(L, LUA_REGISTRYINDEX, IS_SYSTEM_APP_KEY);
+
+ // Get or create mosis table
+ lua_getglobal(L, "mosis");
+ if (lua_isnil(L, -1)) {
+ lua_pop(L, 1);
+ lua_newtable(L);
+ lua_setglobal(L, "mosis");
+ lua_getglobal(L, "mosis");
+ }
+
+ // Create mosis.apps table (system apps only)
+ if (is_system_app) {
+ lua_newtable(L);
+ luaL_setfuncs(L, apps_functions, 0);
+ lua_setfield(L, -2, "apps");
+ }
+
+ // Create mosis.app table (all apps)
+ lua_newtable(L);
+ luaL_setfuncs(L, app_functions, 0);
+ lua_setfield(L, -2, "app");
+
+ lua_pop(L, 1); // pop mosis table
+
+ LOG_DEBUG("Registered app APIs for: %s (system=%d)",
+ current_app_id.c_str(), is_system_app);
+}
+
+} // namespace mosis
diff --git a/src/main/cpp/apps/app_api.h b/src/main/cpp/apps/app_api.h
new file mode 100644
index 0000000..17dc7db
--- /dev/null
+++ b/src/main/cpp/apps/app_api.h
@@ -0,0 +1,21 @@
+// app_api.h - Lua API bindings for app management
+// Milestone 10: Device-Side App Management
+#pragma once
+
+struct lua_State;
+
+namespace mosis {
+
+class AppManager;
+class UpdateService;
+
+// Register Lua APIs for app management
+// - mosis.apps.* - System apps only (App Store, Settings)
+// - mosis.app.* - All apps (info about current app)
+void RegisterAppAPIs(lua_State* L,
+ AppManager* app_manager,
+ UpdateService* update_service,
+ const std::string& current_app_id,
+ bool is_system_app);
+
+} // namespace mosis
diff --git a/src/main/cpp/apps/app_manager.cpp b/src/main/cpp/apps/app_manager.cpp
new file mode 100644
index 0000000..25e1b7d
--- /dev/null
+++ b/src/main/cpp/apps/app_manager.cpp
@@ -0,0 +1,680 @@
+// app_manager.cpp - App installation and management implementation
+// Milestone 10: Device-Side App Management
+
+#include "app_manager.h"
+#include "../sandbox/sandbox_manager.h"
+#include "../logger.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+// For JSON parsing
+#include
+
+// For ZIP extraction
+#include
+
+namespace fs = std::filesystem;
+using json = nlohmann::json;
+
+namespace mosis {
+
+AppManager::AppManager(const std::string& data_root)
+ : m_data_root(data_root)
+{
+ // Create directory structure
+ fs::create_directories(m_data_root + "/apps");
+ fs::create_directories(m_data_root + "/downloads");
+ fs::create_directories(m_data_root + "/backups");
+ fs::create_directories(m_data_root + "/config");
+
+ // Load installed apps registry
+ LoadInstalledApps();
+
+ LOG_INFO("AppManager initialized at: %s", m_data_root.c_str());
+}
+
+AppManager::~AppManager() {
+ SaveInstalledApps();
+}
+
+bool AppManager::Install(const std::string& package_url,
+ const std::string& signature,
+ ProgressCallback callback) {
+ callback({InstallProgress::Stage::Downloading, 0.0f, ""});
+
+ // Generate download path
+ std::string download_path = m_data_root + "/downloads/" + GenerateUUID() + ".mosis";
+
+ // Download package
+ if (!DownloadFile(package_url, download_path, [&](float p) {
+ callback({InstallProgress::Stage::Downloading, p, ""});
+ })) {
+ callback({InstallProgress::Stage::Failed, 0.0f, "Download failed"});
+ return false;
+ }
+
+ callback({InstallProgress::Stage::Verifying, 0.0f, ""});
+
+ // Verify signature
+ if (!signature.empty() && !VerifySignature(download_path, signature)) {
+ fs::remove(download_path);
+ callback({InstallProgress::Stage::Failed, 0.0f, "Signature verification failed"});
+ return false;
+ }
+
+ // Verify package integrity
+ if (!VerifyPackage(download_path)) {
+ fs::remove(download_path);
+ callback({InstallProgress::Stage::Failed, 0.0f, "Package verification failed"});
+ return false;
+ }
+
+ // Continue with installation
+ bool result = InstallFromFile(download_path, callback);
+
+ // Clean up download
+ fs::remove(download_path);
+
+ return result;
+}
+
+bool AppManager::InstallFromFile(const std::string& package_path,
+ ProgressCallback callback) {
+ callback({InstallProgress::Stage::Verifying, 0.0f, ""});
+
+ // Extract manifest to get package_id
+ auto manifest = ExtractManifest(package_path);
+ if (!manifest) {
+ callback({InstallProgress::Stage::Failed, 0.0f, "Invalid manifest"});
+ return false;
+ }
+
+ LOG_INFO("Installing app: %s v%s", manifest->id.c_str(), manifest->version.c_str());
+
+ callback({InstallProgress::Stage::Extracting, 0.0f, ""});
+
+ // Determine installation path
+ std::string install_path = m_data_root + "/apps/" + manifest->id;
+
+ // Check if already installed (update path)
+ if (fs::exists(install_path + "/package")) {
+ LOG_INFO("App already installed, updating: %s", manifest->id.c_str());
+ // Backup existing data
+ BackupAppData(manifest->id);
+ // Remove old package
+ fs::remove_all(install_path + "/package");
+ }
+
+ // Create directories
+ fs::create_directories(install_path + "/package");
+ fs::create_directories(install_path + "/data");
+ fs::create_directories(install_path + "/cache");
+ fs::create_directories(install_path + "/db");
+
+ // Extract package
+ if (!ExtractPackage(package_path, install_path + "/package")) {
+ callback({InstallProgress::Stage::Failed, 0.0f, "Extraction failed"});
+ return false;
+ }
+
+ callback({InstallProgress::Stage::Registering, 0.0f, ""});
+
+ // Get package file size
+ int64_t package_size = 0;
+ try {
+ package_size = static_cast(fs::file_size(package_path));
+ } catch (...) {
+ package_size = 0;
+ }
+
+ // Register app
+ InstalledApp app;
+ app.package_id = manifest->id;
+ app.name = manifest->name;
+ app.version_name = manifest->version;
+ app.version_code = manifest->version_code;
+ app.install_path = install_path;
+ app.permissions = manifest->permissions;
+ app.installed_at = std::chrono::system_clock::now();
+ app.updated_at = std::chrono::system_clock::now();
+ app.package_size = package_size;
+ app.data_size = 0;
+ app.is_system_app = false;
+ app.entry_point = manifest->entry;
+ app.icon_path = manifest->icon;
+ app.developer_name = manifest->developer_name;
+
+ {
+ std::lock_guard lock(m_mutex);
+ m_installed_apps[manifest->id] = app;
+ SaveInstalledApps();
+ }
+
+ LOG_INFO("App installed successfully: %s", manifest->id.c_str());
+ callback({InstallProgress::Stage::Complete, 1.0f, ""});
+ return true;
+}
+
+bool AppManager::Uninstall(const std::string& package_id, bool keep_data) {
+ std::lock_guard lock(m_mutex);
+
+ auto it = m_installed_apps.find(package_id);
+ if (it == m_installed_apps.end()) {
+ LOG_WARN("Cannot uninstall: app not found: %s", package_id.c_str());
+ return false;
+ }
+
+ // Cannot uninstall system apps
+ if (it->second.is_system_app) {
+ LOG_WARN("Cannot uninstall system app: %s", package_id.c_str());
+ return false;
+ }
+
+ LOG_INFO("Uninstalling app: %s (keep_data=%d)", package_id.c_str(), keep_data);
+
+ // Stop app if running
+ if (m_sandbox_manager && m_sandbox_manager->IsAppRunning(package_id)) {
+ m_sandbox_manager->StopApp(package_id);
+ }
+
+ // Remove files
+ std::string install_path = it->second.install_path;
+
+ try {
+ fs::remove_all(install_path + "/package");
+ fs::remove_all(install_path + "/cache");
+
+ if (!keep_data) {
+ fs::remove_all(install_path + "/data");
+ fs::remove_all(install_path + "/db");
+ fs::remove_all(install_path);
+ }
+ } catch (const std::exception& e) {
+ LOG_ERROR("Error removing app files: %s", e.what());
+ return false;
+ }
+
+ // Unregister
+ m_installed_apps.erase(it);
+ SaveInstalledApps();
+
+ LOG_INFO("App uninstalled: %s", package_id.c_str());
+ return true;
+}
+
+bool AppManager::Update(const std::string& package_id,
+ const std::string& package_url,
+ const std::string& signature,
+ ProgressCallback callback) {
+ // Updates use the same flow as Install, which handles existing installations
+ return Install(package_url, signature, callback);
+}
+
+std::vector AppManager::GetInstalledApps() const {
+ std::lock_guard lock(m_mutex);
+
+ std::vector apps;
+ apps.reserve(m_installed_apps.size());
+ for (const auto& [id, app] : m_installed_apps) {
+ apps.push_back(app);
+ }
+ return apps;
+}
+
+std::optional AppManager::GetApp(const std::string& package_id) const {
+ std::lock_guard lock(m_mutex);
+
+ auto it = m_installed_apps.find(package_id);
+ if (it != m_installed_apps.end()) {
+ return it->second;
+ }
+ return std::nullopt;
+}
+
+bool AppManager::IsInstalled(const std::string& package_id) const {
+ std::lock_guard lock(m_mutex);
+ return m_installed_apps.find(package_id) != m_installed_apps.end();
+}
+
+int64_t AppManager::GetAppDataSize(const std::string& package_id) const {
+ std::string data_path = GetAppDataPath(package_id);
+ return CalculateDirectorySize(data_path);
+}
+
+bool AppManager::ClearAppData(const std::string& package_id) {
+ std::string data_path = GetAppDataPath(package_id);
+ std::string db_path = m_data_root + "/apps/" + package_id + "/db";
+
+ try {
+ fs::remove_all(data_path);
+ fs::remove_all(db_path);
+ fs::create_directories(data_path);
+ fs::create_directories(db_path);
+ LOG_INFO("Cleared app data: %s", package_id.c_str());
+ return true;
+ } catch (const std::exception& e) {
+ LOG_ERROR("Error clearing app data: %s", e.what());
+ return false;
+ }
+}
+
+bool AppManager::ClearAppCache(const std::string& package_id) {
+ std::string cache_path = GetAppCachePath(package_id);
+
+ try {
+ fs::remove_all(cache_path);
+ fs::create_directories(cache_path);
+ LOG_INFO("Cleared app cache: %s", package_id.c_str());
+ return true;
+ } catch (const std::exception& e) {
+ LOG_ERROR("Error clearing app cache: %s", e.what());
+ return false;
+ }
+}
+
+bool AppManager::BackupAppData(const std::string& package_id) {
+ std::string data_path = GetAppDataPath(package_id);
+ std::string backup_path = m_data_root + "/backups/" + package_id;
+
+ try {
+ if (fs::exists(data_path)) {
+ fs::remove_all(backup_path);
+ fs::copy(data_path, backup_path, fs::copy_options::recursive);
+ LOG_INFO("Backed up app data: %s", package_id.c_str());
+ }
+ return true;
+ } catch (const std::exception& e) {
+ LOG_ERROR("Error backing up app data: %s", e.what());
+ return false;
+ }
+}
+
+bool AppManager::RestoreAppData(const std::string& package_id) {
+ std::string data_path = GetAppDataPath(package_id);
+ std::string backup_path = m_data_root + "/backups/" + package_id;
+
+ try {
+ if (fs::exists(backup_path)) {
+ fs::remove_all(data_path);
+ fs::copy(backup_path, data_path, fs::copy_options::recursive);
+ LOG_INFO("Restored app data: %s", package_id.c_str());
+ }
+ return true;
+ } catch (const std::exception& e) {
+ LOG_ERROR("Error restoring app data: %s", e.what());
+ return false;
+ }
+}
+
+bool AppManager::LaunchApp(const std::string& package_id) {
+ if (!m_sandbox_manager) {
+ LOG_ERROR("Cannot launch app: sandbox manager not set");
+ return false;
+ }
+
+ auto app = GetApp(package_id);
+ if (!app) {
+ LOG_ERROR("Cannot launch app: not installed: %s", package_id.c_str());
+ return false;
+ }
+
+ std::string app_path = app->install_path + "/package";
+ return m_sandbox_manager->StartApp(package_id, app_path, app->permissions, app->is_system_app);
+}
+
+bool AppManager::StopApp(const std::string& package_id) {
+ if (!m_sandbox_manager) {
+ return false;
+ }
+ return m_sandbox_manager->StopApp(package_id);
+}
+
+bool AppManager::IsAppRunning(const std::string& package_id) const {
+ if (!m_sandbox_manager) {
+ return false;
+ }
+ return m_sandbox_manager->IsAppRunning(package_id);
+}
+
+void AppManager::SetSandboxManager(LuaSandboxManager* manager) {
+ m_sandbox_manager = manager;
+}
+
+std::string AppManager::GetAppPath(const std::string& package_id) const {
+ return m_data_root + "/apps/" + package_id + "/package";
+}
+
+std::string AppManager::GetAppDataPath(const std::string& package_id) const {
+ return m_data_root + "/apps/" + package_id + "/data";
+}
+
+std::string AppManager::GetAppCachePath(const std::string& package_id) const {
+ return m_data_root + "/apps/" + package_id + "/cache";
+}
+
+void AppManager::RegisterSystemApp(const InstalledApp& app) {
+ std::lock_guard lock(m_mutex);
+
+ InstalledApp system_app = app;
+ system_app.is_system_app = true;
+ m_installed_apps[app.package_id] = system_app;
+
+ LOG_INFO("Registered system app: %s", app.package_id.c_str());
+}
+
+bool AppManager::VerifyPackage(const std::string& path) {
+ // Verify ZIP structure and manifest presence
+ unzFile zip = unzOpen(path.c_str());
+ if (!zip) {
+ LOG_ERROR("Cannot open package: %s", path.c_str());
+ return false;
+ }
+
+ bool has_manifest = false;
+
+ if (unzGoToFirstFile(zip) == UNZ_OK) {
+ do {
+ char filename[256];
+ unz_file_info file_info;
+ if (unzGetCurrentFileInfo(zip, &file_info, filename, sizeof(filename),
+ nullptr, 0, nullptr, 0) == UNZ_OK) {
+ if (std::string(filename) == "manifest.json") {
+ has_manifest = true;
+ break;
+ }
+ }
+ } while (unzGoToNextFile(zip) == UNZ_OK);
+ }
+
+ unzClose(zip);
+
+ if (!has_manifest) {
+ LOG_ERROR("Package missing manifest.json: %s", path.c_str());
+ return false;
+ }
+
+ return true;
+}
+
+bool AppManager::VerifySignature(const std::string& path, const std::string& signature) {
+ // TODO: Implement Ed25519 signature verification
+ // For now, accept packages without strict verification
+ LOG_WARN("Signature verification not yet implemented");
+ return true;
+}
+
+std::optional AppManager::ExtractManifest(const std::string& package_path) {
+ unzFile zip = unzOpen(package_path.c_str());
+ if (!zip) {
+ return std::nullopt;
+ }
+
+ std::string manifest_content;
+
+ // Find and read manifest.json
+ if (unzLocateFile(zip, "manifest.json", 0) != UNZ_OK) {
+ unzClose(zip);
+ return std::nullopt;
+ }
+
+ if (unzOpenCurrentFile(zip) != UNZ_OK) {
+ unzClose(zip);
+ return std::nullopt;
+ }
+
+ char buffer[4096];
+ int bytes_read;
+ while ((bytes_read = unzReadCurrentFile(zip, buffer, sizeof(buffer))) > 0) {
+ manifest_content.append(buffer, bytes_read);
+ }
+
+ unzCloseCurrentFile(zip);
+ unzClose(zip);
+
+ // Parse JSON
+ try {
+ json j = json::parse(manifest_content);
+
+ AppManifest manifest;
+ manifest.id = j.value("id", "");
+ manifest.name = j.value("name", "");
+ manifest.version = j.value("version", "1.0.0");
+ manifest.version_code = j.value("version_code", 1);
+ manifest.entry = j.value("entry", "main.rml");
+ manifest.icon = j.value("icon", "");
+ manifest.description = j.value("description", "");
+ manifest.min_api_version = j.value("min_api_version", 1);
+
+ if (j.contains("developer")) {
+ manifest.developer_name = j["developer"].value("name", "");
+ manifest.developer_email = j["developer"].value("email", "");
+ }
+
+ if (j.contains("permissions") && j["permissions"].is_array()) {
+ for (const auto& perm : j["permissions"]) {
+ manifest.permissions.push_back(perm.get());
+ }
+ }
+
+ if (manifest.id.empty()) {
+ LOG_ERROR("Manifest missing required 'id' field");
+ return std::nullopt;
+ }
+
+ return manifest;
+
+ } catch (const json::exception& e) {
+ LOG_ERROR("Failed to parse manifest: %s", e.what());
+ return std::nullopt;
+ }
+}
+
+bool AppManager::ExtractPackage(const std::string& package_path, const std::string& dest_path) {
+ unzFile zip = unzOpen(package_path.c_str());
+ if (!zip) {
+ LOG_ERROR("Cannot open package for extraction: %s", package_path.c_str());
+ return false;
+ }
+
+ bool success = true;
+
+ if (unzGoToFirstFile(zip) == UNZ_OK) {
+ do {
+ char filename[512];
+ unz_file_info file_info;
+
+ if (unzGetCurrentFileInfo(zip, &file_info, filename, sizeof(filename),
+ nullptr, 0, nullptr, 0) != UNZ_OK) {
+ continue;
+ }
+
+ std::string full_path = dest_path + "/" + filename;
+
+ // Skip META-INF directory (signatures)
+ if (std::string(filename).rfind("META-INF/", 0) == 0) {
+ continue;
+ }
+
+ // Create directories
+ size_t len = strlen(filename);
+ if (len > 0 && filename[len - 1] == '/') {
+ fs::create_directories(full_path);
+ continue;
+ }
+
+ // Ensure parent directory exists
+ fs::create_directories(fs::path(full_path).parent_path());
+
+ // Extract file
+ if (unzOpenCurrentFile(zip) != UNZ_OK) {
+ LOG_ERROR("Cannot open file in archive: %s", filename);
+ success = false;
+ break;
+ }
+
+ std::ofstream out(full_path, std::ios::binary);
+ if (!out) {
+ LOG_ERROR("Cannot create file: %s", full_path.c_str());
+ unzCloseCurrentFile(zip);
+ success = false;
+ break;
+ }
+
+ char buffer[8192];
+ int bytes_read;
+ while ((bytes_read = unzReadCurrentFile(zip, buffer, sizeof(buffer))) > 0) {
+ out.write(buffer, bytes_read);
+ }
+
+ out.close();
+ unzCloseCurrentFile(zip);
+
+ } while (unzGoToNextFile(zip) == UNZ_OK);
+ }
+
+ unzClose(zip);
+ return success;
+}
+
+bool AppManager::DownloadFile(const std::string& url, const std::string& dest_path,
+ std::function progress_callback) {
+ // TODO: Implement HTTP download using platform-specific APIs
+ // For now, return false as this is a placeholder
+ LOG_ERROR("HTTP download not yet implemented for: %s", url.c_str());
+ return false;
+}
+
+void AppManager::LoadInstalledApps() {
+ std::string registry_path = m_data_root + "/config/apps.json";
+
+ std::ifstream file(registry_path);
+ if (!file) {
+ LOG_INFO("No existing app registry found");
+ return;
+ }
+
+ try {
+ json j;
+ file >> j;
+
+ if (j.contains("apps") && j["apps"].is_array()) {
+ for (const auto& app_json : j["apps"]) {
+ InstalledApp app;
+ app.package_id = app_json.value("package_id", "");
+ app.name = app_json.value("name", "");
+ app.version_name = app_json.value("version_name", "");
+ app.version_code = app_json.value("version_code", 0);
+ app.install_path = app_json.value("install_path", "");
+ app.package_size = app_json.value("package_size", 0);
+ app.data_size = app_json.value("data_size", 0);
+ app.is_system_app = app_json.value("is_system_app", false);
+ app.entry_point = app_json.value("entry_point", "main.rml");
+ app.icon_path = app_json.value("icon_path", "");
+ app.developer_name = app_json.value("developer_name", "");
+
+ if (app_json.contains("permissions") && app_json["permissions"].is_array()) {
+ for (const auto& perm : app_json["permissions"]) {
+ app.permissions.push_back(perm.get());
+ }
+ }
+
+ // Parse timestamps
+ if (app_json.contains("installed_at")) {
+ auto ts = app_json["installed_at"].get();
+ app.installed_at = std::chrono::system_clock::time_point(
+ std::chrono::seconds(ts));
+ }
+ if (app_json.contains("updated_at")) {
+ auto ts = app_json["updated_at"].get();
+ app.updated_at = std::chrono::system_clock::time_point(
+ std::chrono::seconds(ts));
+ }
+
+ if (!app.package_id.empty()) {
+ m_installed_apps[app.package_id] = app;
+ }
+ }
+ }
+
+ LOG_INFO("Loaded %zu installed apps", m_installed_apps.size());
+
+ } catch (const std::exception& e) {
+ LOG_ERROR("Error loading app registry: %s", e.what());
+ }
+}
+
+void AppManager::SaveInstalledApps() {
+ std::string registry_path = m_data_root + "/config/apps.json";
+
+ json j;
+ j["version"] = 1;
+ j["apps"] = json::array();
+
+ for (const auto& [id, app] : m_installed_apps) {
+ json app_json;
+ app_json["package_id"] = app.package_id;
+ app_json["name"] = app.name;
+ app_json["version_name"] = app.version_name;
+ app_json["version_code"] = app.version_code;
+ app_json["install_path"] = app.install_path;
+ app_json["permissions"] = app.permissions;
+ app_json["package_size"] = app.package_size;
+ app_json["data_size"] = app.data_size;
+ app_json["is_system_app"] = app.is_system_app;
+ app_json["entry_point"] = app.entry_point;
+ app_json["icon_path"] = app.icon_path;
+ app_json["developer_name"] = app.developer_name;
+
+ // Store timestamps as Unix seconds
+ app_json["installed_at"] = std::chrono::duration_cast(
+ app.installed_at.time_since_epoch()).count();
+ app_json["updated_at"] = std::chrono::duration_cast(
+ app.updated_at.time_since_epoch()).count();
+
+ j["apps"].push_back(app_json);
+ }
+
+ std::ofstream file(registry_path);
+ if (file) {
+ file << j.dump(2);
+ LOG_DEBUG("Saved app registry with %zu apps", m_installed_apps.size());
+ } else {
+ LOG_ERROR("Failed to save app registry");
+ }
+}
+
+int64_t AppManager::CalculateDirectorySize(const std::string& path) const {
+ int64_t size = 0;
+ try {
+ for (const auto& entry : fs::recursive_directory_iterator(path)) {
+ if (entry.is_regular_file()) {
+ size += static_cast(entry.file_size());
+ }
+ }
+ } catch (...) {
+ // Directory may not exist
+ }
+ return size;
+}
+
+std::string AppManager::GenerateUUID() const {
+ std::random_device rd;
+ std::mt19937 gen(rd());
+ std::uniform_int_distribution<> dis(0, 15);
+
+ std::stringstream ss;
+ for (int i = 0; i < 32; ++i) {
+ if (i == 8 || i == 12 || i == 16 || i == 20) {
+ ss << '-';
+ }
+ ss << std::hex << dis(gen);
+ }
+ return ss.str();
+}
+
+} // namespace mosis
diff --git a/src/main/cpp/apps/app_manager.h b/src/main/cpp/apps/app_manager.h
new file mode 100644
index 0000000..c2a452c
--- /dev/null
+++ b/src/main/cpp/apps/app_manager.h
@@ -0,0 +1,167 @@
+// app_manager.h - App installation and management
+// Milestone 10: Device-Side App Management
+#pragma once
+
+#include
+#include
+#include