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 @@
-
+ -
+ -
+
+ + + + + + + + + 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 +#include +#include +#include +#include +#include + +namespace mosis { + +// Forward declarations +class LuaSandboxManager; + +// Information about an installed app +struct InstalledApp { + std::string package_id; + std::string name; + std::string version_name; + int version_code = 0; + std::string install_path; + std::vector permissions; + std::chrono::system_clock::time_point installed_at; + std::chrono::system_clock::time_point updated_at; + int64_t package_size = 0; + int64_t data_size = 0; + bool is_system_app = false; + std::string entry_point; + std::string icon_path; + std::string developer_name; +}; + +// Progress stages during installation +struct InstallProgress { + enum class Stage { + Downloading, + Verifying, + Extracting, + Registering, + Complete, + Failed + }; + + Stage stage = Stage::Downloading; + float progress = 0.0f; // 0.0 - 1.0 + std::string error; + + static const char* StageName(Stage s) { + switch (s) { + case Stage::Downloading: return "downloading"; + case Stage::Verifying: return "verifying"; + case Stage::Extracting: return "extracting"; + case Stage::Registering: return "registering"; + case Stage::Complete: return "complete"; + case Stage::Failed: return "failed"; + default: return "unknown"; + } + } +}; + +using ProgressCallback = std::function; + +// Manifest parsed from package +struct AppManifest { + std::string id; + std::string name; + std::string version; + int version_code = 0; + std::string entry; + std::string icon; + std::string description; + std::string developer_name; + std::string developer_email; + std::vector permissions; + int min_api_version = 1; +}; + +class AppManager { +public: + explicit AppManager(const std::string& data_root); + ~AppManager(); + + // Prevent copying + AppManager(const AppManager&) = delete; + AppManager& operator=(const AppManager&) = delete; + + // Installation from URL + bool Install(const std::string& package_url, + const std::string& signature, + ProgressCallback callback); + + // Installation from local file + bool InstallFromFile(const std::string& package_path, + ProgressCallback callback); + + // Uninstallation + bool Uninstall(const std::string& package_id, bool keep_data = false); + + // Updates + bool Update(const std::string& package_id, + const std::string& package_url, + const std::string& signature, + ProgressCallback callback); + + // Query installed apps + std::vector GetInstalledApps() const; + std::optional GetApp(const std::string& package_id) const; + bool IsInstalled(const std::string& package_id) const; + + // Data management + int64_t GetAppDataSize(const std::string& package_id) const; + bool ClearAppData(const std::string& package_id); + bool ClearAppCache(const std::string& package_id); + bool BackupAppData(const std::string& package_id); + bool RestoreAppData(const std::string& package_id); + + // App launching + bool LaunchApp(const std::string& package_id); + bool StopApp(const std::string& package_id); + bool IsAppRunning(const std::string& package_id) const; + + // Integration with sandbox manager + void SetSandboxManager(LuaSandboxManager* manager); + + // Get paths + std::string GetDataRoot() const { return m_data_root; } + std::string GetAppPath(const std::string& package_id) const; + std::string GetAppDataPath(const std::string& package_id) const; + std::string GetAppCachePath(const std::string& package_id) const; + + // System apps registration + void RegisterSystemApp(const InstalledApp& app); + +private: + // Package verification + bool VerifyPackage(const std::string& path); + bool VerifySignature(const std::string& path, const std::string& signature); + + // Package operations + std::optional ExtractManifest(const std::string& package_path); + bool ExtractPackage(const std::string& package_path, const std::string& dest_path); + + // Download helper + bool DownloadFile(const std::string& url, const std::string& dest_path, + std::function progress_callback); + + // Registry persistence + void LoadInstalledApps(); + void SaveInstalledApps(); + + // Directory size calculation + int64_t CalculateDirectorySize(const std::string& path) const; + + // Generate unique ID + std::string GenerateUUID() const; + + std::string m_data_root; + LuaSandboxManager* m_sandbox_manager = nullptr; + mutable std::mutex m_mutex; + std::map m_installed_apps; +}; + +} // namespace mosis diff --git a/src/main/cpp/apps/update_service.cpp b/src/main/cpp/apps/update_service.cpp new file mode 100644 index 0000000..50aa13e --- /dev/null +++ b/src/main/cpp/apps/update_service.cpp @@ -0,0 +1,288 @@ +// update_service.cpp - Background app update checker implementation +// Milestone 10: Device-Side App Management + +#include "update_service.h" +#include "../logger.h" + +#include +#include +#include + +using json = nlohmann::json; + +namespace mosis { + +UpdateService::UpdateService(AppManager* app_manager, const std::string& api_base) + : m_app_manager(app_manager) + , m_api_base(api_base) +{ + LOG_INFO("UpdateService initialized with API: %s", api_base.c_str()); +} + +UpdateService::~UpdateService() { + Stop(); +} + +void UpdateService::Start(std::chrono::hours interval) { + if (m_running) { + LOG_WARN("UpdateService already running"); + return; + } + + m_check_interval = interval; + m_running = true; + m_stop_requested = false; + + m_check_thread = std::thread(&UpdateService::CheckLoop, this); + + LOG_INFO("UpdateService started with %lld hour interval", + static_cast(interval.count())); +} + +void UpdateService::Stop() { + if (!m_running) { + return; + } + + m_stop_requested = true; + + if (m_check_thread.joinable()) { + m_check_thread.join(); + } + + m_running = false; + LOG_INFO("UpdateService stopped"); +} + +std::vector UpdateService::CheckForUpdates() { + std::vector updates; + + // Get list of installed apps + auto installed = m_app_manager->GetInstalledApps(); + if (installed.empty()) { + LOG_DEBUG("No apps installed, skipping update check"); + return updates; + } + + // Build query string with package IDs and versions + std::stringstream package_params; + for (size_t i = 0; i < installed.size(); ++i) { + if (i > 0) package_params << ","; + package_params << installed[i].package_id << ":" << installed[i].version_code; + } + + // Call API + std::string url = m_api_base + "/store/apps/updates?packages=" + package_params.str(); + LOG_DEBUG("Checking for updates: %s", url.c_str()); + + std::string response = HttpGet(url); + if (response.empty()) { + LOG_WARN("Update check failed: no response from server"); + return updates; + } + + // Parse response + try { + json j = json::parse(response); + + if (j.contains("updates") && j["updates"].is_array()) { + for (const auto& update_json : j["updates"]) { + UpdateInfo info; + info.package_id = update_json.value("package_id", ""); + info.name = update_json.value("name", ""); + info.new_version = update_json.value("version", ""); + info.new_version_code = update_json.value("version_code", 0); + info.download_url = update_json.value("download_url", ""); + info.signature = update_json.value("signature", ""); + info.download_size = update_json.value("size", 0); + info.release_notes = update_json.value("release_notes", ""); + info.is_critical = update_json.value("critical", false); + + // Get current version from installed apps + auto current = m_app_manager->GetApp(info.package_id); + if (current) { + info.current_version = current->version_name; + info.current_version_code = current->version_code; + + // Only add if actually newer + if (info.new_version_code > info.current_version_code) { + updates.push_back(info); + } + } + } + } + + LOG_INFO("Found %zu available updates", updates.size()); + + } catch (const json::exception& e) { + LOG_ERROR("Failed to parse update response: %s", e.what()); + } + + // Update last check time + m_last_check = std::chrono::system_clock::now(); + + // Store pending updates + { + std::lock_guard lock(m_mutex); + m_pending_updates = updates; + } + + return updates; +} + +void UpdateService::CheckForUpdatesAsync(UpdateCheckCallback callback) { + std::thread([this, callback]() { + auto updates = CheckForUpdates(); + if (callback) { + callback(updates); + } + }).detach(); +} + +bool UpdateService::ApplyUpdate(const std::string& package_id, ProgressCallback callback) { + // Find update info + UpdateInfo update_info; + bool found = false; + + { + std::lock_guard lock(m_mutex); + for (const auto& update : m_pending_updates) { + if (update.package_id == package_id) { + update_info = update; + found = true; + break; + } + } + } + + if (!found) { + LOG_ERROR("No pending update found for: %s", package_id.c_str()); + if (callback) { + callback({InstallProgress::Stage::Failed, 0.0f, "No update available"}); + } + return false; + } + + LOG_INFO("Applying update: %s %s -> %s", + package_id.c_str(), + update_info.current_version.c_str(), + update_info.new_version.c_str()); + + // Use AppManager to download and install + bool success = m_app_manager->Update(package_id, + update_info.download_url, + update_info.signature, + callback); + + if (success) { + // Remove from pending updates + std::lock_guard lock(m_mutex); + m_pending_updates.erase( + std::remove_if(m_pending_updates.begin(), m_pending_updates.end(), + [&](const UpdateInfo& u) { return u.package_id == package_id; }), + m_pending_updates.end()); + } + + return success; +} + +void UpdateService::ApplyUpdateAsync(const std::string& package_id, + UpdateAppliedCallback callback) { + std::thread([this, package_id, callback]() { + bool success = ApplyUpdate(package_id, nullptr); + if (callback) { + callback(package_id, success); + } + }).detach(); +} + +void UpdateService::ApplyAllUpdates(UpdateAppliedCallback callback) { + std::vector updates; + { + std::lock_guard lock(m_mutex); + updates = m_pending_updates; + } + + for (const auto& update : updates) { + bool success = ApplyUpdate(update.package_id, nullptr); + if (callback) { + callback(update.package_id, success); + } + } +} + +std::vector UpdateService::GetPendingUpdates() const { + std::lock_guard lock(m_mutex); + return m_pending_updates; +} + +bool UpdateService::HasPendingUpdates() const { + std::lock_guard lock(m_mutex); + return !m_pending_updates.empty(); +} + +size_t UpdateService::GetPendingUpdateCount() const { + std::lock_guard lock(m_mutex); + return m_pending_updates.size(); +} + +void UpdateService::CheckLoop() { + LOG_DEBUG("UpdateService check loop started"); + + // Wait for initial delay before first check + auto wait_until = std::chrono::system_clock::now() + std::chrono::minutes(5); + + while (!m_stop_requested) { + // Wait until next check time or stop requested + auto now = std::chrono::system_clock::now(); + if (now < wait_until) { + std::this_thread::sleep_for(std::chrono::seconds(1)); + continue; + } + + // Check if on WiFi (if required) + if (m_wifi_only && !IsOnWifi()) { + LOG_DEBUG("Skipping update check: not on WiFi"); + wait_until = std::chrono::system_clock::now() + std::chrono::minutes(5); + continue; + } + + // Perform update check + LOG_DEBUG("Performing scheduled update check"); + auto updates = CheckForUpdates(); + + // Notify callback if updates available + if (!updates.empty() && m_on_updates_available) { + m_on_updates_available(updates); + } + + // Auto-update if enabled + if (m_auto_update && !updates.empty()) { + if (!m_wifi_only || IsOnWifi()) { + LOG_INFO("Auto-updating %zu apps", updates.size()); + ApplyAllUpdates(nullptr); + } + } + + // Schedule next check + wait_until = std::chrono::system_clock::now() + m_check_interval; + } + + LOG_DEBUG("UpdateService check loop ended"); +} + +std::string UpdateService::HttpGet(const std::string& url) { + // TODO: Implement HTTP GET using platform-specific APIs + // On Android, this would use JNI to call Java HttpURLConnection + // For now, return empty string as placeholder + LOG_WARN("HTTP GET not yet implemented for: %s", url.c_str()); + return ""; +} + +bool UpdateService::IsOnWifi() const { + // TODO: Implement WiFi check using platform-specific APIs + // For now, assume always on WiFi + return true; +} + +} // namespace mosis diff --git a/src/main/cpp/apps/update_service.h b/src/main/cpp/apps/update_service.h new file mode 100644 index 0000000..e6c581e --- /dev/null +++ b/src/main/cpp/apps/update_service.h @@ -0,0 +1,109 @@ +// update_service.h - Background app update checker +// Milestone 10: Device-Side App Management +#pragma once + +#include "app_manager.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace mosis { + +// Information about an available update +struct UpdateInfo { + std::string package_id; + std::string name; + std::string current_version; + std::string new_version; + int current_version_code = 0; + int new_version_code = 0; + std::string download_url; + std::string signature; + int64_t download_size = 0; + std::string release_notes; + bool is_critical = false; +}; + +// Callback for update events +using UpdateCheckCallback = std::function& updates)>; +using UpdateAppliedCallback = std::function; + +class UpdateService { +public: + UpdateService(AppManager* app_manager, const std::string& api_base); + ~UpdateService(); + + // Prevent copying + UpdateService(const UpdateService&) = delete; + UpdateService& operator=(const UpdateService&) = delete; + + // Start background update checking + void Start(std::chrono::hours interval = std::chrono::hours(24)); + void Stop(); + bool IsRunning() const { return m_running; } + + // Manual update check + std::vector CheckForUpdates(); + void CheckForUpdatesAsync(UpdateCheckCallback callback); + + // Download and install update + bool ApplyUpdate(const std::string& package_id, ProgressCallback callback); + void ApplyUpdateAsync(const std::string& package_id, UpdateAppliedCallback callback); + + // Apply all available updates + void ApplyAllUpdates(UpdateAppliedCallback callback); + + // Get pending updates (from last check) + std::vector GetPendingUpdates() const; + bool HasPendingUpdates() const; + size_t GetPendingUpdateCount() const; + + // Settings + void SetAutoUpdate(bool enabled) { m_auto_update = enabled; } + bool GetAutoUpdate() const { return m_auto_update; } + + void SetWifiOnly(bool wifi_only) { m_wifi_only = wifi_only; } + bool GetWifiOnly() const { return m_wifi_only; } + + void SetCheckInterval(std::chrono::hours interval) { m_check_interval = interval; } + std::chrono::hours GetCheckInterval() const { return m_check_interval; } + + // Callbacks + void SetOnUpdatesAvailable(UpdateCheckCallback callback) { m_on_updates_available = callback; } + + // Get last check time + std::chrono::system_clock::time_point GetLastCheckTime() const { return m_last_check; } + +private: + // Background thread function + void CheckLoop(); + + // HTTP request helpers + std::string HttpGet(const std::string& url); + bool IsOnWifi() const; + + AppManager* m_app_manager; + std::string m_api_base; + + std::thread m_check_thread; + std::atomic m_running{false}; + std::atomic m_stop_requested{false}; + + std::chrono::hours m_check_interval{24}; + std::chrono::system_clock::time_point m_last_check; + + bool m_auto_update = false; + bool m_wifi_only = true; + + mutable std::mutex m_mutex; + std::vector m_pending_updates; + + UpdateCheckCallback m_on_updates_available; +}; + +} // namespace mosis