add device-side app management with AppManager, UpdateService and App Store UI (M10)

This commit is contained in:
2026-01-18 22:33:15 +01:00
parent 03556ff1d4
commit 5ea0cdde63
8 changed files with 2343 additions and 3 deletions

View File

@@ -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()

View File

@@ -4,6 +4,7 @@
<link type="text/rcss" href="../../ui/theme.rcss"/>
<link type="text/rcss" href="../../ui/components.rcss"/>
<script src="../../scripts/navigation.lua"></script>
<script src="store.lua"></script>
<title>Store</title>
<style>
.store-screen {
@@ -296,6 +297,121 @@
.bg-red { background-color: #F44336; }
.bg-pink { background-color: #E91E63; }
.bg-indigo { background-color: #3F51B5; }
/* Dialog Overlay */
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.dialog {
background-color: #2D2D2D;
border-radius: 16px;
padding: 24px;
min-width: 280px;
max-width: 320px;
}
.dialog-title {
font-size: 20px;
font-weight: 600;
color: #FFFFFF;
margin-bottom: 16px;
}
.dialog-message {
font-size: 16px;
color: #B3B3B3;
margin-bottom: 24px;
line-height: 1.4;
}
.dialog-status {
font-size: 14px;
color: #B3B3B3;
margin-top: 12px;
text-align: center;
}
.dialog-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
}
.dialog-btn {
background-color: transparent;
color: #BB86FC;
font-size: 16px;
font-weight: 600;
padding: 10px 20px;
border-radius: 8px;
cursor: pointer;
}
.dialog-btn:hover {
background-color: rgba(187, 134, 252, 0.1);
}
/* Progress Bar */
.progress-container {
width: 100%;
height: 4px;
background-color: #1E1E1E;
border-radius: 2px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background-color: #BB86FC;
border-radius: 2px;
transition: width 0.3s ease;
}
/* Toast */
.toast {
position: fixed;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
background-color: #323232;
color: #FFFFFF;
font-size: 14px;
padding: 12px 24px;
border-radius: 8px;
z-index: 1001;
}
/* Badge */
.badge {
position: absolute;
top: 4px;
right: 4px;
background-color: #F44336;
color: #FFFFFF;
font-size: 12px;
font-weight: 600;
min-width: 18px;
height: 18px;
border-radius: 9px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 4px;
}
.store-nav-item {
position: relative;
}
</style>
</head>
<body class="store-screen">
@@ -471,18 +587,44 @@
<!-- Bottom Navigation -->
<div class="store-bottom-nav">
<div class="store-nav-item active">
<div id="nav-apps" class="store-nav-item active" onclick="showHome()">
<img src="../../icons/home.tga"/>
<span>Apps</span>
</div>
<div class="store-nav-item">
<div id="nav-games" class="store-nav-item" onclick="showGames()">
<img src="../../icons/game.tga"/>
<span>Games</span>
</div>
<div class="store-nav-item">
<div id="nav-updates" class="store-nav-item" onclick="showUpdates()">
<img src="../../icons/download.tga"/>
<span>Updates</span>
<div id="updates-badge" class="badge" style="display: none;"></div>
</div>
</div>
<!-- Progress Dialog (hidden by default) -->
<div id="progress-dialog" class="dialog-overlay" style="display: none;">
<div class="dialog">
<div id="progress-title" class="dialog-title">Installing...</div>
<div class="progress-container">
<div id="progress-bar" class="progress-bar" style="width: 0%;"></div>
</div>
<div id="progress-status" class="dialog-status">Preparing...</div>
</div>
</div>
<!-- Error Dialog (hidden by default) -->
<div id="error-dialog" class="dialog-overlay" style="display: none;">
<div class="dialog">
<div class="dialog-title">Error</div>
<div id="error-message" class="dialog-message"></div>
<div class="dialog-actions">
<div class="dialog-btn" onclick="hideError()">OK</div>
</div>
</div>
</div>
<!-- Toast (hidden by default) -->
<div id="toast" class="toast" style="display: none;"></div>
</body>
</rml>

View File

@@ -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 <lua.hpp>
#include <string>
#include <chrono>
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<AppManager*>(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<UpdateService*>(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<int>(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<std::chrono::seconds>(
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<int>(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<int>(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

View File

@@ -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

View File

@@ -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 <fstream>
#include <sstream>
#include <filesystem>
#include <random>
#include <iomanip>
#include <ctime>
// For JSON parsing
#include <nlohmann/json.hpp>
// For ZIP extraction
#include <minizip/unzip.h>
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<int64_t>(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<std::mutex> 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<std::mutex> 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<InstalledApp> AppManager::GetInstalledApps() const {
std::lock_guard<std::mutex> lock(m_mutex);
std::vector<InstalledApp> apps;
apps.reserve(m_installed_apps.size());
for (const auto& [id, app] : m_installed_apps) {
apps.push_back(app);
}
return apps;
}
std::optional<InstalledApp> AppManager::GetApp(const std::string& package_id) const {
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> 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<AppManifest> 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<std::string>());
}
}
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<void(float)> 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<std::string>());
}
}
// Parse timestamps
if (app_json.contains("installed_at")) {
auto ts = app_json["installed_at"].get<int64_t>();
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<int64_t>();
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<std::chrono::seconds>(
app.installed_at.time_since_epoch()).count();
app_json["updated_at"] = std::chrono::duration_cast<std::chrono::seconds>(
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<int64_t>(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

View File

@@ -0,0 +1,167 @@
// app_manager.h - App installation and management
// Milestone 10: Device-Side App Management
#pragma once
#include <string>
#include <vector>
#include <map>
#include <optional>
#include <functional>
#include <mutex>
#include <chrono>
#include <cstdint>
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<std::string> 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<void(const InstallProgress&)>;
// 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<std::string> 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<InstalledApp> GetInstalledApps() const;
std::optional<InstalledApp> 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<AppManifest> 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<void(float)> 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<std::string, InstalledApp> m_installed_apps;
};
} // namespace mosis

View File

@@ -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 <nlohmann/json.hpp>
#include <sstream>
#include <condition_variable>
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<long long>(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<UpdateInfo> UpdateService::CheckForUpdates() {
std::vector<UpdateInfo> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<UpdateInfo> updates;
{
std::lock_guard<std::mutex> 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<UpdateInfo> UpdateService::GetPendingUpdates() const {
std::lock_guard<std::mutex> lock(m_mutex);
return m_pending_updates;
}
bool UpdateService::HasPendingUpdates() const {
std::lock_guard<std::mutex> lock(m_mutex);
return !m_pending_updates.empty();
}
size_t UpdateService::GetPendingUpdateCount() const {
std::lock_guard<std::mutex> 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

View File

@@ -0,0 +1,109 @@
// update_service.h - Background app update checker
// Milestone 10: Device-Side App Management
#pragma once
#include "app_manager.h"
#include <string>
#include <vector>
#include <thread>
#include <atomic>
#include <chrono>
#include <functional>
#include <mutex>
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<void(const std::vector<UpdateInfo>& updates)>;
using UpdateAppliedCallback = std::function<void(const std::string& package_id, bool success)>;
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<UpdateInfo> 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<UpdateInfo> 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<bool> m_running{false};
std::atomic<bool> 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<UpdateInfo> m_pending_updates;
UpdateCheckCallback m_on_updates_available;
};
} // namespace mosis