move docs to docs/ folder, merge architecture files, update references
This commit is contained in:
639
docs/DEV_PORTAL_M10_DEVICE.md
Normal file
639
docs/DEV_PORTAL_M10_DEVICE.md
Normal file
@@ -0,0 +1,639 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user