18 KiB
18 KiB
Milestone 10: Device-Side App Management
Status: Decided Goal: Install, update, and manage apps on Mosis devices.
Decision
C++ AppManager + Lua bindings running on MosisService:
AppManager: C++ class managing installation/updates
Storage: Local device storage (/data/mosis/apps/)
Updates: Background service checking Portal API
UI: App Store system app (RML/Lua)
API: Connects to Portal at portal.mosis.dev (or self-hosted)
Rationale
- Native C++ - AppManager runs in MosisService process for performance
- Background updates - UpdateService thread checks Portal API periodically
- System app - App Store is a privileged RML/Lua app with special permissions
- Ed25519 verification - All packages verified before installation
API Integration
Device Portal (Synology NAS)
┌──────────────┐ ┌──────────────────────┐
│ MosisService │ │ mosis-portal │
│ │ │ │
│ UpdateService├──────GET /store/apps────►│ Chi API Router │
│ │ /updates?pkgs=... │ │
│ │◄─────{updates: [...]}───┤ SQLite portal.db │
│ │ │ │
│ AppManager ├──────GET /packages/...──►│ /volume1/mosis/ │
│ │◄─────[package.mosis]────┤ packages/{dev}/... │
└──────────────┘ └──────────────────────┘
Overview
Device-side app management handles the full lifecycle of apps on user devices: discovery, installation, updates, and removal.
Components
┌─────────────────────────────────────────────────────────────┐
│ MosisService │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │ AppManager │ │ UpdateService │ │ AppStore UI │ │
│ │ (C++ class) │ │ (Background) │ │ (System App) │ │
│ └───────────────┘ └───────────────┘ └───────────────┘ │
│ │ │ │ │
│ └───────────────────┼───────────────────┘ │
│ │ │
│ ┌────────┴────────┐ │
│ │ LuaSandboxManager│ │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Storage Layout
/data/mosis/
├── config/
│ ├── device.json # Device ID, settings
│ └── apps.json # Installed apps registry
├── apps/
│ └── {package_id}/
│ ├── package/ # Extracted app files
│ │ ├── manifest.json
│ │ └── assets/
│ ├── data/ # App data (VirtualFS)
│ ├── cache/ # App cache
│ └── db/ # SQLite databases
├── downloads/ # Temp download location
└── backups/ # App data backups (before update)
AppManager Class
Interface
namespace mosis {
struct InstalledApp {
std::string package_id;
std::string name;
std::string version_name;
int version_code;
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;
int64_t data_size;
};
struct InstallProgress {
enum class Stage {
Downloading,
Verifying,
Extracting,
Registering,
Complete,
Failed
};
Stage stage;
float progress; // 0.0 - 1.0
std::string error;
};
using ProgressCallback = std::function<void(const InstallProgress&)>;
class AppManager {
public:
explicit AppManager(const std::string& data_root);
~AppManager();
// Installation
bool Install(const std::string& package_url,
const std::string& signature,
ProgressCallback callback);
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);
// Queries
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);
// Integration with sandbox
void SetSandboxManager(LuaSandboxManager* manager);
private:
std::string m_data_root;
LuaSandboxManager* m_sandbox_manager = nullptr;
mutable std::mutex m_mutex;
std::map<std::string, InstalledApp> m_installed_apps;
bool VerifyPackage(const std::string& path, const std::string& signature);
bool ExtractPackage(const std::string& path, const std::string& dest);
void LoadInstalledApps();
void SaveInstalledApps();
};
} // namespace mosis
Implementation
bool AppManager::Install(const std::string& package_url,
const std::string& signature,
ProgressCallback callback) {
callback({InstallProgress::Stage::Downloading, 0.0f, ""});
// Download package
std::string download_path = m_data_root + "/downloads/" + GenerateUUID();
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 (!VerifyPackage(download_path, signature)) {
std::filesystem::remove(download_path);
callback({InstallProgress::Stage::Failed, 0.0f, "Signature verification failed"});
return false;
}
// Extract manifest to get package_id
auto manifest = ExtractManifest(download_path);
if (!manifest) {
std::filesystem::remove(download_path);
callback({InstallProgress::Stage::Failed, 0.0f, "Invalid manifest"});
return false;
}
callback({InstallProgress::Stage::Extracting, 0.0f, ""});
// Check if already installed (update path)
std::string install_path = m_data_root + "/apps/" + manifest->package_id;
if (std::filesystem::exists(install_path + "/package")) {
// Backup existing data
BackupAppData(manifest->package_id);
// Remove old package
std::filesystem::remove_all(install_path + "/package");
}
// Extract package
std::filesystem::create_directories(install_path + "/package");
if (!ExtractPackage(download_path, install_path + "/package")) {
callback({InstallProgress::Stage::Failed, 0.0f, "Extraction failed"});
return false;
}
// Clean up download
std::filesystem::remove(download_path);
callback({InstallProgress::Stage::Registering, 0.0f, ""});
// Create data directories
std::filesystem::create_directories(install_path + "/data");
std::filesystem::create_directories(install_path + "/cache");
std::filesystem::create_directories(install_path + "/db");
// Register app
InstalledApp app{
.package_id = manifest->package_id,
.name = manifest->name,
.version_name = manifest->version,
.version_code = manifest->version_code,
.install_path = install_path,
.permissions = manifest->permissions,
.installed_at = std::chrono::system_clock::now(),
.updated_at = std::chrono::system_clock::now(),
.package_size = std::filesystem::file_size(download_path)
};
{
std::lock_guard<std::mutex> lock(m_mutex);
m_installed_apps[manifest->package_id] = app;
SaveInstalledApps();
}
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()) {
return false;
}
// 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;
std::filesystem::remove_all(install_path + "/package");
std::filesystem::remove_all(install_path + "/cache");
if (!keep_data) {
std::filesystem::remove_all(install_path + "/data");
std::filesystem::remove_all(install_path + "/db");
std::filesystem::remove_all(install_path);
}
// Unregister
m_installed_apps.erase(it);
SaveInstalledApps();
return true;
}
Update Service
Background Update Checker
class UpdateService {
public:
UpdateService(AppManager* app_manager, const std::string& api_base);
// Start background checking
void Start(std::chrono::hours interval = std::chrono::hours(24));
void Stop();
// Manual check
std::vector<UpdateInfo> CheckForUpdates();
// Download and install update
bool ApplyUpdate(const std::string& package_id, ProgressCallback callback);
// Settings
void SetAutoUpdate(bool enabled);
void SetWifiOnly(bool wifi_only);
private:
void CheckLoop();
AppManager* m_app_manager;
std::string m_api_base;
std::thread m_check_thread;
std::atomic<bool> m_running{false};
bool m_auto_update = false;
bool m_wifi_only = true;
};
Update Check Flow
1. Get list of installed apps
2. Call API: GET /store/apps/updates?packages=com.a,com.b,com.c
3. API returns list of available updates
4. If auto-update enabled and on WiFi:
- Download and install in background
- Notify user of completed updates
5. If manual:
- Show notification with update count
- User opens App Store to review
App Store System App
UI Screens
Home
├── Featured apps
├── Categories
├── Search
└── My Apps
├── Installed
├── Updates available
└── Previously installed
App Detail
├── Icon, name, developer
├── Screenshots
├── Description
├── Permissions
├── Reviews (future)
└── [Install] / [Update] / [Open]
Settings
├── Auto-update (WiFi only)
├── Storage usage
└── Clear all caches
RML Structure
<!-- app_store/main.rml -->
<rml>
<head>
<link type="text/rcss" href="../../ui/theme.rcss"/>
<link type="text/rcss" href="store.rcss"/>
<script src="store.lua"/>
</head>
<body>
<div class="app-bar">
<h1>App Store</h1>
<div class="search-icon" onclick="showSearch()"/>
</div>
<div id="content">
<!-- Dynamic content loaded by Lua -->
</div>
<div class="bottom-nav">
<div class="nav-item active" onclick="showHome()">
<img src="icons/home.tga"/>
<span>Home</span>
</div>
<div class="nav-item" onclick="showCategories()">
<img src="icons/category.tga"/>
<span>Categories</span>
</div>
<div class="nav-item" onclick="showMyApps()">
<img src="icons/apps.tga"/>
<span>My Apps</span>
</div>
</div>
</body>
</rml>
Lua Store Logic
-- store.lua
local api = require("store_api")
local ui = require("ui")
local state = {
screen = "home",
featured = {},
categories = {},
installed = {},
updates = {}
}
function init()
-- Load installed apps
state.installed = mosis.apps.getInstalled()
-- Fetch featured apps
api.getFeatured(function(apps)
state.featured = apps
render()
end)
-- Check for updates
checkUpdates()
end
function checkUpdates()
local package_ids = {}
for _, app in ipairs(state.installed) do
table.insert(package_ids, app.package_id)
end
api.checkUpdates(package_ids, function(updates)
state.updates = updates
render()
end)
end
function installApp(package_id)
local app = findApp(package_id)
if not app then return end
showProgress(app.name)
mosis.apps.install(app.download_url, app.signature, function(progress)
updateProgress(progress.stage, progress.progress)
if progress.stage == "complete" then
hideProgress()
showToast(app.name .. " installed")
state.installed = mosis.apps.getInstalled()
render()
elseif progress.stage == "failed" then
hideProgress()
showError("Installation failed: " .. progress.error)
end
end)
end
function openApp(package_id)
mosis.apps.launch(package_id)
end
function uninstallApp(package_id)
showConfirm("Uninstall " .. getAppName(package_id) .. "?", function(confirmed)
if confirmed then
mosis.apps.uninstall(package_id)
state.installed = mosis.apps.getInstalled()
render()
end
end)
end
Lua API for Apps
Exposed to System Apps
-- mosis.apps namespace (system apps only)
-- Get installed apps
local apps = mosis.apps.getInstalled()
-- Returns: [{package_id, name, version_name, version_code, installed_at}]
-- Install from store
mosis.apps.install(url, signature, callback)
-- callback(progress): {stage, progress, error}
-- Uninstall
mosis.apps.uninstall(package_id)
-- Launch app
mosis.apps.launch(package_id)
-- Get app info
local info = mosis.apps.getInfo(package_id)
-- Storage management
local size = mosis.apps.getDataSize(package_id)
mosis.apps.clearCache(package_id)
mosis.apps.clearData(package_id)
Exposed to All Apps
-- mosis.app namespace (current app only)
-- Get own package info
local info = mosis.app.info()
-- Returns: {package_id, name, version_name, version_code}
-- Check for update
mosis.app.checkUpdate(function(available, new_version)
if available then
showUpdatePrompt(new_version)
end
end)
-- Open store page for self
mosis.app.openStorePage()
Permissions for App Management
-- Required permission to use mosis.apps.*
permissions = {"system.app_management"}
-- Only granted to:
-- - App Store system app
-- - Settings system app
-- - Other OEM system apps
Installation Intents
From Deep Links
mosis://store/app/com.developer.myapp
mosis://store/install?url=...&sig=...
From App Store
-- User taps Install button
installApp("com.developer.myapp")
From ADB (Development)
adb shell am broadcast -a com.omixlab.mosis.INSTALL_APP \
--es package_path "/sdcard/myapp.mosis"
Security
Package Verification
- Verify ZIP integrity
- Verify Ed25519 signature
- Verify signer is registered developer
- Verify app not in blocklist
- Verify permissions are declared
Installation Sources
| Source | Allowed |
|---|---|
| Official store | Always |
| Developer sideload | If enabled in settings |
| Unknown APK | Never (MosisService only) |
Sandboxing
- All apps run in LuaSandbox
- File access limited to app's data directory
- Network access requires permission
- Hardware access requires permission + user gesture
Deliverables
- Architecture decided (C++ AppManager + Lua bindings)
- AppManager C++ class
- UpdateService background checker
- App Store system app (RML/Lua)
- Lua API bindings (mosis.apps, mosis.app)
- Installation progress UI
- Uninstall confirmation UI
- Storage management UI
- Deep link handling
Open Questions
App backup to cloud?→ Defer to post-MVP (local backups only)Family sharing / multiple devices?→ Defer to post-MVPEnterprise MDM integration?→ Not needed for self-hostedSideloading policy?→ Enabled via Settings toggle (developer mode)