640 lines
18 KiB
Markdown
640 lines
18 KiB
Markdown
# 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
|
|
|
|
1. **Native C++** - AppManager runs in MosisService process for performance
|
|
2. **Background updates** - UpdateService thread checks Portal API periodically
|
|
3. **System app** - App Store is a privileged RML/Lua app with special permissions
|
|
4. **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
|
|
|
|
```cpp
|
|
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
|
|
|
|
```cpp
|
|
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
|
|
|
|
```cpp
|
|
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
|
|
|
|
```xml
|
|
<!-- 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
|
|
|
|
```lua
|
|
-- 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
|
|
|
|
```lua
|
|
-- 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
|
|
|
|
```lua
|
|
-- 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
|
|
|
|
```lua
|
|
-- 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
|
|
|
|
```lua
|
|
-- User taps Install button
|
|
installApp("com.developer.myapp")
|
|
```
|
|
|
|
### From ADB (Development)
|
|
|
|
```bash
|
|
adb shell am broadcast -a com.omixlab.mosis.INSTALL_APP \
|
|
--es package_path "/sdcard/myapp.mosis"
|
|
```
|
|
|
|
---
|
|
|
|
## Security
|
|
|
|
### Package Verification
|
|
|
|
1. Verify ZIP integrity
|
|
2. Verify Ed25519 signature
|
|
3. Verify signer is registered developer
|
|
4. Verify app not in blocklist
|
|
5. 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
|
|
|
|
- [x] 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
|
|
|
|
1. ~~App backup to cloud?~~ → Defer to post-MVP (local backups only)
|
|
2. ~~Family sharing / multiple devices?~~ → Defer to post-MVP
|
|
3. ~~Enterprise MDM integration?~~ → Not needed for self-hosted
|
|
4. ~~Sideloading policy?~~ → Enabled via Settings toggle (developer mode)
|
|
|
|
---
|
|
|
|
## References
|
|
|
|
- [Android PackageManager](https://developer.android.com/reference/android/content/pm/PackageManager)
|
|
- [iOS App Installation](https://developer.apple.com/documentation/devicemanagement/installing_apps)
|