# 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)
