diff --git a/docs/APP-DISCOVERY.md b/docs/APP-DISCOVERY.md new file mode 100644 index 0000000..dc91600 --- /dev/null +++ b/docs/APP-DISCOVERY.md @@ -0,0 +1,184 @@ +# App Discovery System + +Local app discovery without requiring a backend server. + +## Status + +| Feature | Status | +|---------|--------| +| Directory scanning | βœ… Implemented | +| Manifest parsing | βœ… Implemented | +| Home screen rendering | βœ… Implemented | +| App launching | πŸ”„ In Progress | +| Package installation | ⏳ Planned | + +## Overview + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Home Screen β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Dialer β”‚ β”‚ Messagesβ”‚ β”‚ TestApp β”‚ β”‚ MyApp β”‚ ← scanned β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ from β”‚ +β”‚ apps/ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ /data/data/com.omixlab.mosis/files/ β”‚ +β”‚ β”œβ”€β”€ apps/ ← Installed third-party apps β”‚ +β”‚ β”‚ β”œβ”€β”€ com.mosis.testapp/ β”‚ +β”‚ β”‚ β”‚ β”œβ”€β”€ manifest.json ← App metadata β”‚ +β”‚ β”‚ β”‚ β”œβ”€β”€ main.rml ← Entry point β”‚ +β”‚ β”‚ β”‚ β”œβ”€β”€ app.lua β”‚ +β”‚ β”‚ β”‚ └── icon.tga β”‚ +β”‚ β”‚ └── com.example.myapp/ β”‚ +β”‚ β”‚ └── ... β”‚ +β”‚ β”œβ”€β”€ downloads/ ← Pending .mosis packages β”‚ +β”‚ └── config/ β”‚ +β”‚ └── apps.json ← App registry (optional cache) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## Discovery Flow + +### Phase 1: Direct Folder Discovery (MVP) βœ… + +1. **On boot**: Home screen calls `mosis.apps.getInstalled()` +2. **AppManager** scans `/files/apps/` for folders containing `manifest.json` +3. **For each app**: Read manifest, extract name/icon/entry point +4. **Home screen**: Render app grid with icons +5. **On tap**: Launch app via `mosis.apps.launch(package_id)` + +### Phase 2: Package Installation (Future) + +1. User copies `.mosis` file to `/files/downloads/` +2. Store app or file manager shows pending packages +3. User taps "Install" β†’ `mosis.apps.install(path)` +4. Package extracted to `/files/apps/{package_id}/` +5. Home screen refreshes to show new app + +## Implementation + +### AppManager (C++) + +The `AppManager::ScanAppsDirectory()` method scans for installed apps: + +```cpp +// app_manager.cpp +void AppManager::ScanAppsDirectory() { + std::string apps_dir = m_data_root + "/apps"; + for (const auto& entry : fs::directory_iterator(apps_dir)) { + if (!entry.is_directory()) continue; + + // Look for manifest.json + std::string manifest_path = entry.path().string() + "/manifest.json"; + // Parse and register app... + } +} +``` + +### Lua API + +```lua +-- Get all installed apps (system + third-party) +local apps = mosis.apps.getInstalled() +-- Returns: [{package_id, name, icon_path, entry, version, is_system_app}, ...] + +-- Launch an app +mosis.apps.launch("com.mosis.testapp") + +-- Check if app is running +local running = mosis.apps.isRunning("com.mosis.testapp") +``` + +### Home Screen (home.lua) + +The home screen dynamically renders third-party apps: + +```lua +function initHome(doc) + -- Get installed apps, filter to third-party only + local apps = mosis.apps.getInstalled() + for _, app in ipairs(apps) do + if not app.is_system_app then + -- Render app icon + end + end +end + +function launchThirdPartyApp(package_id) + mosis.apps.launch(package_id) +end +``` + +## Test App Deployment + +To deploy a test app manually: + +```bash +# 1. Push to temp location (avoid permission issues) +adb push test-apps/com.mosis.sandbox-test/ //data/local/tmp/com.mosis.sandbox-test/ + +# 2. Copy to app's private storage +adb shell "run-as com.omixlab.mosis cp -r /data/local/tmp/com.mosis.sandbox-test /data/data/com.omixlab.mosis/files/apps/" + +# 3. Verify +adb shell "run-as com.omixlab.mosis ls /data/data/com.omixlab.mosis/files/apps/com.mosis.sandbox-test/" +``` + +For Desktop Designer: +```bash +# Copy to designer's sandbox_data folder +cp -r test-apps/com.mosis.sandbox-test/ designer/build/Debug/sandbox_data/apps/ +``` + +## App Manifest Format + +```json +{ + "id": "com.mosis.testapp", + "name": "Test App", + "version": "1.0.0", + "version_code": 1, + "entry": "main.rml", + "icon": "icon.tga", + "description": "A test application", + "permissions": ["storage", "network"] +} +``` + +## Directory Structure + +### System Apps (bundled in APK assets) +``` +assets/apps/ +β”œβ”€β”€ home/ # Home screen (always loaded) +β”œβ”€β”€ dialer/ +β”œβ”€β”€ messages/ +β”œβ”€β”€ contacts/ +β”œβ”€β”€ settings/ +β”œβ”€β”€ browser/ +└── store/ +``` + +### Third-Party Apps (in private storage) +``` +/data/data/com.omixlab.mosis/files/apps/ +β”œβ”€β”€ com.mosis.testapp/ +β”‚ β”œβ”€β”€ manifest.json +β”‚ β”œβ”€β”€ main.rml +β”‚ β”œβ”€β”€ app.lua +β”‚ β”œβ”€β”€ styles.rcss +β”‚ └── icon.tga +└── com.example.game/ + └── ... +``` + +## Future Enhancements + +1. **Package signature verification** before installation +2. **Version tracking** in apps.json registry +3. **Update detection** by comparing version_code +4. **Uninstall** with data cleanup option +5. **Store integration** for HTTP-based discovery diff --git a/src/main/assets/apps/home/home.lua b/src/main/assets/apps/home/home.lua new file mode 100644 index 0000000..dca426b --- /dev/null +++ b/src/main/assets/apps/home/home.lua @@ -0,0 +1,151 @@ +-- home.lua - Home screen dynamic app rendering +-- Handles system apps and discovered third-party apps + +-- System apps with their navigation keys and colors +local system_apps = { + -- Row 1 + {name = "Phone", icon = "phone", color = "#4CAF50", nav = "dialer"}, + {name = "Messages", icon = "message", color = "#2196F3", nav = "messages"}, + {name = "Contacts", icon = "contacts", color = "#FF9800", nav = "contacts"}, + {name = "Browser", icon = "browser", color = "#F44336", nav = "browser"}, + -- Row 2 + {name = "Gallery", icon = "gallery", color = "#9C27B0", nav = nil}, + {name = "Camera", icon = "camera", color = "#00BCD4", nav = "camera"}, + {name = "Settings", icon = "settings", color = "#607D8B", nav = "settings"}, + {name = "Music", icon = "music", color = "#E91E63", nav = "music"}, + -- Row 3 + {name = "Calendar", icon = "calendar", color = "#3F51B5", nav = nil}, + {name = "Clock", icon = "clock", color = "#009688", nav = nil}, + {name = "Notes", icon = "notes", color = "#795548", nav = nil}, + {name = "Maps", icon = "maps", color = "#FF5722", nav = nil}, + -- Row 4 + {name = "Store", icon = "store", color = "#8BC34A", nav = "store"}, + {name = "Files", icon = "files", color = "#CDDC39", nav = nil}, + {name = "Calculator", icon = "calculator", color = "#FFC107", nav = nil}, + {name = "Weather", icon = "weather", color = "#673AB7", nav = nil}, +} + +-- State +local installed_apps = {} +local home_document = nil -- Store document reference + +-- Initialize on load (receives document from onload event) +function initHome(doc) + print("[Home] Initializing home screen...") + home_document = doc + + -- Get installed third-party apps + if mosis and mosis.apps then + installed_apps = mosis.apps.getInstalled() or {} + print("[Home] Found " .. #installed_apps .. " installed apps") + + -- Filter to only third-party (non-system) apps + local third_party = {} + for _, app in ipairs(installed_apps) do + if not app.is_system_app then + table.insert(third_party, app) + print("[Home] Third-party app: " .. app.name .. " (" .. app.package_id .. ")") + end + end + installed_apps = third_party + else + print("[Home] Warning: mosis.apps API not available") + installed_apps = {} + end + + -- Render dynamic apps + renderThirdPartyApps() +end + +-- Generate a color based on package_id +function getAppColor(package_id) + local colors = { + "#BB86FC", "#03DAC6", "#FF9800", "#2196F3", + "#4CAF50", "#F44336", "#E91E63", "#3F51B5", + "#009688", "#795548", "#FF5722", "#673AB7" + } + + -- Simple hash of package_id to pick a color + local hash = 0 + for i = 1, #package_id do + hash = hash + package_id:byte(i) + end + + return colors[(hash % #colors) + 1] +end + +-- Get first letter for placeholder icon +function getAppInitial(name) + return name:sub(1, 1):upper() +end + +-- Render third-party apps into the grid +function renderThirdPartyApps() + -- Use stored document reference + if not home_document then + print("[Home] Could not get document reference") + return + end + + local grid = home_document:GetElementById("third-party-apps") + if not grid then + print("[Home] third-party-apps container not found") + return + end + + -- Clear existing content + grid.inner_rml = "" + + if #installed_apps == 0 then + print("[Home] No third-party apps to display") + return + end + + -- Build HTML for each app + local html = "" + for _, app in ipairs(installed_apps) do + local color = getAppColor(app.package_id) + local initial = getAppInitial(app.name) + local icon_html + + -- Check if app has an icon + if app.icon and app.icon ~= "" then + -- Third-party app icon path would be in their install directory + -- For now, use initial as we need file:// protocol support + icon_html = '' .. initial .. '' + else + icon_html = '' .. initial .. '' + end + + html = html .. [[ +
+
+ ]] .. icon_html .. [[ +
+ ]] .. app.name .. [[ +
+ ]] + end + + grid.inner_rml = html + print("[Home] Rendered " .. #installed_apps .. " third-party apps") +end + +-- Launch a third-party app +function launchThirdPartyApp(package_id) + print("[Home] Launching app: " .. package_id) + + if mosis and mosis.apps then + local success = mosis.apps.launch(package_id) + if success then + print("[Home] App launched: " .. package_id) + else + print("[Home] Failed to launch app: " .. package_id) + end + else + print("[Home] Cannot launch app: mosis.apps not available") + end +end + +-- initHome() is called via onload in home.rml diff --git a/src/main/assets/apps/home/home.rml b/src/main/assets/apps/home/home.rml index ca70240..b42c94b 100644 --- a/src/main/assets/apps/home/home.rml +++ b/src/main/assets/apps/home/home.rml @@ -4,6 +4,7 @@ + Virtual Smartphone - Home - +
12:30 @@ -121,6 +156,11 @@
Weather
+ + +
+ +
diff --git a/src/main/cpp/apps/app_manager.cpp b/src/main/cpp/apps/app_manager.cpp index e42df85..deaf25e 100644 --- a/src/main/cpp/apps/app_manager.cpp +++ b/src/main/cpp/apps/app_manager.cpp @@ -566,12 +566,93 @@ bool AppManager::DownloadFile(const std::string& url, const std::string& dest_pa return false; } +void AppManager::ScanAppsDirectory() { + std::string apps_dir = m_data_root + "/apps"; + + if (!fs::exists(apps_dir)) { + return; + } + + LOG_INFO("Scanning apps directory: %s", apps_dir.c_str()); + + for (const auto& entry : fs::directory_iterator(apps_dir)) { + if (!entry.is_directory()) { + continue; + } + + std::string package_id = entry.path().filename().string(); + + // Skip if already registered + if (m_installed_apps.find(package_id) != m_installed_apps.end()) { + continue; + } + + // Look for manifest.json in direct folder or package/ subfolder + std::string manifest_path = entry.path().string() + "/manifest.json"; + std::string manifest_path_pkg = entry.path().string() + "/package/manifest.json"; + + std::string actual_manifest; + std::string app_base_path; + + if (fs::exists(manifest_path)) { + actual_manifest = manifest_path; + app_base_path = entry.path().string(); + } else if (fs::exists(manifest_path_pkg)) { + actual_manifest = manifest_path_pkg; + app_base_path = entry.path().string() + "/package"; + } else { + continue; + } + + // Parse manifest + std::ifstream mf(actual_manifest); + if (!mf) { + continue; + } + + try { + json j; + mf >> j; + + InstalledApp app; + app.package_id = j.value("id", package_id); + app.name = j.value("name", package_id); + app.version_name = j.value("version", "1.0.0"); + app.version_code = j.value("version_code", 1); + app.install_path = entry.path().string(); + app.entry_point = j.value("entry", "main.rml"); + app.icon_path = j.value("icon", ""); + app.is_system_app = false; + + if (j.contains("developer")) { + app.developer_name = j["developer"].value("name", ""); + } + + if (j.contains("permissions") && j["permissions"].is_array()) { + for (const auto& perm : j["permissions"]) { + app.permissions.push_back(perm.get()); + } + } + + app.installed_at = std::chrono::system_clock::now(); + app.updated_at = std::chrono::system_clock::now(); + + m_installed_apps[app.package_id] = app; + LOG_INFO("Discovered app: %s (%s)", app.name.c_str(), app.package_id.c_str()); + + } catch (const std::exception& e) { + LOG_WARN("Failed to parse manifest for %s: %s", package_id.c_str(), e.what()); + } + } +} + 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"); + LOG_INFO("No existing app registry found, scanning directory..."); + ScanAppsDirectory(); return; } @@ -618,11 +699,15 @@ void AppManager::LoadInstalledApps() { } } - LOG_INFO("Loaded %zu installed apps", m_installed_apps.size()); + LOG_INFO("Loaded %zu apps from registry", m_installed_apps.size()); } catch (const std::exception& e) { LOG_ERROR("Error loading app registry: %s", e.what()); } + + // Always scan directory to discover newly copied apps + ScanAppsDirectory(); + LOG_INFO("Total installed apps: %zu", m_installed_apps.size()); } void AppManager::SaveInstalledApps() { diff --git a/src/main/cpp/apps/app_manager.h b/src/main/cpp/apps/app_manager.h index c2a452c..1f3e108 100644 --- a/src/main/cpp/apps/app_manager.h +++ b/src/main/cpp/apps/app_manager.h @@ -152,6 +152,9 @@ private: void LoadInstalledApps(); void SaveInstalledApps(); + // Directory scanning for app discovery + void ScanAppsDirectory(); + // Directory size calculation int64_t CalculateDirectorySize(const std::string& path) const;